Integrating JavaScript Libraries
- Use Case
- Integration Options
- What is a WebJar?
- How can we use WebJars
- Why should we use WebJars?
- What is RequireJS?
- How can we use RequireJS?
- Why should we use RequireJS?
- Time Ago LiveTable Date: First Version
- Time Ago LiveTable Date: Second Version
- How can we package WebJars?
- Why should we package WebJars?
- What is JSHint?
- How can we use JSHint?
- What is Jasmine?
- How can we use Jasmine?
- Related
Use Case
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. It can parse, validate, manipulate, and display dates from JavaScript.
Integration Options
There are several ways we can integrate Moment.js in XWiki:
- copy moment.js somewhere in /resources$xwiki.jsfx.use('path/to/moment.js')
- you need file system access
- it leads to a custom XWiki WAR and thus upgrade complexity
- Extension Manager doesn't support installing resources in the WAR
- attach moment.js to a wiki page<script src="$xwiki.getAttachmentURL('Demo.MomentJS', 'moment.js')"
type="text/javascript"></script>- installable as XAR extension but moment.js code is included in the extension sources
- can slow/break the blame view on GitHub
copy moment.js in a JSX object
$xwiki.jsx.use('Demo.MomentJS')- the library code is still included in the extension sources
- 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$xwiki.jsx.use('Demo.MomentJSv2_10_3')
- but then you need to update your code too which is bad because the dependency version should be part of the configuration.
- Load moment.js from CDN<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.3/moment.js"
type="text/javascript"></script>- the library code is not included in the extension sources any more
- but the version is still specified in the code
- and XWiki might be behind a Proxy/Firewall with limited internet access
- Deploy moment.js as a WebJar and load it using RequireJS and the WebJar Script Service.
What is a WebJar?
- A JAR (Java Archive) file that packages client-side web libraries
- 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.
- Check www.webjars.org for more information and the list of available WebJars you can use
- Most WebJar are published on Maven Central so you can integrate them in your Maven build
- All resource paths must follow this convention:
- META-INF/resources/webjars/${name}/${version}
- META-INF/resources/webjars/jquery/1.11.1/jquery.js
- META-INF/resources/webjars/jstree/3.0.8/themes/default/style.css
How can we use WebJars
- Deployed like a normal JAR inside WEB-INF/lib or through Extension Manager
- Maven Project Dependency<dependency>
<groupId>org.webjars</groupId>
<artifactId>jstree</artifactId>
<version>3.0.8</version>
<scope>runtime</scope>
</dependency> - Script Service<script href="$services.webjars.url('momentjs', 'min/moment.js')"
type="text/javascript" ></script>
Why should we use WebJars?
- Installable with Extension Manager
- Explicit & Transitive Dependencies
- Library code is not included in your sources
- Versioning and Cache
- The library version is not specified in your source code
- But it is part of the resource URL so there's no need to clear the browser cache after an upgradehttp://<server>/xwiki/webjars/momentjs/2.10.3/min/moment.min.js
- Both minified and non-minified resources are usually available
- You can debug using the non-minified version
Still, adding the script tag manually is not nice. We have RequireJS for this though.
What is RequireJS?
- RequireJS is a JavaScript file and module loader
- You can organize your code in modules that declare explicitly their dependencies
- Modules are loaded / imported asynchronously, with all their transitive dependencies
- This is called *Asynchronous Module Definition* (AMD)
- Modules can export (publish) APIs (e.g. an object or a function) which are "injected" in your code
- Dependency Injection
How can we use RequireJS?
- Define a new moduledefine('climb-mountain', ['boots', 'backpack', 'poles'], function(boots, $bp, poles) {
// Prepare the climb tools.
/* ... */
// Export the API
return function(mountainName) {
// Climb the specified mountain.
};
}); - Use existing modulesrequire.config({
paths: {
moment: [
'//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.3/moment.min',
"$!services.webjars.url('momentjs', 'min/moment.min')"
],
'climb-mountain': '$xwiki.getURL("Fun.ClimbMountain", "jsx", "language=$xcontext.language")'
}
});
require(['jquery', 'moment', 'climb-mountain'], function($, moment, climb) {
climb('Mont Blanc');
}); - Examples from the FAQ:
Why should we use RequireJS?
- Clear declaration of dependencies and avoids the use of globals
- Module identifiers can be mapped to different paths which allows swapping out implementation
- This is great for creating mocks for unit testing
- Encapsulates the module definition. Gives you the tools to avoid polluting the global namespace.
- The JavaScript code becomes more modularized
- We can use different versions of a lib at the same time
Time Ago LiveTable Date: First Version
Using a JSX:
paths: {
moment: "$services.webjars.url('momentjs', 'min/moment.min')"
}
});
require(['jquery', 'moment', 'xwiki-events-bridge'], function($, moment) {
$(document).on('xwiki:livetable:newrow', function(event, data) {
var dateString = data.data['doc_date'];
var timeAgo = moment(dateString, "YYYY/MM/DD HH:mm").fromNow();
$(data.row).find('td.doc_date').html(timeAgo);
};
});
Time Ago LiveTable Date: Second Version
Let's make it more generic:
return function(column, liveTableId, dateFormat) {
column = column || 'doc.date';
column = column.replace(/^doc\./, 'doc_');
dateFormat = dateFormat || 'YYYY/MM/DD HH:mm';
var eventName = 'xwiki:livetable:newrow';
if (liveTableId) {
eventName = 'xwiki:livetable:' + liveTableId + ':newrow';
}
$(document).on(eventName, function(event, data) {
var dateString = data.data[column];
var timeAgo = moment(dateString, dateFormat).fromNow();
$(data.row).find('td.' + column).html(timeAgo);
});
};
});
How can we package WebJars?
Unfortunately there's no dedicated/integrated Maven plugin for packaging a WebJar so we need to mix a couple of standard Maven plugins:
- We put the resources in src/main/resources as expected for a Maven project
- src/main/resources/livetable-timeago.js
- Copy the WebJar resources to the right path before packing the jar<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-webjar-resources</id>
<phase>validate</phase>
<goals>
<goal>resources</goal>
</goals>
<configuration>
<!-- Follow the specifications regarding the WebJar content path. -->
<outputDirectory>
${project.build.outputDirectory}/META-INF/resources/webjars/${project.artifactId}/${project.version}
</outputDirectory>
</configuration>
</execution>
</executions>
</plugin> - Package the WebJar resources as a JAR<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<includes>
<!-- Include only the WebJar content -->
<include>META-INF/**</include>
</includes>
</configuration>
</plugin>
Why should we package WebJars?
- See Why should we use WebJars?
- Group resources by functionality
- State dependencies clearly
- Apply quality tools
- Static code verification (JSHint)
- Unit and integration tests (Jasmine)
- Minification (YUI Compressor)
Let's add some quality tools.
What is JSHint?
JSHint is a tool that helps to detect errors and potential problems in your JavaScript code.
[ERROR] 6,50: Missing semicolon.
[ERROR] 11,18: Blocks are nested too deeply. (3)
[ERROR] 16,5: 'foo' is not defined.
How can we use JSHint?
<groupId>com.cj.jshintmojo</groupId>
<artifactId>jshint-maven-plugin</artifactId>
<version>1.3.0</version>
<executions>
<execution>
<goals>
<goal>lint</goal>
</goals>
</execution>
</executions>
<configuration>
<globals>require,define,document</globals>
<!-- See http://jshint.com/docs/options/ -->
<options>maxparams:3,maxdepth:2,eqeqeq,undef,unused,immed,latedef,noarg,noempty,nonew</options>
<directories>
<directory>src/main/resources</directory>
</directories>
</configuration>
</plugin>
What is Jasmine?
- Jasmine is a DOM-less simple JavaScript testing framework
- It does not rely on browsers, DOM, or any JavaScript framework
- It has a Maven plugin
- Not as nice as Mockito on Java but still very useful
it("contains spec with an expectation", function() {
expect(true).toBe(true);
});
});
How can we use Jasmine?
Let's add an unit test in src/test/javascript/livetable-timeago.js:
var $ = jasmine.createSpy('$');
define('jquery', [], function() {
return $;
});
var moment = jasmine.createSpy('moment');
define('moment', [], function() {
return moment;
});
define('xwiki-events-bridge', [], {});
// Unit tests
define(['livetable-timeago'], function(timeAgo) {
describe('Live Table Time Ago module', function() {
it('Change date to time ago using defaults', function() {
// Setup mocks.
var $doc = jasmine.createSpyObj('$doc', ['on']);
var $row = jasmine.createSpyObj('$row', ['find']);
var $cell = jasmine.createSpyObj('$cell', ['html']);
var eventData = {
data: {doc_date: '2015/07/19 12:35'},
row: {}
};
$.andCallFake(function(selector) {
if (selector === document) {
return $doc;
} else if (selector === eventData.row) {
return $row;
} else if (selector === 'td.doc_date') {
return $cell;
}
});
$doc.on.andCallFake(function(eventName, listener) {
eventName == 'xwiki:livetable:newrow' && listener(null, eventData);
});
$row.find.andCallFake(function(selector) {
if (selector === 'td.doc_date') {
return $cell;
}
});
var momentObj = jasmine.createSpyObj('momentObj', ['fromNow']);
moment.andCallFake(function(dateString, dateFormat) {
if (dateString === eventData.data.doc_date && dateFormat === 'YYYY/MM/DD HH:mm') {
return momentObj;
}
});
var timeAgoDate = '1 day ago';
momentObj.fromNow.andReturn(timeAgoDate);
// Run the operation.
timeAgo();
// Verify the results.
expect($cell.html).toHaveBeenCalledWith(timeAgoDate);
});
});
});
We use the Jasmine Maven plugin to run the tests:
<groupId>com.github.searls</groupId>
<artifactId>jasmine-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>test</goal>
</goals>
</execution>
</executions>
<configuration>
<specRunnerTemplate>REQUIRE_JS</specRunnerTemplate>
<preloadSources>
<source>webjars/require.js</source>
</preloadSources>
<jsSrcDir>${project.basedir}/src/main/resources</jsSrcDir>
<timeout>10</timeout>
</configuration>
</plugin>