BT

Developing Modular JavaScript Components

Posted by Frederik Dohr on Dec 13, 2013 |

While most web applications these days employ an abundance of JavaScript, keeping client-side functionality focused, robust and maintainable remains a significant challenge.

Even though basic tenets like separation of concerns or DRY are taken for granted in other languages and ecosystems, many of these principles are often ignored when it comes to browser-side parts of an application.

This is in part due to the somewhat challenging history of JavaScript, a language which for a long time struggled to be taken seriously.

Perhaps more significant though is the server-client distinction: While there are numerous elaborations on architectural styles explaining how to manage that distinction -- e.g. ROCA -- there is often a perceived lack of concrete guidance on how to implement these concepts.1

This frequently leads to highly procedural and comparatively unstructured code for front-end augmentations. While it is useful that JavaScript and the browser allow this direct and unmediated approach, as it encourages and simplifies initial explorations by reducing overhead, that style quickly leads to implementations which are difficult to maintain.

This article will present an example of evolving a simple widget from a largely unstructured code base to a reusable component.

Filtering Contacts

The purpose of this sample widget is to filter a list of contacts by name. The final result -- including the history of its evolution -- is provided as a GitHub repository; readers are encouraged to review the commits and comment there.

In line with the principles of progressive enhancement, we start out with a basic HTML structure describing our data, here using the h-card microformat to take advantage of established semantics, which helps provide a meaningful contract:

 <!-- index.html --> 
  
<ul>
<li class="h-card">
<img src="http://example.org/jake.png" alt="avatar" class="u-photo">
<a href="http://jakearchibald.com" class="p-name u-url">Jake Archibald</a>
(<a href="mailto:jake@example.com" class="u-email">e-mail</a>)
</li>
<li class="h-card">
<img src="http://example.org/christian.png" alt="avatar" class="u-photo">
<a href="http://christianheilmann.com" class="p-name u-url">Christian Heilmann</a>
(<a href="mailto:christian@example.com" class="u-email">e-mail</a>)
</li>
<li class="h-card">
<img src="http://example.org/john.png" alt="avatar" class="u-photo">
<a href="http://ejohn.org" class="p-name u-url">John Resig</a>
(<a href="mailto:john@example.com" class="u-email">e-mail</a>)
</li>
<li class="h-card">
<img src="http://example.org/nicholas.png" alt="avatar" class="u-photo">
<a href="http://www.nczonline.net" class="p-name u-url">Nicholas Zakas</a>
(<a href="mailto:nicholas@example.com" class="u-email">e-mail</a>)
</li>
</ul>

Note that whether the corresponding DOM structure is based on HTML provided by the server or generated by another component is not actually relevant, as long as our component can rely on this foundation -- which essentially constitutes a DOM-based data structure of the form [{ photo, website, name, e-mail }] -- to be present upon initialization. This ensures loose coupling and avoids tying ourselves to any particular system.

With this in place, we can begin to implement our widget. The first step is to provide an input field for the user to enter the desired name. This is not part of the DOM contract but entirely the responsibility of our widget and thus injected dynamically (after all, without our widget there would be no point in having such a field at all).

 // main.js     
var contacts = jQuery("ul.contacts");
jQuery('<input type="search" />').insertBefore(contacts);

