Publisher Subscriber Pattern

I've been experimenting with the publisher/subscriber pattern in JavaScript. Admittely, this blog post is not polished, but I wanted to get it up so that I wouldn't forget about it. Hopefully it won't remain neglected for long.

Note that this pattern is similar to the Observer pattern, but may be a little more flexible because the Observer pattern requires each listener to implement an update() method. With pub/sub you can pass in a callback that gets triggered when an event is published.

Here's the HTML:

<!doctype html>
<html lang="en-US">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Widget w/PubSub that can be subclassed</title>
<style type="text/css">
#widgetList li{list-style:none; background-color:#ccc; border:1px solid black; padding:6px 10px; cursor: pointer; }
#widgetList li:hover{background-color:#fff; }
</style>
</head>
<body>
<div id="widgetList"></div>
</body>
</html>

 

Here's the publisher base class:

// set up namespace
rem = {};

rem.Publisher = function(){

this.eventTypes = {};
// eventType's key is an event name and the value is an array of objects
// each object in the array has 'subsriber' property and a 'callback' property
// for ex: eventTypes['click'] = [{subscriber: someSubsrciber, callback: someFunction},...]
};


rem.Publisher.prototype.subscribe = function(type, subscriber, callback){

// get all subcribers who are already registered for this event type
var subscribers = this.eventTypes[type];

if(subscribers){
// check to see if the subscriber is already in the array
if(this.getSubscriberIndex(subscribers, subscriber) != -1){
// the subscriber is already registered for this message
return;
}
}else{
// there are no subscribers for this event type so create an array
subscribers = [];
this.eventTypes[type] = subscribers;
}

// add the subscriber
subscribers.push({subscriber: subscriber, callback: callback});
};


rem.Publisher.prototype.unsubscribe = function(type, subscriber){
if(subscriber){
var subscribers = this.eventTypes[type];

if(subscribers){
var i = this.getSubscriberIndex(subscribers, subscriber);
if(i != -1){
this.eventTypes[type].splice(i,1);
}
}
}else{
delete this.eventTypes[type];
}
}


rem.Publisher.prototype.publish = function(type){
// get all the subscribers for this event type
var subscribers = this.eventTypes[type];

if(subscribers){
// loop through them and trigger the callback
for(var i=0; i < subscribers.length; i++){

//get the args for the callback (so they can be used in the .apply() call)
var args = [];
for(var j=0; j < arguments.length - 1; j++){
args.push(arguments[j+1]);
}

subscribers[i].callback.apply(subscribers[i].subscriber, args);
}
}
}


rem.Publisher.prototype.getSubscriberIndex = function(subscribers, subscriber){
for(var i=0; i < subscribers.length; i++){
if(subscribers[i].subscriber == subscriber){
return i;
}
}
return -1;
}

 

Here's a concrete object that sub classes Publisher:

//WidgetList class
rem.WidgetList = function(){
//call to super...
rem.Publisher.call(this);

this.target = null;
this.widgets = [];
}

// 'inherit' the Publisher prototype
rem.WidgetList.prototype = new rem.Publisher;

rem.WidgetList.prototype.init = function(options){

// verify that options are passed in...
if(!options){
alert("no param passed into Widget.init()");
}
// verify that target element and widgets array are passed in as part of the options object...
this.target = options.target || alert("no 'target' passed in param for WidgetList.init()");
this.widgets = options.widgets || alert("no 'widget' passed in param for WidgetList.init()");

// create a UL and loop through widgets array, adding an LI for each widget...
var list = document.createElement("ul");
for(var x=0; x < widgets.length; x++){
var listItem = document.createElement("li");
listItem.innerHTML = widgets[x].name;
listItem.setAttribute("data-widget-id", widgets[x].id);
list.appendChild(listItem);
}
// append the UL to the target element...
target.appendChild(list);

// set up event handling on the UL...
var thisWidget = this;
function handleListItemClick(event){
var target = (event.target ? event.target : event.srcElement);
var widgetID;
if(widgetID = target.getAttribute("data-widget-id")){
var widgetName = target.innerHTML;
// here's the kicker, note that this particular sub class of Publisher calls publish() with the widgetName, widgetId params after the event
// type. These two params will get passed into the callback that is used when the subscription is set up (see below)
thisWidget.publish("widget_selected", widgetName, widgetID);
}
}

if(list.addEventListener){
list.addEventListener('click',handleListItemClick,false);
}else{
list.attachEvent('x', handleListItemClick);
}

}

 

Put it all together:

var myWidgetList = new rem.WidgetList();
var target = document.getElementById("widgetList");
var widgets = [{name:"Widget 1",id:1}, {name:"Widget 2",id:2},{name:"Widget 3",id:3}];

myWidgetList.init({target:target, widgets:widgets});

// in this case the subscriber is just a plain old object, but in a real project it might be something like a controller
var subscriber = {
// note that when the publisher calls publish(), it will pass 'widgetName' and 'widg'
callback: function(widgetName, widgetID){
alert("widget was clicked: " + widgetName + " ID: " + widgetID);
}
};
myWidgetList.subscribe("widget_selected", subscriber, subscriber.callback);