JavaScript

Publisher Subscriber Pattern

By Niall Kader3 min read

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);