Look at this: two blog posts in as many weeks. Maybe I can do this after all :-)
Ok, so I'm kind of building up to a post I have in mind for a little later where I'll explore, in-depth, my simple approaches to implementing classical underpinnings in javascript (including a discussion on the many benefits of doing so). My
previous post speaks to one of the fundamental things that would be on any seasoned OO dev's checklist: a robust inheritance model. This week I'll be sharing another of those fundamental checklist items, which is a robust event model (meaning class level custom events, not the already available DOM events like "click" and "mouseover").
Also, in a later post I'll share why I'm stuck on
prototype.js as the basis for my code. For now I'll assume that you either already love it and use it, are interested in using it, or can make the connection from what I share here to your library of choice if you are using one of the other ones (and of course there are many options for what I'll call the "Javascript Abstraction Provider" layer besides just prototype.js).
For now, on to EventPublisher.
Why the need?
To put it simply: a robust class level custom event model is a critical component to creating a scalable, maintainable foundation for any application development platform. I talk to seasoned "old-line" engineers and consultants fairly frequently who still, to this day, dismiss serious javascript development for the same reasons almost everyone did before we all recently became a lot smarter [a lot of us anyway]. That is, they still think of scripts with disdain, fearing the spaghetti messes that usually resulted from the mish mashing approaches of yore. "Javascript is not maintainable" they still tell me. "It's messy... it doesn't scale well... it's not well suited for team development" - I still hear this stuff. Clearly, we still have a great deal of misinformed people out there.
So, the basis for my whole educational series on this blog, which started last week, and my answer to those people still not wanting to learn how to write javascript for whatever old reason, is this: "Javascript is the language of the web document. Learn it, buddy. It's expressive, powerful and malleable. Here are some ways to maintain its expressive, dynamic nature while enabling scalable, maintainable, collaborative, object oriented application development."
The code
I first wrote EventPublisher about a year and a half ago, and first posted it to the
Ruby on Rails Spinoffs mailing list just over a year ago (btw, this is the official [unofficial] support list for the prototype.js library, it's not just for RoR users). That original version can be found
here, and a quick walk through can be found
here.
Since that version, I have adopted a slightly different object model, which I touched on near the end of my post last week. So naturally I modified the EventPublisher class to take advantage of that.
Here is the new code:
EventPublisher = Class.create();
EventPublisher.prototype = {
/**
* @constructor
*/
initialize: function() {
//private variables
var allEvents = {}; // cache object
//public instance methods
/**
* Attaches a {handler} function to the publisher's {eventName} event for execution upon the event firing
* @param {String} eventName
* @param {Function} handler
* @param {Boolean} asynchFlag [optional] Defaults to false if omitted. Indicates whether to execute {handler} asynchronously (true) or not (false).
*/
this.attachEventHandler = function(eventName, handler) {
eventName = eventName + "_evt"; // appending _evt to event name to avoid collisions
// using an event cache array to track all handlers for proper cleanup
if (allEvents[eventName] == null)
allEvents[eventName] = [];
//create a custom object containing the handler method and the asynch flag
var asynchVar = arguments.length > 2 ? arguments[2] : false;
var handlerObj = {
method: handler,
asynch: asynchVar
};
allEvents[eventName].push(handlerObj);
};
/**
* Removes a single handler from a specific event
* @param {String} eventName The event name to clear the handler from
* @param {Function} handler A reference to the handler function to un-register from the event
*/
this.removeEventHandler = function(eventName, handler) {
eventName = eventName + "_evt"; // appending _evt to event name to avoid collisions
if (allEvents[eventName] != null)
allEvents[eventName] = allEvents[eventName].reject(function(obj) { return obj.method == handler; });
};
/**
* Removes all handlers from a single event
* @param {String} eventName The event name to clear handlers from
*/
this.clearEventHandlers = function(eventName) {
eventName = eventName + "_evt"; // appending _evt to event name to avoid collisions
allEvents[eventName] = null;
};
/**
* Removes all handlers from ALL events
*/
this.clearAllEventHandlers = function() {
allEvents = {};
};
/**
* Fires the event {eventName}, resulting in all registered handlers to be executed.
* @param {String} eventName The name of the event to fire
* @params {Object} args [optional] Any object, will be passed into the handler function as the only argument
*/
this.fireEvent = function(eventName) {
var evtName = eventName + "_evt"; // appending _evt to event name to avoid collisions
if (allEvents[evtName] != null) {
var len = allEvents[evtName].length; //optimization
for (var i = 0; i < len; i++) {
try {
if (arguments.length > 1) {
if (allEvents[evtName][i].asynch) {
var eventArgs = arguments[1];
var method = allEvents[evtName][i].method.bind(this);
setTimeout(function() { method(eventArgs) }.bind(this), 10);
}
else
allEvents[evtName][i].method(arguments[1]);
} else {
if (allEvents[evtName][i].asynch) {
var eventHandler = allEvents[evtName][i].method;
setTimeout(eventHandler, 1);
}
else if (allEvents && allEvents[evtName] && allEvents[evtName][i] && allEvents[evtName][i].method)
allEvents[evtName][i].method();
}
} catch (e) {
if (this.id)
{
alert("error: error in " + this.id + ".fireEvent():\n\nevent name: " + eventName + "\n\nerror message: " + e.message);
}
else
alert("error: error in [unknown object].fireEvent():\n\nevent name: " + eventName + "\n\nerror message: " + e.message);
}
}
}
};
}
};
My reasons for defining classes as above (defining instance only members within the constructor) are, again, explained in last week's post.
Creating a global event dispatcher object
Ok, now we have the EventPublisher class as a tool in our box. How to put it to use. The first way is to create an instance of it that is accessible at the global level:
var globalEvents = new EventPublisher();
...which all your other objects will be able to use to publish and subscribe to global application-level events:
someClass = Class.create();
someClass.prototype = {
initialize: function() {
this.name = "Foo";
//do some stuff...
//something happened that might be interesting at a global level
globalEvents.fireEvent( "someEvent", {name: this.name} );
}
};
anotherClass = Class.create();
anotherClass.prototype = {
initialize: function() {
//private methods
function someEventHappened(args) {
alert(args.name);
}
//this class is interested in knowing whenever "someEvent" happens
globalEvents.attachEventHandler( "someEvent", someEventHappened );
}
};
var testSub = new anotherClass();
var testPub = new someClass();
On that last line, when the someClass instance is created and it fires the global "someEvent" event, the anotherClass instance "hears" it and the registered handler is executed, resulting in "Foo" being alerted.
Some notes about event arguments (and javascript object notation)
Notice above that the 2nd argument to the .fireEvent() method allows you to pass arguments. The arguments are in the form of a plain old javascript object. For anyone that may be reading this that is still a little new to javascript, I just wanted to take a brief moment to explain this. The syntax I used above: "{name: this.name}", created what's known as an anonymous object. The object had 1 property "name" with the value of this.name ("Foo" in our case). The syntax I used is also frequently referred to as "object notation" or "javascript object notation". The term "javascript object notation" is also known as JSON, although usually people use that acronym when referring to using it as a data transfer format during Ajax operations (more on JSON can be found on
Douglas Crockford's json.org, and of course on
Wikipedia).
someClass's arguments object for the fired event could have been written in any of the following semantically identical ways:
//using js's Object primitive
var args = new Object();
args.name = this.name;
globalEvents.fireEvent( "someEvent", args );
//assigning to a variable using object notation
var args = {name: this.name};
globalEvents.fireEvent( "someEvent", args );
//creating an anonymous object inline (the original version)
globalEvents.fireEvent( "someEvent", {name: this.name} );
Subclassing EventPublisher for class-level event notification
NOTE: The code example below assumes that we are using the inheritance model from my previous post.
Ok, now we have a global event dispatcher, but what if our events are not really interesting at a global level? For any class that needs to fire instance level events, simply inherit from EventPublisher:
class1 = Class.create();
Object.inherit(class1, EventPublisher);
Object.extend(class1.prototype, {
initialize: function() {
//base class construction
this.base();
//instance methods
this.doSomething = function () {
//something happened...
this.fireEvent( "somethingHappened" );
};
}
});
var test = new class1();
//listen
test.attachEventHandler( "somethingHappened", function () { alert("IT HAPPENED!!!"); } );
//make the event fire
test.doSomething();
I hope you are getting the gist. Now any object, or any code for that matter, that can see the "test" instance can listen for its events to fire, and then take action. I will leave it at that for now, and explore some more complicated event pub/sub scenarios in future posts as I continue to discuss the implementation of classical underpinnings in the dynamic javascript language.
Summary
So at this point in the game, we're armed with both a robust inheritance model and a robust event model. These are two of the big items on the checklist for implementing a truly powerful application platform. One which is scalable, maintainable, is well suited to team development, and is fun to work with. Really, using the bits from this post, my last post, and of course prototype.js itself, you should have all you need to get started. The rest of the posts in this series will discuss the general approach, explore some of the merits of prototype.js, and build on what I've already shared. The end result is going to be a full scale, yet lightweight, flexible platform, which you will be able to use to create some really solid web applications that will live long happy lives.
-Ryan Gahl
I work as a professional software engineering consultant, specializing in web-based (inter/intra/extranet) applications, Ajax, and C#.
My services can be purchased by calling the following number: 1-262-951-6727
I am becoming an expert engineer.