Custom JavaScript Event Listeners
April 10, 2007
I’ve Moved!
In order to demonstrate my code better I needed the ability to execute JavaScript on my pages and I wanted a simpler solution to offering source files than what Box.net offered.
I’m now over at http://blog.josh-davis.org.
You can get to this post on my new site at http://blog.josh-davis.org/2007/04/10/custom-event-listeners/
Original Post
Introduction
One of the core objects used in all my current development, the object almost all higher level objects rely on to interact with each other, is my custom event listener. Custom event listeners allow me broadcast and capture events across JavaScript objects. An example of a custom event is if I have a JS menu objects for which I want to detect an ‘open’ event from another JS object. I could detect the mouse click and assume the menu is open, but what if the menu object has to do determine whether it’s allowed to open or what if the menu is opened programmatically triggered by some other event? Obviously you can’t always make assumptions based on built-in event, but instead have to handle object defined events in a robust manner.
Why a Custom Event Handler?
Why did I create a custom event handler rather than use the browsers built-in event handling system (built-in listeners are those such as mouseout, mouseover, click, etc. that are the result of a user’s actions or a browser event)? Well the primary reason was that at the time I developed this IE 6 had no way to create user defined event handlers; Mozilla based browsers did have the capability, but it was easier to treat all browser the same way. I’m not sure if IE 7 still can’t handle user defined events, but at this point I like the way mine works so I haven’t taken any time to research it.
Bind
One of the keys to the custom event handler (and actually built-in event handling in object oriented JS programming) is the ability for the events to fire within the appropriate scope, the Function.prototype.bind function. If an event simply called the method of an object the method would execute as its own function without any understanding of its parent object. However, by binding the method to its parent it is able to see the object’s properties and sibling methods. The bind function is pretty straight-forward as all it does is “apply” the parent object to the method and pass along the arguments from the function call.
Function.prototype.bind = function (object)
{
var method = this;
return function ()
{
return method.apply(object, arguments);
};
};
Event
The base Event object is pretty basic. It’s just a constructor declaring two empty arrays. The this.events array will be used to hold custom events while the this.builtinEvts array will be used to hold built-in events.
function Event()
{
this.events = [];
this.builtinEvts = [];
}
getActionIdx
In order to determine whether a listener has been registered already or not, and to have the ability to remove listeners, there needs to be a method to retrieve the index of the event. In order to do that I need to work my way down through the tree I’ve created to store the event for each object. The tree looks like this:
this.event[]
|
object[]
|
event[]
|
listener{}
|
----------------
| |
action binding
The index returned will be that of the listener within the final event array. If the object, event, or listener is not defined then getActionIdx will return -1. I could have defined listener as another object with two properties, action and binding, but decided against it. In the future if I determine the listener needs to be more complex I may do so to make the code more readable, but for now it suits my needs.
Event.prototype.getActionIdx = function(obj,evt,action,binding)
{
if(obj && evt)
{ var curel = this.events[obj][evt];
if(curel)
{
var len = curel.length;
for(var i = len-1;i >= 0;i--)
{
if(curel[i].action == action && curel[i].binding == binding)
{
return i;
}
}
}
else
{
return -1;
}
}
return -1;
};
addListener
Now to the meat of the Event object.
The addListener method will add a new listener to an object for a given event. Because odd things can happen if a listener gets registered multiple times it needs to use getActionIdx to determine whether the listener already exists or not. If the object hasn’t had any listeners registered then a new array will need to be created for the given object. If the specified event hasn’t been registered under the object then a new array will need to be created as well. Finally, if the specified action and binding haven’t been registered under the event then the listener will be added.
Event.prototype.addListener = function(obj,evt,action,binding)
{
if(this.events[obj])
{
if(this.events[obj][evt])
{
if(this.getActionIdx(obj,evt,action,binding) == -1)
{
var curevt = this.events[obj][evt];
curevt[curevt.length] = {action:action,binding:binding};
}
}
else
{
this.events[obj][evt] = [];
this.events[obj][evt][0] = {action:action,binding:binding};
}
}
else
{
this.events[obj] = [];
this.events[obj][evt] = [];
this.events[obj][evt][0] = {action:action,binding:binding};
}
};
removeListener
Removing a listener is as simple as getting its index and splicing it out.
Event.prototype.removeListener = function(obj,evt,action,binding)
{
if(this.events[obj])
{
if(this.events[obj][evt])
{
var idx = this.actionExists(obj,evt,action,binding);
if(idx >= 0)
{
this.events[obj][evt].splice(idx,1);
}
}
}
};
fireEvent
Adding and removing listeners is all well and good, but what’s the point if you can’t use it for anything. What fireEvent does is make it all work. When a JS object performs an action that you want to broadcast and then catch within another object you use fireEvent to broadcast that event to all registered listeners of that object. It does this by looking for the object, then the event within that object, and finally executing all actions registered for that event. The reason I pass through “e” is because many of my events do occur as a result of a built-in listener and may need to do something with the event object (like get the mouse x/y).
Event.prototype.fireEvent = function(e,obj,evt,args)
{
if(!e){e = window.event;} if(obj && this.events)
{
var evtel = this.events[obj];
if(evtel)
{
var curel = evtel[evt];
if(curel)
{
for(var act in curel)
{
var action = curel[act].action;
if(curel[act].binding)
{
action = action.bind(curel[act].binding);
}
action(e,args);
}
}
}
}
};
Built-in Listeners
The Event object I use also includes built-in listeners, but I removed the functionality from this example to shorten the article; I will add built-in listeners back into the Event object in a later article. I originally dealt with built-in listeners completely outside of my custom event handler because of the browsers native capability to handle them. However, two things changed my mind.
The first thing that changed my mind was that I ended up having to keep a global array of built-in listeners so I could clean up after them properly to prevent memory leaks within browsers. It made more sense to clean up both built-in and custom listeners at the same time. I’ve also removed the cleanup to shorten and simplify the article, but again I’ll follow up on it another time and explain why it’s even a necessity.
The second thing that changed my mind was that I was using custom and built-in listeners in very similar ways. In the end it was just easier to remember two similar method calls within a single Event object.
An Example
var gEVENT = new Event();function TestObject()
{
//this is a test object
}
TestObject.prototype.initialize = function()
{
gEVENT.fireEvent(null,this,'initialize');
};
var test = new TestObject();
gEVENT.addListener(test,'initialize',function(){alert('test initialized');});
If at any point test.initialize() is called then it will fire an “initialize” event which in turn calls the anonymous function to open an alert box. The example is over simplified to make it easy to understand, but you could easily replace the anonymous function with one from another JS object (just don’t forget to bind it to its parent object using the bind argument in addListener).
Conclusion
Hopefully, this makes sense and will be helpful to some. Being my first “how to” article I’m sure there are a lot of improvements I can make and I hope you’ll offer suggestions on how to improve my writing style. I also know there are improvements I can make to my code too. Please, please, please tell me how I can improve. If I wanted to just have a very good custom event handler to use in my future projects I would use one of the many frameworks out there, but my goal is to improve my programming skills. I learn a great deal by going through the exercise of trying to solve the problem myself. I learn even more when my work is critiqued and I’m told how I can improve.
And, finally, here’s the Event code all in one piece:
/**
* Binds a function to the given object's scope
*
* @param {Object} object The object to bind the function to.
* @return {Function} Returns the function bound to the object's scope.
*/
Function.prototype.bind = function (object)
{
var method = this;
return function ()
{
return method.apply(object, arguments);
};
};/**
* Create a new instance of Event.
*
* @classDescription This class creates a new Event.
* @return {Object} Returns a new Event object.
* @constructor
*/
function Event()
{
this.events = [];
this.builtinEvts = [];
}
/**
* Gets the index of the given action for the element
*
* @memberOf Event
* @param {Object} obj The element attached to the action.
* @param {String} evt The name of the event.
* @param {Function} action The action to execute upon the event firing.
* @param {Object} binding The object to scope the action to.
* @return {Number} Returns an integer.
*/
Event.prototype.getActionIdx = function(obj,evt,action,binding)
{
if(obj && evt)
{
var curel = this.events[obj][evt];
if(curel)
{
var len = curel.length;
for(var i = len-1;i >= 0;i--)
{
if(curel[i].action == action && curel[i].binding == binding)
{
return i;
}
}
}
else
{
return -1;
}
}
return -1;
};
/**
* Adds a listener
*
* @memberOf Event
* @param {Object} obj The element attached to the action.
* @param {String} evt The name of the event.
* @param {Function} action The action to execute upon the event firing.
* @param {Object} binding The object to scope the action to.
* @return {null} Returns null.
*/
Event.prototype.addListener = function(obj,evt,action,binding)
{
if(this.events[obj])
{
if(this.events[obj][evt])
{
if(this.getActionIdx(obj,evt,action,binding) == -1)
{
var curevt = this.events[obj][evt];
curevt[curevt.length] = {action:action,binding:binding};
}
}
else
{
this.events[obj][evt] = [];
this.events[obj][evt][0] = {action:action,binding:binding};
}
}
else
{
this.events[obj] = [];
this.events[obj][evt] = [];
this.events[obj][evt][0] = {action:action,binding:binding};
}
};
/**
* Removes a listener
*
* @memberOf Event
* @param {Object} obj The element attached to the action.
* @param {String} evt The name of the event.
* @param {Function} action The action to execute upon the event firing.
* @param {Object} binding The object to scope the action to.
* @return {null} Returns null.
*/
Event.prototype.removeListener = function(obj,evt,action,binding)
{
if(this.events[obj])
{
if(this.events[obj][evt])
{
var idx = this.actionExists(obj,evt,action,binding);
if(idx >= 0)
{
this.events[obj][evt].splice(idx,1);
}
}
}
};
/**
* Fires an event
*
* @memberOf Event
* @param e [(event)] A builtin event passthrough
* @param {Object} obj The element attached to the action.
* @param {String} evt The name of the event.
* @param {Object} args The argument attached to the event.
* @return {null} Returns null.
*/
Event.prototype.fireEvent = function(e,obj,evt,args)
{
if(!e){e = window.event;}
if(obj && this.events)
{
var evtel = this.events[obj];
if(evtel)
{
var curel = evtel[evt];
if(curel)
{
for(var act in curel)
{
var action = curel[act].action;
if(curel[act].binding)
{
action = action.bind(curel[act].binding);
}
action(e,args);
}
}
}
}
};
EDIT: April 27, 2007
I found out about the Box.net widget today so I’m now able to link to source files. You can now download the files related to this article and test them out:
October 17, 2007 at 12:21 pm
[...] independently observe idle/active state of the user and act accordingly. There are quite few very, good, explanations of custom events, just in case you’re not sure what they are, as well as [...]
November 29, 2007 at 4:01 am
Hi.
Good design, who make it?
December 17, 2007 at 5:30 pm
Very nice and usefull. I found one little bug. In the removeListener function you call the function ‘actionExists’ but that function does not exist. After replacing it with ‘getActionIdx’ it worked like a charm!
Grz. Sil
April 10, 2008 at 4:02 pm
Are there any comprehensive event handler scripts that are fully cross-browser capable which can hook all events and translate to a standard structure? IE5/NN-current and including future looking standards… I find absolutely no information on the intermediate variations with various browser versions, nor do I find sufficient information on trapping the lowest level event handler across all browser environments. I will create this model and present public domain if none other exists, and would appreciate all insight or existing profile models of the systems as they exist. WilfredGuerin@Gmail.com please.