Version 4.2 by Oana Florea on 2019/09/13

Show last authors
1 {{box cssClass="floatinginfobox" title="**Contents**"}}
2 {{toc depth="1"/}}
3 {{/box}}
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**>>platform:DevGuide.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 * Examples from the FAQ:
142 ** [[FAQ.How to integrate React and XWiki]]
143 ** [[FAQ.How to integrate d3js and XWiki]]
144
145 = Why should we use RequireJS? =
146
147 * Clear declaration of dependencies and avoids the use of globals
148 * Module identifiers can be mapped to different paths which allows swapping out implementation
149 ** This is great for creating mocks for unit testing
150 * Encapsulates the module definition. Gives you the tools to avoid polluting the global namespace.
151 * The JavaScript code becomes more modularized
152 * We can use different versions of a lib at the same time
153
154 = Time Ago LiveTable Date: First Version =
155
156 Using a JSX:
157
158 {{code language="js"}}
159 require.config({
160 paths: {
161 moment: "$services.webjars.url('momentjs', 'min/moment.min')"
162 }
163 });
164
165 require(['jquery', 'moment', 'xwiki-events-bridge'], function($, moment) {
166 $(document).on('xwiki:livetable:newrow', function(event, data) {
167 var dateString = data.data['doc_date'];
168 var timeAgo = moment(dateString, "YYYY/MM/DD HH:mm").fromNow();
169 $(data.row).find('td.doc_date').html(timeAgo);
170 };
171 });
172 {{/code}}
173
174 = Time Ago LiveTable Date: Second Version =
175
176 Let's make it more generic:
177
178 {{code language="js"}}
179 define(['jquery', 'moment', 'xwiki-events-bridge'], function($, moment) {
180 return function(column, liveTableId, dateFormat) {
181 column = column || 'doc.date';
182 column = column.replace(/^doc\./, 'doc_');
183 dateFormat = dateFormat || 'YYYY/MM/DD HH:mm';
184 var eventName = 'xwiki:livetable:newrow';
185 if (liveTableId) {
186 eventName = 'xwiki:livetable:' + liveTableId + ':newrow';
187 }
188 $(document).on(eventName, function(event, data) {
189 var dateString = data.data[column];
190 var timeAgo = moment(dateString, dateFormat).fromNow();
191 $(data.row).find('td.' + column).html(timeAgo);
192 });
193 };
194 });
195 {{/code}}
196
197 = How can we package WebJars? =
198
199 Unfortunately there's no dedicated/integrated Maven plugin for packaging a WebJar so we need to mix a couple of standard Maven plugins:
200
201 * We put the resources in ##src/main/resources## as expected for a Maven project
202 ** ##src/main/resources/livetable-timeago.js##
203 * Copy the WebJar resources to the right path before packing the jar(((
204 {{code language="xml"}}
205 <plugin>
206 <artifactId>maven-resources-plugin</artifactId>
207 <executions>
208 <execution>
209 <id>copy-webjar-resources</id>
210 <phase>validate</phase>
211 <goals>
212 <goal>resources</goal>
213 </goals>
214 <configuration>
215 <!-- Follow the specifications regarding the WebJar content path. -->
216 <outputDirectory>
217 ${project.build.outputDirectory}/META-INF/resources/webjars/${project.artifactId}/${project.version}
218 </outputDirectory>
219 </configuration>
220 </execution>
221 </executions>
222 </plugin>
223 {{/code}}
224 )))
225 * Package the WebJar resources as a JAR(((
226 {{code language="xml"}}
227 <plugin>
228 <groupId>org.apache.maven.plugins</groupId>
229 <artifactId>maven-jar-plugin</artifactId>
230 <configuration>
231 <includes>
232 <!-- Include only the WebJar content -->
233 <include>META-INF/**</include>
234 </includes>
235 </configuration>
236 </plugin>
237 {{/code}}
238 )))
239
240 = Why should we package WebJars? =
241
242 * See [[Why should we use WebJars?>>platform:DevGuide.IntegratingJavaScriptLibraries]]
243 * Group resources by functionality
244 * State dependencies clearly
245 * Apply quality tools
246 ** Static code verification (JSHint)
247 ** Unit and integration tests (Jasmine)
248 ** Minification (YUI Compressor)
249
250 Let's add some quality tools.
251
252 = What is JSHint? =
253
254 [[JSHint>>http://jshint.com/]] is a tool that helps to detect errors and potential problems in your JavaScript code.
255
256 {{code language="none"}}
257 [ERROR] 3,18: This function has too many parameters. (4)
258 [ERROR] 6,50: Missing semicolon.
259 [ERROR] 11,18: Blocks are nested too deeply. (3)
260 [ERROR] 16,5: 'foo' is not defined.
261 {{/code}}
262
263 = How can we use JSHint? =
264
265 {{code language="xml"}}
266 <plugin>
267 <groupId>com.cj.jshintmojo</groupId>
268 <artifactId>jshint-maven-plugin</artifactId>
269 <version>1.3.0</version>
270 <executions>
271 <execution>
272 <goals>
273 <goal>lint</goal>
274 </goals>
275 </execution>
276 </executions>
277 <configuration>
278 <globals>require,define,document</globals>
279 <!-- See http://jshint.com/docs/options/ -->
280 <options>maxparams:3,maxdepth:2,eqeqeq,undef,unused,immed,latedef,noarg,noempty,nonew</options>
281 <directories>
282 <directory>src/main/resources</directory>
283 </directories>
284 </configuration>
285 </plugin>
286 {{/code}}
287
288 = What is Jasmine? =
289
290 * [[Jasmine>>http://jasmine.github.io/]] is a DOM-less simple JavaScript testing framework
291 * It does not rely on browsers, DOM, or any JavaScript framework
292 * It has a Maven plugin
293 * Not as nice as Mockito on Java but still very useful
294
295 {{code language="js"}}
296 describe("A suite", function() {
297 it("contains spec with an expectation", function() {
298 expect(true).toBe(true);
299 });
300 });
301 {{/code}}
302
303 = How can we use Jasmine? =
304
305 Let's add an unit test in ##src/test/javascript/livetable-timeago.js##:
306
307 {{code language="js"}}
308 // Mock module dependencies.
309
310 var $ = jasmine.createSpy('$');
311 define('jquery', [], function() {
312 return $;
313 });
314
315 var moment = jasmine.createSpy('moment');
316 define('moment', [], function() {
317 return moment;
318 });
319
320 define('xwiki-events-bridge', [], {});
321
322 // Unit tests
323
324 define(['livetable-timeago'], function(timeAgo) {
325 describe('Live Table Time Ago module', function() {
326 it('Change date to time ago using defaults', function() {
327 // Setup mocks.
328 var $doc = jasmine.createSpyObj('$doc', ['on']);
329 var $row = jasmine.createSpyObj('$row', ['find']);
330 var $cell = jasmine.createSpyObj('$cell', ['html']);
331
332 var eventData = {
333 data: {doc_date: '2015/07/19 12:35'},
334 row: {}
335 };
336
337 $.andCallFake(function(selector) {
338 if (selector === document) {
339 return $doc;
340 } else if (selector === eventData.row) {
341 return $row;
342 } else if (selector === 'td.doc_date') {
343 return $cell;
344 }
345 });
346
347 $doc.on.andCallFake(function(eventName, listener) {
348 eventName == 'xwiki:livetable:newrow' && listener(null, eventData);
349 });
350
351 $row.find.andCallFake(function(selector) {
352 if (selector === 'td.doc_date') {
353 return $cell;
354 }
355 });
356
357 var momentObj = jasmine.createSpyObj('momentObj', ['fromNow']);
358 moment.andCallFake(function(dateString, dateFormat) {
359 if (dateString === eventData.data.doc_date && dateFormat === 'YYYY/MM/DD HH:mm') {
360 return momentObj;
361 }
362 });
363
364 var timeAgoDate = '1 day ago';
365 momentObj.fromNow.andReturn(timeAgoDate);
366
367 // Run the operation.
368 timeAgo();
369
370 // Verify the results.
371 expect($cell.html).toHaveBeenCalledWith(timeAgoDate);
372 });
373 });
374 });
375 {{/code}}
376
377 We use the Jasmine Maven plugin to run the tests:
378
379 {{code language="xml"}}
380 <plugin>
381 <groupId>com.github.searls</groupId>
382 <artifactId>jasmine-maven-plugin</artifactId>
383 <executions>
384 <execution>
385 <goals>
386 <goal>test</goal>
387 </goals>
388 </execution>
389 </executions>
390 <configuration>
391 <specRunnerTemplate>REQUIRE_JS</specRunnerTemplate>
392 <preloadSources>
393 <source>webjars/require.js</source>
394 </preloadSources>
395 <jsSrcDir>${project.basedir}/src/main/resources</jsSrcDir>
396 <timeout>10</timeout>
397 </configuration>
398 </plugin>
399 {{/code}}
400
401 =Related=
402
403 {{velocity}}
404 #set($tag = 'javascript')
405 #set ($list = $xwiki.tag.getDocumentsWithTag($tag))
406 (((
407 (% class="xapp" %)
408 === $services.localization.render('xe.tag.alldocs', ["//${tag}//"]) ===
409
410 #if ($list.size()> 0)
411 {{html}}#displayDocumentList($list false $blacklistedSpaces){{/html}}
412 #else
413 (% class='noitems' %)$services.localization.render('xe.tag.notags')
414 #end
415 )))
416 {{/velocity}}

Get Connected