Subscribe to my Feed, follow me on Twitter, recommend me on Working With Rails or see my code on GitHub
A simple custom event system
Say you have a lot of elements on a page that all depend on each other in some way. When one element changes, you want to let the others know so they can take the appropriate action. You could have the changed element perform the action on the dependent element, but that could get messy real fast. A better solution is to leave them decoupled and have the changed element send an event or a message (not sure what the right terminology would be here, if I use both just assume they mean the same thing). The dependent element could subscribe to this particular event and perform the necessary action when it receives it. This is a tried and tested solution that’s surprisingly simple and easy to implement. In this article I’ll go through implementing a simple messaging system in JavaScript (I’m talking DOM elements here).
This is what we want to be able to do:
var obj = {};
Messages.subscribe('a message', function(bar){
obj.foo = bar;
});
Messages.fire('a message', 'it works');
alert(obj.foo); //Alerts "it works"
First, we need an object in which to keep everything:
Messages = {
subscriptions: {}
};
Let’s implement the subscription logic first. This is a function that let’s something subscribe to a particular event (identified by a string in this case), and pass a function that gets run when that event happens.
Messages.subscribe = function(message, callback, scope){
if (!this.subscriptions[message]) { this.subscriptions[message] = []; }
this.subscriptions[message].push({callback:callback, scope:scope});
};
This function adds an object containing the callback function (which gets run when the event it’s subscribed to happens), and an optional scope in which the callback gets run. Each message can have several subscriptions, so they’re contained in an array. The array for a particular message is created if it doesn’t already exist.
Now that we can subscribe to events, we need a way to fire one too; it would be pretty useless otherwise. The fire method will check the subscriptions object to see if there are any subscriptions for the event that’s getting fired.
Messages.fire = function(message){
var i, s, args, subscriptions;
if (subscriptions = this.subscriptions[message]) {
args = Array.prototype.slice.apply(arguments, 1);
for (i=0; i<subscriptions.length; i++) {
s = subscriptions[i];
s.callback.apply(s.scope || window, args);
}
}
};
Now we can fire an event, and all the subscription callbacks for that event will be run. Note that we pass the rest of the arguments given to the fire method on to the callback.
Finally, it should be possible to unsubscribe from an event. What this does is it goes through the array for the event specified and removes any subscriptions whose callback matches the passed function:
Messages.unsubscribe = function(message, callback){
var i, s;
if (s = this.subscriptions[message]) {
for (i=0; i<s.length; i++) {
if (s[i].callback == callback) { s.splice(i,1); }
}
}
};
- Ok, that’s neat, but what can I do with it?
Well, let’s say you’re a little weird and keep one of those blahgs everyone’s talking about. You have a content area where you list your posts in full and a sidebar where you have links to the latest posts for your readers’ convenience. You’ve also added some of that nifty Ajax technology to be able to delete a post asynchronously from the page. What if you delete a post that also is in the sidebar list? The list will be out of sync and display a link to a post that no longer exists. You could make the delete action on the post go look in the sidebar to see if it’s in there too, and remove it, but then you’ve coupled those two previously separate elements, and coupling is bad, says the experts. I guess you can see where I’m going with this.. The sidebar list can subscribe to an event that gets fired by the post’s delete action!
Let’s say this is the “content” part:
<div class="posts">
<div class="post" id="post_2">
<h3>Post 1</h3>
<p>This is post 1</p>
</div>
<div class="post" id="post_2">
<h3>Post 2</h3>
<p>This is post 2</p>
</div>
</div>
You’ll notice there aren’t any “delete” links or buttons there, we’re imagining that they’ve somehow been added dynamically. The corresponding sidebar list looks like this:
<ul class="posts">
<li id="sidebar_post_1"><a href="/posts/1">Post 1</a></li>
<li id="sidebar_post_2"><a href="/posts/2">Post 2</a></li>
</ul>
This is the function that deletes a post from the “content” part, extremely simplified for clarity:
function deletePost(post){
sendRequest('DELETE', '/posts/'+post.id);
post.removeFromDOM();
Messages.fire('post was deleted', post.id);
};
After deleting the post and removing it, the action fires a message “post was deleted”. This message is subscribed to by the sidebar list:
Messages.subscribe('post was deleted', function(id){
removePostFromSidebar(id);
});
Again, extremely simplified. When a post gets deleted, a message gets sent, along with the ID of the post that was deleted. The sidebar list subscribes to this message, so its callback function gets run, and this function in turn removes the corresponding post from the list.
