Frontend development guidelines

Install frontend dependencies

The front end stylesheets are written using Sass (this depends on node.js being installed on the system)

Instructions for installing Node.js can be found on the Node.js website. Please check the ones relevant to your own distribution

On Ubuntu, run the following to install Node.js official repository and the node package:

curl -sL https://deb.nodesource.com/setup_13.x | sudo -E bash -
sudo apt-get install -y nodejs

Note

If you use the package on the default Ubuntu repositories (eg sudo apt-get install nodejs), the node binary will be called nodejs. This will prevent the CKAN Sass script to work properly, so you will need to create a link to make it work:

ln -s /usr/bin/nodejs /usr/bin/node

For more information, refer to the Node.js instructions.

Dependencies can then be installed via the node package manager (npm). We use gulp to make our Sass compiler a watcher style script.

cd into the CKAN source folder (eg /usr/lib/ckan/default/src/ckan ) and run:

$ npm install

You may need to use sudo depending on your CKAN install type.

File structure

All front-end files to be served via a web server are located in the public directory (in the case of the new CKAN base theme it’s public/base).

css/
  main.css
scss/
  main.scss
  _ckan.scss
  ...
javascript/
  main.js
  utils.js
  components/
  ...
vendor/
  jquery.js
  jquery.plugin.js
  underscore.js
  bootstrap.css
  ...

All files and directories should be lowercase with hyphens used to separate words.

css

Should contain any site specific CSS files including compiled production builds generated by Sass.

scss

Should contain all the scss files for the site. Additional vendor styles should be added to the vendor directory and included in main.scss.

javascript

Should contain all website files. These can be structured appropriately. It is recommended that main.js be used as the bootstrap filename that sets up the page.

vendor

Should contain all external dependencies. These should not contain version numbers in the filename. This information should be available in the header comment of the file. Library plugins should be prefixed with the library name. If a dependency has many files (such as bootstrap) then the entire directory should be included as distributed by the maintainer.

Stylesheets

Because all the stylesheets are using Sass we need to compile them before beginning development by running:

$ npm run watch

This will watch for changes to all of the scss files and automatically rebuild the CSS for you. To quit the script press ctrl-c. If you need sourcemaps for debugging, set DEBUG environment variable. I.e:

$ DEBUG=1 npm run watch

There are many Sass files which attempt to group the styles in useful groups. The main two are:

main.scss:

This contains all the styles for the website including dependancies and local styles. The only files that are excluded here are those that are conditionally loaded such as IE only CSS and large external apps (like some preview plugins) that only appear on a single page.

ckan.scss:

This includes all the local ckan stylesheets.

Note

Whenever a CSS change effects main.scss it’s important than after the merge into master that a $ npm run build should be run and committed.

There is a basic pattern primer available at: http://localhost:5000/testing/primer/ that shows all the main page elements that make up the CKAN core interface.

JavaScript

The core of the CKAN JavaScript is split up into three areas.

  • Core (such as i18n, pub/sub and API clients)

  • Modules (small HTML components or widgets)

  • jQuery Plugins (very small reusable components)

Core

Everything in the CKAN application lives on the ckan namespace. Currently there are four main components that make up the core.

  • Modules

  • Publisher/Subscriber

  • Client

  • i18n/Jed

Modules

Modules are the core of the CKAN website, every component that is interactive on the page should be a module. These are then initialized by including a data-module attribute on an element on the page. For example:

<select name="format" data-module="autocomplete"></select>

The idea is to create small isolated components that can easily be tested. They should ideally not use any global objects, all functionality should be provided to them via a “sandbox” object.

There is a global factory that can be used to create new modules and jQuery and Localisation methods are available via this.sandbox.jQuery and this.sandbox.translate() respectively. To save typing these two common objects we can take advantage of JavaScript closures and use an alternative module syntax that accepts a factory function.

ckan.module('my-module', function (jQuery) {
  return {
    initialize: function () {
      // Called when a module is created.
      // jQuery and translate are available here.
    },
    teardown: function () {
      // Called before a module is removed from the page.
    }
  }
});

