Last modified by Vincent Massol on 2023/10/10

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

Get Connected