Introducing GroundworkJS

In my opinion the JavaScript bootstrap is one of the most awkward parts of a web page, developer comprehension and future maintainability depend on getting the architecture of it right. In general there are three strategies:

  1. Load and instantiate all website functionality for each request regardless of the page content
  2. Write framework specific views with dependencies tied to pre-defined routes
  3. Use an asset pipeline to load only the dependencies defined in the application views

None of the approaches mentioned above are suited to loading and executing granular functionality in a way that a constantly changing (read, “CMS driven”) site requires. The first approach is robust but may become a performance bottleneck even with the best minification, execution organisation and network optimisation. The second approach is inflexible and could very quickly become a maintenance nightmare trying to wire-up all eventualities. Any site with a large number of mixed and re-used components built with either of these approaches will have architectural difficulties when a new feature request arrives.

Using GroundworkJS

To try and find the best compromise between plug-and-play scripts, code maintainability, content flexibility and performance I have written GroundworkJS, a simple bootstrap for loading and binding DOM elements to JavaScript modules; the glue between your HTML document and your scripts that act upon it.

The main power of GroundworkJS is not itself but the environment it stitches together. There are great benefits to writing modular JavaScript but without established conventions wiring all the scripts together may cause a level of confusion that outweighs the gains.

Conventions

In GroundworkJS functionality is grouped into components. Components are split into sub-types of widgets, directives, utilities and mashups:

Widget
A collection of functionality that is stateful and may have an API; E.G. tabs, modal dialogues and slideshows.
Directive
A run-once piece of functionality; E.G. loading a Google map or the one-way transformation of an element.
Utility
A re-usable piece of functionality to help other components; E.G. feature detection or an AJAX library.
Mashup
A combination of multiple components functionality; E.G. binding multiple slideshow widgets together or loading tab content with AJAX.

HTML

Components are explicitly bound to DOM elements with a data-gw-component attribute and multiple components can be applied by specifying a comma-separated list:

<div id="content-slider" data-gw-component="widget/slider">
    <ul>
        <li><img /></li>
        <li><img /></li>
        <li><img /></li>
    </ul>
</div>

<img src="images/foo.png" alt="Foo" data-gw-component="directive/to-svg" />

<ul class="tab-menu" data-gw-component="mashup/ajax-tabs">
    <li><a href="/page-1.html">Page 1</a></li>
    <li><a href="/page-2.html">Page 2</a></li>
    <li><a href="/page-3.html">Page 3</a></li>
</ul>

JavaScript

Components can provide a function or an object. Objects should include an init method which will receive the element as its first argument:

define(function () {
  return {
    init: function (element) {  },
    next: function () {  },
    prev: function () {  },
    teardown: function () {  }
  };
});
~/js/component/widget/slider.js

Functions will be handled as constructors (called with the new operator) and will also receive the element. All components may have an optional teardown method:

define(function () {
  function Tabs (element) {  }

  Tabs.prototype.toggle = function () {  };

  Tabs.prototype.teardown = function () {  };

  return Tabs;
});
~/js/component/widget/tabs.js

Directives shouldn’t maintain state or provide an API so may provide an anonymous function:

define(function () {
  return function (element) {
    if (window.SVGElement) {
      element.src = element.src.replace(/\.(png|gif)/, '.svg');
    }
  };
});
~/js/component/directive/to-svg.js

And finally, mashups combine multiple components to create new or extend existing functionality:

define([ "widget/tabs", "utility/ajax" ], function (Tabs, ajax) {
  return function (element) {
    var tabs = new Tabs(element);

    tabs.target.addEventListener("click", function (e) {
      e.preventDefault();

      ajax.get(this.href, function(data) {  });
    });
  };
});
~/js/component/mashup/ajax-tabs.js

Configuration

GroundworkJS requires a module loader and there are plenty to choose from depending on project requirements or developer preferences. The script loader must be setup to find the components then GroundworkJS can be booted up:

// Configuration for RequireJS
require.config({
  baseUrl: "/path/to/static/assets",
  paths: {
    component: "path/to/components"
  }
});

require([ "groundwork" ], function (groundwork) {
  groundwork.startup();
});
~/js/bootstrap.js

Development pleasure

I’ve developed several sites now with GroundworkJS in its various forms and I have found it to be a real pleasure to use. Adding documented conventions to a project has been a hugely worthwhile task on its own because it has sped up decision making and encouraged a clear separation of concerns. In my experience developing with modular JavaScript has scaled well within our team and alongside a few other improvements our front-end performance has been absolutely great. However, as JavaScript size increases we may need to consider bundling modules up-front to reduce startup time.

If you’re interested in quickly getting up and running with GroundworkJS please check out Wirework, a generator for Yo which includes GroundworkJS with the all dependencies and tools wired up and ready to use.

View the project on GitHub

Comments