Integrating JavaScript Libraries in XWiki

Version 1.2 by Marius Dumitru Florea on 2015/07/28


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:

  1. copy moment.js somewhere in /resources
    • 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
  2. attach moment.js to a wiki page
    <script src="$xwiki.getAttachmentURL('Demo.MomentJS', 'moment.js'"
      • installable as XAR extension but moment.js code is included in the extension sources
      • can slow/break the blame view on GitHub
  3. copy moment.js in a JSX object
      • 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
        • but then you need to update your code too which is bad because the dependency version should be part of the configuration.
  4. Load moment.js from CDN
    <script src="//"
    • 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
  5. 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 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
  • Script Service
    <script href="$services.webjars.url('momentjs', 'min/moment.js')"
     type="text/javascript" />

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 upgrade
  • 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* (ADM)
  • 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 module
    define('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 modules
      paths: {
        moment: [
         "$!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');

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 =['doc_date'];
   var timeAgo = moment(dateString, "YYYY/MM/DD HH:mm").fromNow();

Time Ago LiveTable Date: Second Version

Let's make it more generic:

define(['jquery', 'moment', 'xwiki-events-bridge'], function($, moment) {
 return function(column, liveTableId, dateFormat) {
    column = column || '';
    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 =[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
           <!-- Follow the specifications regarding the WebJar content path. -->
  • Package the WebJar resources as a JAR
         <!-- Include only the WebJar content -->

Why should we pacakge 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] 3,18: This function has too many parameters. (4)
[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?

   <!-- See -->

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
describe("A suite", function() {
  it("contains spec with an expectation", function() {

How can we use Jasmine?

Let's add an unit test in src/test/javascript/livetable-timeago.js:

// Mock module dependencies.

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 === && dateFormat === 'YYYY/MM/DD HH:mm') {
         return momentObj;

     var timeAgoDate = '1 day ago';

     // Run the operation.

     // Verify the results.

We use the Jasmine Maven plugin to run the tests:


Get Connected