$('someElement')

Thursday, March 29, 2007

EventPublisher: custom events a la pub/sub using prototype.js

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.

10 Comments:

  • Heh. Ryan, you seem to be doing the EXACT same things as me all over the place. I use a custom event management class almost exactly like this. I'll send it to you with the rest of my lazy-loading code tonight.

    -Eric H.

    By Blogger Eric Ryan Harrison, at 5:44 AM, March 30, 2007  

  • Sweet. Can't wait to share ideas Eric; sounds like you and I think alike.

    By Blogger Ryan Gahl, at 9:15 AM, March 30, 2007  

  • Just curious, why do you do :

    this.attachEventHandler = function(eventName, handler)


    instead of using EventPublisher.prototype?

    I have also done my own notifier class (much more simple than yours but does not do all what yours does)

    Seb

    By Anonymous Anonymous, at 2:30 PM, April 04, 2007  

  • @seb: I explain this in my previous post about inheritance (link via the sidebar). Tacking would should be instance level members onto the prototype object statically is wrong in my opinion. Read the previous post to know more (I explain near the bottom). If a method is to be an instance method, it should not be possible to access it EXCEPT from instances. If you tack methods on to the prototype, it could then be called by doing "myClass.prototype.someMethod" - which is NOT from an instance (obviously).

    By Blogger Ryan Gahl, at 2:58 PM, April 04, 2007  

  • typo: should be "Tacking *what* should be..."

    By Blogger Ryan Gahl, at 3:05 PM, April 04, 2007  

  • Hi,

    I've a problem with this.removeEventHandler.

    The reject function always returns false on the check, but when I do the following : obj.method.wrap == handler.wrap && obj.method.argumentNames == handler.argumentNames, it's always true (even if it's not)

    Do you have an idea on what the solution could be ?

    Thanks,
    E.

    By Anonymous Anonymous, at 6:43 AM, April 29, 2008  

  • Hi! I have found your code a while ago, and I did brought some modifications to it so the class suited more the need of our project. Well, they are not major changes, but I think it could still be interesting to some other coders out there. Here's the complete listing of the modified class. (Sorry for the formatting, this space is very narrow) :


    /**
    * Source : http://www.someelement.com/2007/03/eventpublisher-custom-events-la-pubsub.html
    */
    EventPublisher = Class.create();

    EventPublisher.DEFAULT_ASYNCH_TIMEOUT = 10; // default valeur for asynchronous event handlers

    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|Number} asynchFlag [optional] Defaults to false if omitted. Indicates whether to execute
    * {handler} asynchronously (true) or not (false).
    * If asynchFlag is a number, it specifies the number of default
    * milliseconds to wait before executing the handler
    * @param {Boolean} [optional] determine if multiple asynchronous calls to {handler} should be made. If (false),
    * then any pending asynchronous call waiting are cancelled, and replaced by a new call
    */
    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;
    if ( typeof asynchVar == 'numeric' && asynchVar < 10 ) asynchVar = 10;
    var multiAsynch = arguments.length > 3 ? arguments[3] : false;
    var handlerObj = {
    method: handler,
    asynch: asynchVar,
    multiEvents: multiAsynch
    };
    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
    * @param {Object} args [optional] Any object, will be passed into the handler function as the only argument
    * @param {Boolean|Numeric} [optional] a value indicating that the listener should be notified asynchronously
    * boolean : fires asynchronously after EventPublisher.DEFAULT_ASYNCH_TIMEOUT milliseconds
    * numeric : fires the handler after the specified number of milliseconds
    * @param {Boolean} [optional] specify if asynchronous event handlers should be fired again, even if pending event
    * are waiting for a delay.
    * true, multiple events will be fired within a delay,
    * false, any event waiting for a delay will be cancelled before being fired again
    */
    this.fireEvent = function(eventName) {
    var evtName = eventName + "_evt"; // appending _evt to event name to avoid collisions
    if ( allEvents[evtName] ) {
    var args = arguments; // sauvegarde des arguments....
    allEvents[evtName].each(function(listener) {
    try {
    var eventArgs = args.length > 1 ? args[1] : null;
    var delay = args.length > 2 ? args[2] : listener.asynch;
    var multiEvents = args.length > 3 ? args[3] : listener.multiEvents;
    if (listener.timedDelay && !multiEvents) {
    clearTimeout(listener.timedDelay);
    listener.timedDelay = null;
    }
    if (delay>0) {
    listener.timedDelay = setTimeout(function() {
    listener.timedDelay = null;
    listener.method(eventArgs);
    }, delay);
    } else {
    listener.method(eventArgs);
    }
    } catch (e) {
    alert("Error in " + (this.id ? this.id : '[unknown object]') + ".fireEvent():\n\n" +
    "Event name: " + eventName + "\n\n" +
    "Message: " + e.message + (e.lineNumber ? ' at line ' + e.lineNumber : ''));
    }
    });
    }
    };
    }
    };

    By Blogger Yanick, at 12:57 PM, August 08, 2008  

  • Really nice, exactly what I'm looking for. Thanks for putting this together, it seems really useful.

    By Blogger Mustafa, at 8:57 PM, September 04, 2008  

  • now i'm a little confused - since protoype 1.6 one has the abillity to fire and listen to custom events - would I still want to use this class or was this the way of doing it before 1.6?

    By Anonymous Anonymous, at 9:22 PM, November 21, 2008  

  • I really like and appreciate your work, thank you for sharing such a useful information about motivational management objectives, keep updating the information, hear i prefer some more information about Event Management Institute Delhi

    By Blogger Prince Jose, at 2:22 AM, April 17, 2020  

Post a Comment

<< Home