Loosely Coupled JavaScript Using PubSub
I recently watched a very interesting video by Rebecca Murphy that discussed using the pubsub architecture to create loosely coupled JavaScript. The concept is to develop your JavaScript objects and allow the communication between the objects to occur via events that are managed by a publisher / subscriber (pubsub) service. The diagram below illustrates a pubsub service that exposes three methods: publish, subscribe, and unsubscribe. In this case, two ‘panels’ are subscribing to the service.
A data source in the system then publishes data to the pubsub service. The pubsub service forwards this data to the appropriate subscribers. This is illustrated below.
The pubsub service allows subscriptions to specific ‘events’. Much like you can subscribe to specific channels with your cable TV provider.
Let’s take a look at a specific example. Imagine we have a web page. On that we page is a form that allows the user to enter their name, lucky number, and select a favorite color.
In addition to this form we have a number of other panels that present the data in a specialized way. For example we could have a panel that presents the names of all registered users. Another panel could present data about only the most recent users. While another displays a list of unique lucky numbers. Yet another presents color swatches of all the favorite colors. The development of these panels could be quite complex. Each will probably have its own HTML, CSS, and JavaScript.
By using the pubsub service to couple these panels to the data source, each can be developed independently of the others. The development process feels a bit like using an IOC container. Develop each component and then bind them together. Here is a link to a demo page.
HTML
The markup for this example is shown below:
<div id="page"> <div id="form"> <p> <label for="first">First Name</label> <input type="text" id="first" /> </p> <p> <label for="last">Last Name</label> <input type="text" id="last" /> </p> <p> <label for="number">Lucky Number</label> <input type="text" id="number" /> </p> <p> <label for="color">Favorite Color</label> <input type="text" id="color" name="color" value="#006699" /> <div id="colorpicker"></div> </p> <p> <button id="add">Add</button> </p> </div> <div id="panel-wrap"> <div id="names" class="panel"></div> <div id="lastuser" class="panel"></div> <div id="numbers" class="panel"></div> <div id="colors" class="panel"></div> </div> </div>
There are two main sections. The top defines the form (in a literal sense not the html element) that is used as the data source. The bottom creates placeholders for the panels.
JavaScript
I am using a slightly modified version of a pubsub plug-in created by Peter Higgins. The only modification that I did was to add a try catch block around the publish method. Without that modification any one subscriber can prevent later subscribers from receiving the event by having ‘exceptional code’ (in a bad way). For completeness here is the final code (without the comments):
;(function(d){ var cache = {}; d.publish = function(/* String */topic, /* Array? */args){ try{ d.each(cache[topic], function(){ this.apply(d, args || []); }); } catch (err) { // handle this error console.log(err); } }; d.subscribe = function(/* String */topic, /* Function */callback){ if(!cache[topic]){ cache[topic] = []; } cache[topic].push(callback); return [topic, callback]; // Array }; d.unsubscribe = function(/* Array */handle){ var t = handle[0]; cache[t] && d.each(cache[t], function(idx){ if(this == handle[1]){ cache[t].splice(idx, 1); } }); }; })(jQuery);
The main page uses this service to bind publishers and subscribers to each other. The relevant JavaScript is shown below:
// Initialize the panels // 1st param: the container element // 2nd param: the event to subscribe var userPanel = new UserPanel( $('#names'), 'new/user'); var lastPanel = new LastUser( $('#lastuser'), 'new/all'); var numberPanel = new LuckyNumbers( $('#numbers'), 'new/number'); var colorPanel = new FavColors( $('#colors'), 'new/color'); // Bind to the click event $('#add').click(function(){ // Get the values from the form var first = $('#first').val(); var last = $('#last').val(); var color = $('#color').val(); var number = $('#number').val(); if(isNaN(number)){ alert("woah...that's not a number"); $('#number').focus(); return; } // Publish the new data $.publish('new/user', [first,last]); $.publish('new/number', [number]); $.publish('new/color', [color]); $.publish('new/all', [first,last,number,color]); });
First this code creates and initializes four panel objects. Each panel takes a container element and an event in the constructor. Each panel then subscribes to the appropriate event and renders itself. The code above binds to the ‘click’ event of the form that we created earlier. At the bottom of that binding are the publish events supported by the form. The data is sliced up and made available via a number of events that expose only that slice.
Each panel is contained in its own JavaScript file. They all follow the same pattern. The following code shows the ‘UserPanel’ object. View source the demo for the other panels.
function UserPanel(parent, event){ var _parent = parent; var _html = ''; var _title = 'Registered Users'; var _tmpl = '<li>{{name}}</li>'; var _renderUserPanel = function(name){ if(name!=undefined){ var elem = _tmpl.replace('{{name}}', name); _html = _html + elem; } var result = '<h3>' + _title + '</h3>'; if(_html!=''){ result += '<ul>' + _html + '</ul>'; } else { result += '<ul><li>no users</li></ul>'; } _parent.html(result); } var _subscribe = function(event){ $.subscribe(event, function(first, last){ var name = first + ' ' + last; _renderUserPanel(name); }); } // render the initial markup _renderUserPanel(); if(event!=undefined){ _subscribe(event); } }
Summary
There is a definite trade off with this architecture. On one hand there is a bit more code. On the other hand the architecture is more decoupled. I struggled a bit to come up with a meaningful example that tips the balance in favor of decoupled architecture. With simple pubsub examples, the added code appears as too much cost for the added benefit. I hope this example has found the right balance. If you know of a better example, please provide links in the comments.
Great example of a pubsub system with Javascript.
I think adding the try/catch is a very good idea.
Great article!!! do you know if the “;” at the beginning of the plugin is deliberate or is that a typo?
The purpose of the ‘;’ is to terminate any unterminated lines of javascript that precede this code. I’ve seen it done elsewhere, and haven’t really put too much thought into whether it is the ‘right thing to do’. Seemed at best a good defensive move, and at worst an extra character.
Bob
Just thought I’d note that you may wish to reconsider using console without at least assigning a default console variable. Those few who view your demo in IE without the Dev Console open would get a JavaScript error at that point in your plugin…
Otherwise, this is a great, concise overview! Thanks!
Thanks for the the overview. This came in handy!
Good post. I absolutely appreciate this site.
Continue the good work!