Note

A guide on creating your own modules is located in the Building a JavaScript Module guide.

Publisher/subscriber

There is a simple pub/sub module included under ckan.pubsub it’s methods are available to modules via this.sandbox.publish/subscribe/unsubscribe. This can be used to publish messages between modules.

Modules should use the publish/subscribe methods to talk to each other and allow different areas of the UI to update where relevant.

ckan.module('language-picker', function (jQuery) {
  return {
    initialize: function () {
      var sandbox = this.sandbox;
      this.el.on('change', function () {
        sandbox.publish('change:lang', this.selected);
      });
    }
  }
});

ckan.module('language-notifier', function (jQuery) {
  return {
    initialize: function () {
      this.sandbox.subscribe('change:lang', function (lang) {
        alert('language is now ' + lang);
      });
    }
  }
});

Client

Ideally no module should use jQuery.ajax() to make XHR requests to the CKAN API, all functionality should be provided via the client object.

ckan.module('my-module', function (jQuery) {
  return {
    initialize: function () {
      this.sandbox.client.getCompletions(this.options.completionsUrl);
    }
  }
});

Internationalization

See Internationalizing strings in JavaScript code.

Life cycle

CKAN modules are intialised on dom ready. The ckan.module.initialize() will look for all elements on the page with a data-module attribute and attempt to create an instance.

<select name="format" data-module="autocomplete" data-module-key="id"></select>

The module will be created with the element, any options object extracted from data-module-* attributes and a new sandbox instance.

Once created the modules initialize() method will be called allowing the module to set themselves up.

Modules should also provide a teardown() method this isn’t used at the moment except in the unit tests to restore state but may become useful in the future.

jQuery plugins

Any functionality that is not directly related to ckan should be packaged up in a jQuery plug-in if possible. This keeps the modules containing only ckan specific code and allows plug-ins to be reused on other sites.

Examples of these are jQuery.fn.slug(), jQuery.fn.slugPreview() and jQuery.proxyAll().

Unit tests

Every core component, module and plugin should have a set of unit tests. Tests can be filtered using the grep={regexp} query string parameter.

Each file has a description block for it’s top level object and then within that a nested description for each method that is to be tested:

describe('ckan.module.MyModule()', function () {
  describe('.initialize()', function () {
    it('should do something...', function () {
      // assertions.
    });
  });

  describe('.myMethod(arg1, arg2, arg3)', function () {
  });
});

The `.beforeEach()` and `.afterEach()` callbacks can be used to setup objects for testing (all blocks share the same scope so test variables can be attached):

describe('ckan.module.MyModule()', function () {
  before(() => {
      // Open CKAN front page
      cy.visit('/');

      // Pull the class out of the registry.
      cy.window().then(win => {
        // make module available as this.MyModule
        cy.wrap(win.ckan.module.registry['my-module']).as('MyModule');
        win.jQuery('<div id="fixture">').appendTo(win.document.body)
      })
    });

  beforeEach(function () {
    // window object is needed to access the javascript objects
    cy.window().then(win => {
      // Create a test element.
      this.el = win.jQuery('<div />');

      // Create a test sandbox.
      this.sandbox = win.ckan.sandbox();

      // Create a test module.
      this.module = new this.MyModule(this.el, {}, this.sandbox);
    });
  });

  afterEach(function () {
    // Clean up.
    this.module.teardown();
  });
});

Templates can also be loaded using the .loadFixture() method that is available in all test contexts. Tests can be made asynchronous by using promises (Cypress returns a promise in almost all functions):

describe('ckan.module.MyModule()', function () {

  before(function (done) {
    cy.visit('/');

    // Add a fixture element to page
    cy.window().then(win => {
        win.jQuery('<div id="fixture">').appendTo(win.document.body)
    })

    // Load the template once.
    cy.loadFixture('my-template.html').then((template) => {
        cy.wrap(template).as('template');
    });
  });

  beforeEach(function () {
    // Assign the template to the module each time.
    cy.window().then(win => {
        win.jQuery('#fixture').html(this.template).children();
    });
  });