Decoupling your Javascript files

Here's a design pattern I've used a few times now, to help decouple large Javascript applications into multiple separate files. Let's say you already write your Javascript in separate files, depending on what functionality it provides. Let's also say you use the following function wrappers, to avoid cluttering the default window namespace:

// Basic form
(function() {
  var /*...*/ ;
  var /*...*/ ;
})();
 
// Extension to use a short form of jQuery, only in your own code
(function($) {
  var /*...*/ ;
  var /*...*/ ;
})(jQuery);

Well done if so. You're writing very tidy Javascript. But do you ever find that it's too tidy?

For example, say you've written two pieces of functionality to work on a webpage, and they need to communicate between them. Maybe you've got accordion navigation that you want to be disabled, or behave differently, when you switch drag and drop on. But how do you:

  1. have the two files communicate this
  2. in a decoupled way - i.e. responding behaviours in the right files
  3. so that if one or other file isn't present, nothing breaks

You can quickly (if messily) solve a. by breaking your encapsulation: have a window.foo() function defined in the first module, that the second module can access. Or you can solve a. and b. by breaking your modularity: have code in the second module that knows about the internals of the first module and does the right thing. You can then go on to solve a, b. and c. by checking in the second module that the first module's behaviours have been applied. But every time you solve it like this, you'll do so slightly differently, so that along with breaking modularity and writing lots of ad-hock checking code, your application will become hard to maintain.

Enabling and enforcing modular decoupling

What we need instead is a way of communicating using events and registered handlers, that isn't dependent on particular elements being both present and also known to all modules. We could sub-type the event with a textual "key" that indicates what kind of communication we're passing between modules. And if a module doesn't exist, or isn't listening, then the worst-case scenario is that nothing happens: silently, with the minimum of brittleness.

With all that in mind, then before you load your two modular files, load a third file containing just this piece of Javascript (I'm using .bind(), in order to be compatible with Drupal 7's jQuery 1.4.x):

/**
 * Decoupled communication between Javascript files
 */
(function($) {
 
var comms = {};
// Listener on the comms event
$(document).bind("comms", function(ev, message, data) {
  var data = data || {};
  if (message in comms) {
    for (var i = 0; i < comms[message].length; i++) {
      comms[message][i](data);
    }
  }
});
// Listener to permit Javascript files to register their callbacks
$(document).bind("commsregister", function(ev, message, callback) {
  if (!(message in comms)) {
    comms[message] = [];
  }
  comms[message].push(callback);
});
 
})(jQuery);

What does this do that's special? Well, it binds handlers to two event types, on the document special DOM element, which all in-browser Javascript can reasonably assume exists. Calling the first handler paves the way for the second one:

  1. When a JS module fires the commsregister event, passing in a textual "key" and a function, the function is registered as a handler for comms events carrying that key.
  2. When a JS module fires the comms event, passing in a key and some optional data, then this event and data is passed on to all registered callbacks for that key.

The comms handlers are stored by the decoupler in a private comms variable that nobody else can fool around with, and this becomes the key to safe, lightly-coupled communication between your modular Javascript files.

Example

Here's an example HTML file that brings two modules together with the decoupler, to illustrate this:

<!DOCTYPE HTML>
<html>
  <head>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"
      ></script>
    <script src="js/decoupler.js"></script>
    <script src="js/one.js"></script>
    <script src="js/two.js"></script>
  </head>
  <body>
    <h1>This is a HTML page</h1>
    <p>Here is a paragraph with <a href="#">a link that goes nowhere</a>. Try clicking it, though!</p>
  </body>
</html>

And here are the two communicating Javascript files:

one.js

(function($) {
 
// This registers a comms listener but then just waits for incoming comms
$(document).trigger("commsregister", ["bang", function() { 
  console.log("one.js received incoming comms for 'bang' event");
  alert("Bang!");
}]);
 
})(jQuery);

two.js

(function($) {
 
// Once the document is fully loaded and we can see DOM elements...
$(function() {
  // ... then we bind an event handler to the onclick event of the HTML link,
  // preventing onclick default, transmitting a "bang" comms event instead
  $("a").click(function(ev) {
    ev.preventDefault();
    console.log("two.js sending outgoing comms for 'bang' event");
    $(document).trigger("comms", "bang");
  });
});
 
})(jQuery);

What does this example do? Well, what it does is almost as important as its failure modes - what it doesn't do, and how it doesn't do it!

  1. On DOM parsing, one.js registers itself as a handler for comms events of sub-type "bang". If two.js hasn't loaded, page load continues silently.
  2. On document ready, two.js hijacks the link's onclick event with its own handler. If two.js hasn't loaded, execution ends silently.
  3. When the user clicks the link, two.js turns the onclick event into a "bang" comms event, passing it up to the document for any handling. If the decoupler hasn't loaded, execution ends silently.
  4. When the decoupler receives the comms "bang" event, it looks to see if any functions have been registered as callbacks for that sub-type of comms event. If one.js hasn't loaded, execution ends silently.
  5. Finally, the "bang" comms event is handled by the callback in one.js, and an alert box is displayed.

There are also console.log() calls in there for debugging purposes: run in a browser that supports them to see when different bits of code are fired.

Silent decoupling achieved

As you can see above, at every phase of the execution process, then if there's any syntax errors in other files, or even network outages, then the remaining modules just quietly decouple.

Obviously a more complex application will need to deal with absent modules one way or another, but already you have an application where separate resources can be loaded, and not be rigidly interdependent solely through a shared global namespace.