This post was originally published on the New Bamboo blog, before New Bamboo joined thoughtbot in London.
Websockets-based activity dashboard app. Read on to know more about the ideas behind it
In this post I’ll attempt to summarise some patterns for designing event-based Javascript applications extracted from our projects and previous blog posts on the subject. I’ll end with an overview of the techniques described and how they play together in a real-world application. This post is a follow-up from a presentation at the London Javascript User Group.
Background
We’ve blogged about Javascript patterns for complex applications before. If you’re short on time, that post describes the way we lay out our client-side apps here at New Bamboo. It’s an MVC-like setup where we have Model objects doing the persistence and data binding, and View objects listening to state change in those models via the observer patterns and custom events. If you haven’t, I strongly recommend you read that post before this one.
The key component of this approach is using custom events to decouple the communication between layers. This is a simple View example *:
var UserPane = function(event_source, $container){
function update(event, user){
$container.append('<li>'+user.name+'</li>')
}
$(event_source).bind('user_added', update)
}
And you would use it like:
new UserPane(document, $('ul#user_list'));
// Wrap DOM events and re-trigger with useful data using jQuery
$('#users input:radio').change(function(evt){
$(document).trigger('user_added', {name: $(this).val()});
});
The example defines a View class that knows in which DOM element to render content ($container) and binds itself to the ‘user_added’ event triggered on the document object by other layers in the application.
We’re passing the document
element as the source of events, but
you’re free to swap it by anything that you can bind to and trigger events from.
It doesn’t even need to be a DOM element, and this is where OO and evented
programming starts to look less counter-intuitive than one might have thought:
you can build your own Event Emitters and model your domain around them.
Event Emitters In the wild
That is exactly what libraries such as Node.js are doing with an EventEmitter class you can extend from. The following is an object that emits a “tick” event with a random number every 5 seconds.
var EventEmitter = require('events').EventEmitter,
puts = require('sys').puts;
var Ticker = function( interval ){
var self = this,
nextTick = function(){
self.emit('tick', Math.random() * 1000);
setTimeout(nextTick, interval);
}
nextTick();
};
// Extend from EventEmitter 'addListener' and 'emit' methods
Ticker.prototype = new EventEmitter;
// A ticker instance with an interval of 5 seconds
var ticktock = new Ticker( 5000 );
// Bind an event handler to the 'tick' event
ticktock.addListener('tick', function( number ) {
puts('number emitted: '+ number);
});
The nice thing about this is that objects interacting with the Ticker instance
only need to bind themselves to events emitted by it. There’s no need for public
methods apart from addListener
(or $.bind
in our previous jQuery example).
How we use Event Emitters
Back in the browser side of things, you can build event emitters that represent objects in your domain and trigger events when their status changes. Then other parts of the application can observe those events and react accordingly. A nice abstraction of this pattern is js-model, an ORM-like Javascript library we developed to model domain objects with extra server persistence baked-in.
// Js-model example
var Post = Model("post")
Post.bind("add", function(new_post) {
addObjectIntoUI(new_post)
})
// This will trigger the 'add' event on the Post object,
// executing any handlers observing it.
var post = new Post({ foo: "bar" })
More info on js-model on its documentation page.
This is also the approach that we’ve taken with our Pusher websockets service. With it you can bind your local objects to events coming from the server in a completely transparent way.
var server = new Pusher('you app key', 'some channel');
server.bind('user_added', function(user){
$('ul#user_list').append('<li>'+user.name+'</li>')
})
And of course you would build your own objects to do the binding and keep your main app clean and expressive.
new UserPane( server, $('ul#user_list') );
I talk about how this is done in this post about json websockets.
Do it yourself
Because Pusher.js is framework-agnostic, we don’t use jQuery’s event-binding
API. Instead, Pusher implements its own bind
method. But once
you’re using this pattern in one part of your app there’s no reason you couldn’t
use it for the rest of your domain objects. The only thing they need to do is
implement binding and triggering of events. Here is a simple abstract
prototype you can use in your own objects. This is the Ticker example again
using that snippet:
/* Periodically send out dummy events
--------------------------------------------*/
var Ticker = function( interval ){
var self = this,
nextTick = function(){
self.trigger('tick', Math.random() * 1000);
setTimeout(nextTick, interval);
}
nextTick();
};
// Extend from AbstractEventsDispatcher 'bind' and 'trigger' methods
Ticker.prototype = new AbstractEventsDispatcher;
var ticktock = new Ticker( 5000 );
ticktock.bind('tick', function( number ) {
puts('number emitted: '+ number);
});
The only reason I’ve changed ‘addListener’ to ‘bind’ and ‘emit’ to ‘trigger’ is to keep it familiar for jQuery users.
What’s important here is that once you model your objects around this minimal event-centric interface (‘bind’ and ‘trigger’) it makes little difference what your event emitter objects actually do -and how they do it. You app can be fully evented while retaining the best bits of object orientation such as polymorphism and inheritance.
A working application
I’ve recently built an activity dashboard app that uses Pusher when deployed to production but a mocked, in-browser event emitter object in development to simulate a stream of events coming from the server. My View objects are oblivious to the origin of those events and just focus on binding to them and rendering them onscreen as they come. You can see it working on the video above.
It starts by defining a series of self-contained widget objects. I then loop
through them and instantiate each with the same instance of an event emitter -in
this case Pusher or a mock server implementing bind
for custom
events.
This is the basic code:
var server = new MockServer() // or new Pusher('app_key', 'channel_name'), or your own event emitter
// Instantiate widgets
for(widget in Widgets.available){
new widget( server );
}
And this is a simplified widget that listens to ‘closed_order’ events and plays a sound using HTML5’s audio tag:
Widgets.available.soundAlert = function( server ){
// add audio files to document
var $audio = $('<audio src="/audio/alert.mp3" preload="true" />').appendTo('body');
function play(data){
$audio.load().play();
}
// bind to events you want to alert
server.bind( 'order_closed', play )
}
I can add as many widgets as I want (and even remove them, pause them and inspect them in run-time), with no dependencies between them. Because they bind themselves to events they’re interested in, each one is completely orthogonal to the system.
Finally, this is an example of the MockServer object I use to replace Pusher for development purposes:
/* Periodically send out dummy events
--------------------------------------------*/
var MockServer = function(){
var self = this, interval, event_names = ['order_closed', 'order_cancelled', 'order_shipped'];
function randomEvent(){
var event_name = event_names[Math.round(Math.random()*2)];
self.trigger(event_name, {
info: 'Mock order, 3 products',
total: Math.random() * 1000
});
}
interval = setInterval(randomEvent, 2000);
};
// Extend event-binding interface
MockServer.prototype = new AbstractEventsDispatcher;
Wrap up
This illustrates the general architecture that permeates all recent Javascript projects that we’ve developed. In future articles we’ll talk about techniques and specific problems in more detail.
Even though I’ve included an abstract class that I used for the examples on this page, the emphasis is on the patterns more than on particular implementations and APIs.