Version 1.2 by Marius Dumitru Florea on 2015/07/28

Show last authors
1 == Agenda ==
2
3 {{toc depth="1" /}}
4
5 = Use Case =
6
7 Suppose you want to display the Date column from the Document Index live table as **time ago**. So instead of showing "**2015/07/17 15:44**" you would like to display "**2 days ago**". Of course, you can do this from the server side, but for the purpose of this tutorial we will achieve this using a JavaScript library called [[**Moment.js**>>http://momentjs.com/]]. It can parse, validate, manipulate, and display dates from JavaScript.
8
9 = Integration Options =
10
11 There are several ways we can integrate Moment.js in XWiki:
12
13 1. copy moment.js somewhere in /resources(((
14 {{code language="none"}}
15 $xwiki.jsfx.use('path/to/moment.js')
16 {{/code}}
17 )))
18 1*. you need file system access
19 1*. it leads to a custom XWiki WAR and thus upgrade complexity
20 1*. Extension Manager doesn't support installing resources in the WAR
21 1. attach moment.js to a wiki page(((
22 {{code language="html"}}
23 <script src="$xwiki.getAttachmentURL('Demo.MomentJS', 'moment.js'"
24 type="text/javascript"></script>
25 {{/code}}
26 )))
27 1**. installable as XAR extension but moment.js code is included in the extension sources
28 1**. can slow/break the blame view on GitHub
29 1. copy moment.js in a JSX object(((
30 {{code language="none"}}
31 $xwiki.jsx.use('Demo.MomentJS')
32 {{/code}}
33 )))
34 1**. the library code is still included in the extension sources
35 1**. when you upgrade the library version you need to ask your users to clear the browser cache or you need to put the library version in the document name(((
36 {{code language="none"}}
37 $xwiki.jsx.use('Demo.MomentJSv2_10_3')
38 {{/code}}
39 )))
40 1***. but then you need to update your code too which is bad because the dependency version should be part of the configuration.
41 1. Load moment.js from CDN(((
42 {{code language="html"}}
43 <script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.3/moment.js"
44 type="text/javascript"></script>
45 {{/code}}
46 )))
47 1*. the library code is not included in the extension sources any more
48 1*. but the version is still specified in the code
49 1*. and XWiki might be behind a Proxy/Firewall with limited internet access
50 1. Deploy moment.js as a [[**WebJar**>>extensions:Extension.WebJars Integration]] and load it using [[**RequireJS**>>JavaScriptAPI||anchor="HRequireJSandjQueryAPIs"]] and the WebJar Script Service.
51
52 = What is a WebJar? =
53
54 * A JAR (Java Archive) file that packages client-side web libraries
55 * It can contain any resource file that is usable from the client side: JavaScript, CSS, HTML, client-side templates (e.g. Mustache, Handlebars), JSON, etc.
56 * Check www.webjars.org for more information and the list of available WebJars you can use
57 * Most WebJar are published on Maven Central so you can integrate them in your Maven build
58 * All resource paths must follow this convention:
59 ** ##META-INF/resources/webjars/${**name**}/${**version**}##
60 ** ##META-INF/resources/webjars/jquery/1.11.1/jquery.js##
61 ** ##META-INF/resources/webjars/jstree/3.0.8/themes/default/style.css##
62
63 = How can we use WebJars =
64
65 * Deployed like a normal JAR inside ##WEB-INF/lib## or through Extension Manager
66 * Maven Project Dependency(((
67 {{code language="xml"}}
68 <dependency>
69 <groupId>org.webjars</groupId>
70 <artifactId>jstree</artifactId>
71 <version>3.0.8</version>
72 <scope>runtime</scope>
73 </dependency>
74 {{/code}}
75 )))
76 * Script Service(((
77 {{code language="html"}}
78 <script href="$services.webjars.url('momentjs', 'min/moment.js')"
79 type="text/javascript" />
80 {{/code}}
81 )))
82
83 = Why should we use WebJars? =
84
85 * Installable with Extension Manager
86 * Explicit & Transitive Dependencies
87 * Library code is not included in your sources
88 * Versioning and Cache
89 ** The library version is not specified in your source code
90 ** But it is part of the resource URL so there's no need to clear the browser cache after an upgrade(((
91 {{code language="none"}}
92 http://<server>/xwiki/webjars/momentjs/2.10.3/min/moment.min.js
93 {{/code}}
94 )))
95 * Both minified and non-minified resources are usually available
96 ** You can debug using the non-minified version
97
98 Still, adding the script tag manually is not nice. We have RequireJS for this though.
99
100 = What is RequireJS? =
101
102 * RequireJS is a JavaScript file and module loader
103 * You can organize your code in **modules** that declare **explicitly** their **dependencies**
104 * Modules are loaded / imported asynchronously, with all their transitive dependencies
105 * This is called *Asynchronous Module Definition* (ADM)
106 * Modules can **export** (publish) APIs (e.g. an object or a function) which are "injected" in your code
107 ** Dependency Injection
108
109 = How can we use RequireJS? =
110
111 * Define a new module(((
112 {{code language="js"}}
113 define('climb-mountain', ['boots', 'backpack', 'poles'], function(boots, $bp, poles) {
114 // Prepare the climb tools.
115 /* ... */
116
117 // Export the API
118 return function(mountainName) {
119 // Climb the specified mountain.
120 };
121 });
122 {{/code}}
123 )))
124 * Use existing modules(((
125 {{code language="js"}}
126 require.config({
127 paths: {
128 moment: [
129 '//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.3/moment.min',
130 "$!services.webjars.url('momentjs', 'min/moment.min')"
131 ],
132 'climb-mountain': '$xwiki.getURL("Fun.ClimbMountain", "jsx", "language=$xcontext.language")'
133 }
134 });
135
136 require(['jquery', 'moment', 'climb-mountain'], function($, moment, climb) {
137 climb('Mont Blanc');
138 });
139 {{/code}}
140 )))
141
142 = Why should we use RequireJS? =
143
144 * Clear declaration of dependencies and avoids the use of globals
145 * Module identifiers can be mapped to different paths which allows swapping out implementation
146 ** This is great for creating mocks for unit testing
147 * Encapsulates the module definition. Gives you the tools to avoid polluting the global namespace.
148 * The JavaScript code becomes more modularized
149 * We can use different versions of a lib at the same time
150
151 = Time Ago LiveTable Date: First Version =
152
153 Using a JSX:
154
155 {{code language="js"}}
156 require.config({
157 paths: {
158 moment: "$services.webjars.url('momentjs', 'min/moment.min')"
159 }
160 });
161
162 require(['jquery', 'moment', 'xwiki-events-bridge'], function($, moment) {
163 $(document).on('xwiki:livetable:newrow', function(event, data) {
164 var dateString = data.data['doc_date'];
165 var timeAgo = moment(dateString, "YYYY/MM/DD HH:mm").fromNow();
166 $(data.row).find('td.doc_date').html(timeAgo);
167 };
168 });
169 {{/code}}
170
171 = Time Ago LiveTable Date: Second Version =
172
173 Let's make it more generic:
174
175 {{code language="js"}}
176 define(['jquery', 'moment', 'xwiki-events-bridge'], function($, moment) {
177 return function(column, liveTableId, dateFormat) {
178 column = column || 'doc.date';
179 column = column.replace(/^doc\./, 'doc_');
180 dateFormat = dateFormat || 'YYYY/MM/DD HH:mm';
181 var eventName = 'xwiki:livetable:newrow';
182 if (liveTableId) {
183 eventName = 'xwiki:livetable:' + liveTableId + ':newrow';
184 }
185 $(document).on(eventName, function(event, data) {
186 var dateString = data.data[column];
187 var timeAgo = moment(dateString, dateFormat).fromNow();
188 $(data.row).find('td.' + column).html(timeAgo);
189 });
190 };
191 });
192 {{/code}}
193
194 = How can we package WebJars? =
195
196 Unfortunately there's no dedicated/integrated Maven plugin for packaging a WebJar so we need to mix a couple of standard Maven plugins:
197
198 * We put the resources in ##src/main/resources## as expected for a Maven project
199 ** ##src/main/resources/livetable-timeago.js##
200 * Copy the WebJar resources to the right path before packing the jar(((
201 {{code language="xml"}}
202 <plugin>
203 <artifactId>maven-resources-plugin</artifactId>
204 <executions>
205 <execution>
206 <id>copy-webjar-resources</id>
207 <phase>validate</phase>
208 <goals>
209 <goal>resources</goal>
210 </goals>
211 <configuration>
212 <!-- Follow the specifications regarding the WebJar content path. -->
213 <outputDirectory>
214 ${project.build.outputDirectory}/META-INF/resources/webjars/${project.artifactId}/${project.version}
215 </outputDirectory>
216 </configuration>
217 </execution>
218 </executions>
219 </plugin>
220 {{/code}}
221 )))
222 * Package the WebJar resources as a JAR(((
223 {{code language="xml"}}
224 <plugin>
225 <groupId>org.apache.maven.plugins</groupId>
226 <artifactId>maven-jar-plugin</artifactId>
227 <configuration>
228 <includes>
229 <!-- Include only the WebJar content -->
230 <include>META-INF/**</include>
231 </includes>
232 </configuration>
233 </plugin>
234 {{/code}}
235 )))
236
237 = Why should we pacakge WebJars? =
238
239 * See [[Why should we use WebJars?>>||anchor=""]]
240 * Group resources by functionality
241 * State dependencies clearly
242 * Apply quality tools
243 ** Static code verification (JSHint)
244 ** Unit and integration tests (Jasmine)
245 ** Minification (YUI Compressor)
246
247 Let's add some quality tools.
248
249 = What is JSHint? =
250
251 [[JSHint>>http://jshint.com/]] is a tool that helps to detect errors and potential problems in your JavaScript code.
252
253 {{code language="none"}}
254 [ERROR] 3,18: This function has too many parameters. (4)
255 [ERROR] 6,50: Missing semicolon.
256 [ERROR] 11,18: Blocks are nested too deeply. (3)
257 [ERROR] 16,5: 'foo' is not defined.
258 {{/code}}
259
260 = How can we use JSHint? =
261
262 {{code language="xml"}}
263 <plugin>
264 <groupId>com.cj.jshintmojo</groupId>
265 <artifactId>jshint-maven-plugin</artifactId>
266 <version>1.3.0</version>
267 <executions>
268 <execution>
269 <goals>
270 <goal>lint</goal>
271 </goals>
272 </execution>
273 </executions>
274 <configuration>
275 <globals>require,define,document</globals>
276 <!-- See http://jshint.com/docs/options/ -->
277 <options>maxparams:3,maxdepth:2,eqeqeq,undef,unused,immed,latedef,noarg,noempty,nonew</options>
278 <directories>
279 <directory>src/main/resources</directory>
280 </directories>
281 </configuration>
282 </plugin>
283 {{/code}}
284
285 = What is Jasmine? =
286
287 * [[Jasmine>>http://jasmine.github.io/]] is a DOM-less simple JavaScript testing framework
288 * It does not rely on browsers, DOM, or any JavaScript framework
289 * It has a Maven plugin
290 * Not as nice as Mockito on Java but still very useful
291
292 {{code language="js"}}
293 describe("A suite", function() {
294 it("contains spec with an expectation", function() {
295 expect(true).toBe(true);
296 });
297 });
298 {{/code}}
299
300 = How can we use Jasmine? =
301
302 Let's add an unit test in ##src/test/javascript/livetable-timeago.js##:
303
304 {{code language="js"}}
305 // Mock module dependencies.
306
307 var $ = jasmine.createSpy('$');
308 define('jquery', [], function() {
309 return $;
310 });
311
312 var moment = jasmine.createSpy('moment');
313 define('moment', [], function() {
314 return moment;
315 });
316
317 define('xwiki-events-bridge', [], {});
318
319 // Unit tests
320
321 define(['livetable-timeago'], function(timeAgo) {
322 describe('Live Table Time Ago module', function() {
323 it('Change date to time ago using defaults', function() {
324 // Setup mocks.
325 var $doc = jasmine.createSpyObj('$doc', ['on']);
326 var $row = jasmine.createSpyObj('$row', ['find']);
327 var $cell = jasmine.createSpyObj('$cell', ['html']);
328
329 var eventData = {
330 data: {doc_date: '2015/07/19 12:35'},
331 row: {}
332 };
333
334 $.andCallFake(function(selector) {
335 if (selector === document) {
336 return $doc;
337 } else if (selector === eventData.row) {
338 return $row;
339 } else if (selector === 'td.doc_date') {
340 return $cell;
341 }
342 });
343
344 $doc.on.andCallFake(function(eventName, listener) {
345 eventName == 'xwiki:livetable:newrow' && listener(null, eventData);
346 });
347
348 $row.find.andCallFake(function(selector) {
349 if (selector === 'td.doc_date') {
350 return $cell;
351 }
352 });
353
354 var momentObj = jasmine.createSpyObj('momentObj', ['fromNow']);
355 moment.andCallFake(function(dateString, dateFormat) {
356 if (dateString === eventData.data.doc_date && dateFormat === 'YYYY/MM/DD HH:mm') {
357 return momentObj;
358 }
359 });
360
361 var timeAgoDate = '1 day ago';
362 momentObj.fromNow.andReturn(timeAgoDate);
363
364 // Run the operation.
365 timeAgo();
366
367 // Verify the results.
368 expect($cell.html).toHaveBeenCalledWith(timeAgoDate);
369 });
370 });
371 });
372 {{/code}}
373
374 We use the Jasmine Maven plugin to run the tests:
375
376 {{code language="xml"}}
377 <plugin>
378 <groupId>com.github.searls</groupId>
379 <artifactId>jasmine-maven-plugin</artifactId>
380 <executions>
381 <execution>
382 <goals>
383 <goal>test</goal>
384 </goals>
385 </execution>
386 </executions>
387 <configuration>
388 <specRunnerTemplate>REQUIRE_JS</specRunnerTemplate>
389 <preloadSources>
390 <source>webjars/require.js</source>
391 </preloadSources>
392 <jsSrcDir>${project.basedir}/src/main/resources</jsSrcDir>
393 <timeout>10</timeout>
394 </configuration>
395 </plugin>
396 {{/code}}

Get Connected