Introducing GroundworkJS

— Updated

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:

(function() {
    tabs(".tab-menu");
    slider(".content-slider");
    expandableText(".expandable");

    if (window.SVGElement) {
        var images = document.querySelectorAll("img.to-svg");

        Array.prototype.forEach.apply(images, function(image) {
            image.src = image.src.replace(/\.(png|gif)/, '.svg');
        });
    }
})();

2) Write framework specific views with dependencies tied to pre-defined routes:

routes({
    "/": "BlogArchive",
    "/view/:post": "BlogPost"
});

controller("BlogArchive", ["plugins/expandable-text"], function(expandableText) {
    expandableText(".expandable");
});

controller("BlogPost", ["plugins/slider"], function(slider) {
    slider(".content-slider");
});

3) Use an asset pipeline to load only the dependencies defined in the application views:

// Many, many lines of configuration code

None of the approaches mentioned above are suited to loading and executing granular functionality in a way that a constantly changing 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 for 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 “widgets”, “directives”, “utilities” and “mashups”. Widgets, directives, utilities and mashups are all types of “component”.

Widget
A collection of functionality that is stateful and has 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 an 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 return a function or an object literal. Objects should include an init method which will receive the element:

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() {

    var Tabs = function(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 return 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

Mashups combine multiple components to create new or extend existing functionality:

define(["widget/tabs", "utility/ajax"], function(Tabs, ajax) {

    return function(element) {
        this.tabs = new Tabs(element);

        this.tabs.target.addEventListener("click", function(e) {
            e.preventDefault();
            ajax.get(this.href, function(data) {
            });
        }, false);
    };

});
~/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.

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 powered by Disqus

A photo of Matt Hinchliffe

About Me

I'm a 29 year old web developer building new stuff at the Financial Times based in London. I specialise in crafting scalable, performance-driven code, tackle accessibility issues and keep an opinionated interest in the latest hotness. I like my tea robustly brewed, white and with no sugar, thanks!