Version 2.1 by Pascal Bastien on 2016/06/22

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

Get Connected