Version 4.2 by Oana Florea on 2019/09/13

Hide last authors
Ecaterina Moraru (Valica) 3.5 1 {{box cssClass="floatinginfobox" title="**Contents**"}}
Pascal Bastien 2.1 2 {{toc depth="1"/}}
Ecaterina Moraru (Valica) 3.5 3 {{/box}}
Marius Dumitru Florea 1.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"}}
Alex Cotiugă 3.7 23 <script src="$xwiki.getAttachmentURL('Demo.MomentJS', 'moment.js')"
Marius Dumitru Florea 1.1 24 type="text/javascript"></script>
25 {{/code}}
26 )))
Vincent Massol 3.6 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
Marius Dumitru Florea 1.1 29 1. copy moment.js in a JSX object(((
30 {{code language="none"}}
31 $xwiki.jsx.use('Demo.MomentJS')
32 {{/code}}
33 )))
Vincent Massol 3.6 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(((
Marius Dumitru Florea 1.1 36 {{code language="none"}}
37 $xwiki.jsx.use('Demo.MomentJSv2_10_3')
38 {{/code}}
39 )))
Vincent Massol 3.6 40 1*. but then you need to update your code too which is bad because the dependency version should be part of the configuration.
Marius Dumitru Florea 1.1 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
Vincent Massol 3.3 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.
Marius Dumitru Florea 1.1 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 )))
Oana Florea 3.8 141 * Examples from the FAQ:
142 ** [[FAQ.How to integrate React and XWiki]]
143 ** [[FAQ.How to integrate d3js and XWiki]]
Marius Dumitru Florea 1.1 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
Pascal Bastien 2.1 240 = Why should we package WebJars? =
Marius Dumitru Florea 1.1 241
Vincent Massol 3.3 242 * See [[Why should we use WebJars?>>platform:DevGuide.IntegratingJavaScriptLibraries]]
Marius Dumitru Florea 1.1 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}}
Oana Florea 4.1 400
Oana Florea 4.2 401 =Related=
Oana Florea 4.1 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