(We're using jQuery here merely for convenience and because it is widely familiar; the same principles apply whichever DOM manipulation library, if any, is used.)

This script file, along with the jQuery dependency, is referenced at the bottom of our HTML file.

Next we attach the desired functionality -- hiding entries which do not match the input -- to this newly created field:

 // main.js     
var contacts = jQuery("ul.contacts");
jQuery('<input type="search" />').insertBefore(contacts).on("keyup", onFilter);
function onFilter(ev) {
var filterField = jQuery(this);
var contacts = filterField.next();
var input = filterField.val();
var names = contacts.find("li .p-name");
names.each(function(i, node) {
var el = jQuery(node);
var name = el.text();
var match = name.indexOf(input) === 0;
var contact = el.closest(".h-card");
if(match) {
contact.show();
} else {
contact.hide();
}
});
}

(Referencing a separate, named function rather than defining an anonymous function inline often makes callbacks more manageable.)

Note that this event handler relies on a particular DOM environment of the element triggering that event (mapped to the execution context  this here). From this element we traverse the DOM to access the list of contacts and find all elements containing a name within (as defined by the microformat semantics). When a name does not begin with the current input, we hide the respective container element (traversing upwards again), otherwise we ensure it's visible.

Testing

This already provides the basic functionality we asked for -- so it's a good time to solidify that by writing a test2. In this example we will be using QUnit.

We begin with a minimal HTML page to serve as entry point for our test suite. Of course we also need to reference our code along with its dependencies (in this case, jQuery), just like in the regular HTML page we created previously.

  <!-- test/index.html -->     
<div id="qunit"></div>
<div id="qunit-fixture"></div>

<script src="jquery.js"></script>
<script src="../main.js"></script>

<script src="qunit.js"></script>
 

With that infrastructure in place, we can add some sample data -- a list of h-cards, i.e. the same HTML structure we started out with -- to the #qunit-fixture element. This element is reset for each test, providing a clean slate and thus avoiding side effects.

Our first test ensures that the widget was initialized properly and that filtering works as expected, hiding DOM elements that don't match the simulated input:

 // test/test_filtering.js     
QUnit.module("contacts filtering", {
setup: function() { // cache common elements on the module object
this.fixtures = jQuery("#qunit-fixture");
this.contacts = jQuery("ul.contacts", this.fixtures);
} });
QUnit.test("filtering by initials", function() {
var filterField = jQuery("input[type=search]", this.fixtures);
QUnit.strictEqual(filterField.length, 1);
var names = extractNames(this.contacts.find("li:visible"));
QUnit.deepEqual(names, ["Jake Archibald", "Christian Heilmann",
"John Resig", "Nicholas Zakas"]);
filterField.val("J").trigger("keyup"); // simulate user input
var names = extractNames(this.contacts.find("li:visible"));
QUnit.deepEqual(names, ["Jake Archibald", "John Resig"]);
});
function extractNames(contactNodes) {
return jQuery.map(contactNodes, function(contact) {
return jQuery(".p-name", contact).text();
});
}

(strictEqual avoids JavaScript's type coercion, thus preventing subtle errors.)

After amending our test suite with a reference to this test file (below the QUnit reference), opening the suite in the browser should tell us that all tests passed:

Animations

While our widget works fairly well, it isn't very attractive yet, so let's add some simple animations. jQuery makes this very easy: We just have to replace show and hide with slideUp and slideDown, respectively. This significantly improves the user experience for our modest example.

However, re-running the test suite, it now claims that filtering did not work, with all four contacts still being displayed:

This is because animations (just like AJAX operations) are asynchronous, so the filtering results are checked before the animation has completed. We can use QUnit's asyncTest to defer that check accordingly:

  // test/test_filtering.js          

QUnit.asyncTest("filtering by initials", 3, function() { // expect 3 assertions
// ...
filterField.val("J").trigger("keyup"); // simulate user input
var contacts = this.contacts;
setTimeout(function() { // defer checks until animation has completed
var names = extractNames(contacts.find("li:visible"));
QUnit.deepEqual(names, ["Jake Archibald", "John Resig"]);
QUnit.start(); // resumes test execution
}, 500);
});

Since checking the test suite in the browser can become tedious, we can use PhantomJS, a headless browser, along with the QUnit runner to automate the process and emit results on the console:

 $ phantomjs runner.js test/index.html   
Took 545ms to run 3 tests. 3 passed, 0 failed.

This also makes it easy to automate tests via continuous integration. (Though of course it doesn't cover cross-browser issues since PhantomJS uses WebKit only. However, there are headless browsers for Firefox's Gecko and for Internet Explorer's Trident engines as well.)

Containment

So far our code is functional, but not very elegant: For starters, it litters the global namespace with two variables - contacts and onFilter - since browsers do not execute JavaScript files in isolated scope. However, we can do that ourselves to prevent leaking into the global scope. Since functions are the only scoping mechanism in JavaScript, we simply wrap an anonymous function around the entire file and then call this function at the bottom:

 (function() {    
var contacts = jQuery("ul.contacts");
jQuery('<input type="search" />').insertBefore(contacts).on("keyup", onFilter);
function onFilter(ev) {
// ...
}
}());

This is known as an immediately invoked function expression (IIFE).

Effectively, we now have private variables within a self-contained module.

We can take this one step further to ensure we don't accidentally introduce new global variables by forgetting a var declaration. For this we activate strict mode, which protects against a host of common traps3:

 (function() {    
"use strict"; // NB: must be the very first statement within the function
// ...

}());

Specifying this within an IIFE wrapper ensures that it only applies to modules where it was explicitly requested.

Since we now have module-local variables, we can also use this to introduce local aliases for convenience - for example in our tests:

 // test/test_filtering.js

(function($) {
"use strict";
var strictEqual = QUnit.strictEqual;
// ...
var filterField = $("input[type=search]", this.fixtures);
strictEqual(filterField.length, 1);
}(jQuery));

We now have two shortcuts - $ and strictEqual, the former being defined via an IIFE argument - which are valid only within this module.

Widget API

While our code is fairly well structured now, the widget is initialized automatically on startup, i.e. whenever the code is first loaded. This makes it difficult to reason about and also prevents dynamic (re)initialization, e.g. on different or newly created elements.

Remedying this simply requires putting the existing initialization code into a function:

 // widget.js
  
window.createFilterWidget = function(contactList) {
$('<input type="search" />').insertBefore(contactList). on("keyup", onFilter); };

This way we have decoupled the widget's functionality from its life cycle within the respective application. Thus responsibility for initialization is shifted to the application -- or, in our case, the test harness -- which usually means a tiny bit of "glue code" to manage widgets within the application's context.

Note that we're explicitly attaching our function to the global window, as that's the simplest way to make functionality accessible outside our IIFE. However, this couples the module internals to a particular, implicit context: window might not always be the global object (e.g.in Node.js).

A more elegant approach is to be explicit about which parts are exposed to the outside and to bundle that information in one place. For this we can take advantage of our IIFE once more: Since it is just a function, we simply return the public parts -- i.e. our API -- at the bottom and assign that return value to a variable in the outer (global) scope:

 // widget.js
  
var CONTACTSFILTER = (function($) {
function createFilterWidget(contactList) { // ... }
// ...
return createFilterWidget;
}(jQuery));

This is known as the revealing module pattern. The use of capitals is a convention to highlight global variables.

Encapsulating State

At this point, our widget is both functional and reasonably structured, with a proper API. However, introducing additional functionality in the same fashion - purely based on combining mutually independent functions - can easily lead to chaos. This is particularly relevant for UI components where state is an important factor.

In our example, we want to allow users to decide whether the filtering should be case-sensitive, so we add a checkbox and extend our event handler accordingly:

 // widget.js
  
var caseSwitch = $('<input type="checkbox" />');
// ... function onFilter(ev) { var filterField = $(this); // ... var caseSwitch = filterField.prev().find("input:checkbox"); var caseSensitive = caseSwitch.prop("checked"); if(!caseSensitive) { input = input.toLowerCase(); } // ... }

This further increases reliance on the particular DOM context in order to reconnect to the widget's elements within the event handler. One option is to move that discovery into a separate function which determines the component parts based on the given context. A more conventional option is the object-oriented approach. (JavaScript lends itself to both functional and object-oriented4 programming, allowing the developer to choose whichever style is best suited for the given task.)

Thus we can rewrite our widget to spawn an instance which keeps track of all its components:

 // widget.js
  
function FilterWidget(contactList) { this.contacts = contactList; this.filterField = $('<input type="search" />').insertBefore(contactList); this.caseSwitch = $('<input type="checkbox" />'); }

This changes the API slightly, but significantly: Instead of calling createFilterWidget(...), we now initialize our widget with new FilterWidget(...) - which invokes the constructor in the context of a newly-created object (this). In order to highlight the need for the new operator, constructor names are capitalized by convention (much like class names in other languages).5

Of course we also need to port the functionality to this new scheme - starting with a method to hide contacts based on the given input, which closely resembles the functionality previously found in onFilter:

 // widget.js
  
FilterWidget.prototype.filterContacts = function(value) { var names = this.contacts.find("li .p-name"); var self = this; names.each(function(i, node) { var el = $(node); var name = el.text(); var contact = el.closest(".h-card"); var match = startsWith(name, input, self.caseSensitive); if(match) { contact.show(); } else { container.hide(); } }); }

(Here self is used to make this accessible within the scope of the each callback, which has its own this and thus cannot access the outer scope's directly. Thus referencing self from the inner scope creates a closure.)

Note how this filterContacts method, rather than performing context-dependent DOM discovery, simply references elements previously defined in the constructor. String matching has been extracted into a separate general-purpose function -- illustrating that not everything necessarily needs to become an object method:

 function startsWith(str, value, caseSensitive) {
     if(!caseSensitive) {
         str = str.toLowerCase();
         value = value.toLowerCase();
     }
     return str.indexOf(value) === 0;
 }

Next we attach event handlers, without which this functionality would never be triggered:

  // widget.js    
function FilterWidget(contactList) {
// ...
this.filterField.on("keyup", this.onFilter);
this.caseSwitch.on("change", this.onToggle);
}
FilterWidget.prototype.onFilter = function(ev) {
var input = this.filterField.val(); this.filterContacts(input);
};
FilterWidget.prototype.onToggle = function(ev) {
this.caseSensitive = this.caseSwitch.prop("checked");
};

Running our tests - which, apart from the minor API change above, should not require any adjustments - will reveal an error here, as thisis not what we might expect it to be. We've already learned that event handlers are invoked with the respective DOM element as execution context, so we need to work around that in order to provide access to the widget instance. For this we can take advantage of closures to remap the execution context:

 

 // widget.js
  
function FilterWidget(contactList) { // ... var self = this; this.filterField.on("keyup", function(ev) { var handler = self.onFilter; return handler.call(self, ev); }); }

(call is a built-in method to invoke any function in the context of an arbitrary object, with the first argument corresponding to this within that function. Alternatively apply might be used in combination with the implicit arguments variable to avoid explicitly referencing individual arguments within this indirection: handler.apply(self, arguments).6)

The end result is a widget where each function has a clear, well-encapsulated responsibility.

jQuery API

When using jQuery, the current API seems somewhat inelegant. We can add a a thin wrapper to provide an alternative API that feels more natural to jQuery developers:

 jQuery.fn.contactsFilter = function() {
     this.each(function(i, node) {
         new CONTACTSFILTER(node);
     });
     return this;
 };

(A more elaborate contemplation is provided by jQuery's own plugin guide.)

Thus we can use jQuery("ul.contacts").contactsFilter(), while keeping this as a separate layer ensures that we're not tied to this particular ecosystem; future versions might provide additional API wrappers for different ecosystems or even decide to remove or replace jQuery as a dependency. (Of course in our case, abandoning jQuery would also mean we'd have to rewrite the internals accordingly.)

Conclusion and Outlook

Hopefully this article managed to convey some of the key principles of writing maintainable JavaScript components. Of course not every component should follow this exact pattern, but the concepts presented here should provide the necessary toolkit essential to any such component.

Further enhancements might use the Asynchronous Module Definition (AMD), which improves encapsulation and makes explicit any dependencies between modules, thus allowing for loading code on demand - e.g. via RequireJS.

In addition, there are exciting new developments on the horizon: The next version of JavaScript (officially ECMAScript 6) will introduce a language-level module system, though as with any new feature, widespread availability depends on browser support. Similarly, Web Components is an upcoming set of browser APIs intended to improve encapsulation and maintainability - many of which can be experimented with today using Polymer. Though how well Web Components fare with progressive enhancement remains to be seen.

 

1This is less of an issue for single-page applications, as the respective roles of server and client are very different in this context. However, a juxtaposition of these approaches is beyond the scope of this article.

2Arguably the test(s) might have been written first.

3JSLint can additionally be used to protect against such and other common issuesIn our repository we're using JSLint Reporter.

4JavaScript uses prototypes rather than classes - the main difference being that whereas classes are usually "special" in some way, here any object can act as a prototype and can thus be used as a template for creating new instances. For the purposes of this article, the difference is negligible.

5Modern versions of JavaScript introduced Object.create as an alternative to the "pseudo-classical" syntax. The core principles of prototypal inheritance remain the same.

6jQuery.proxy might be used to shorten this to this.filterField.on("keyup", $.proxy(self, "onFilter"));

About the Author

Frederik Dohr started his career as a reluctant web developer hacking on TiddlyWiki, sometimes called the original single-page application. After a few years of working with a bunch of clever folks at Osmosoft, BT's open source innovation team, he left London to return to Germany. He now works for innoQ, where he continues his vocal quest for simplicity while gaining a whole new perspective on developing with, for and on the web.

 

 

 

Hello stranger!

You need to Register an InfoQ account or to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Tell us what you think

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread
Community comments

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

Discuss

Educational Content

General Feedback
Bugs
Advertising
Editorial
InfoQ.com and all content copyright © 2006-2013 C4Media Inc. InfoQ.com hosted at Contegix, the best ISP we've ever worked with.
Privacy policy
BT