Published on Josh Davis (http://www.josh-davis.org)
Custom JavaScript Event Listeners

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.

  1. Function.prototype.bind = function (object)
  2. {
  3.         var method = this;
  4.         return function ()
  5.         {
  6.                 return method.apply(object, arguments);
  7.         };
  8. };

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.

  1. function Event()
  2. {
  3.         this.events = [];
  4.         this.builtinEvts = [];
  5. }

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.

  1. Event.prototype.getActionIdx = function(obj,evt,action,binding)
  2. {
  3.         if(obj && evt)
  4.         {
  5.                 var curel = this.events[obj][evt];
  6.                 if(curel)
  7.                 {
  8.                         var len = curel.length;
  9.                         for(var i = len-1;i >= 0;i--)
  10.                         {
  11.                                 if(curel[i].action == action && curel[i].binding == binding)
  12.                                 {
  13.                                         return i;
  14.                                 }
  15.                         }
  16.                 }
  17.                 else
  18.                 {
  19.                         return -1;
  20.                 }
  21.         }
  22.         return -1;
  23. };

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.

  1. Event.prototype.addListener = function(obj,evt,action,binding)
  2. {
  3.         if(this.events[obj])
  4.         {
  5.                 if(this.events[obj][evt])
  6.                 {
  7.                         if(this.getActionIdx(obj,evt,action,binding) == -1)
  8.                         {
  9.                                 var curevt = this.events[obj][evt];
  10.                                 curevt[curevt.length] = {action:action,binding:binding};
  11.                         }
  12.                 }
  13.                 else
  14.                 {
  15.                         this.events[obj][evt] = [];
  16.                         this.events[obj][evt][0] = {action:action,binding:binding};
  17.                 }
  18.         }
  19.         else
  20.         {
  21.                 this.events[obj] = [];
  22.                 this.events[obj][evt] = [];
  23.                 this.events[obj][evt][0] = {action:action,binding:binding};
  24.         }
  25. };

removeListener

Removing a listener is as simple as getting its index and splicing it out.

  1. Event.prototype.removeListener = function(obj,evt,action,binding)
  2. {
  3.         if(this.events[obj])
  4.         {
  5.                 if(this.events[obj][evt])
  6.                 {
  7.                         var idx = this.actionExists(obj,evt,action,binding);
  8.                         if(idx >= 0)
  9.                         {
  10.                                 this.events[obj][evt].splice(idx,1);
  11.                         }
  12.                 }
  13.         }
  14. };

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

  1. Event.prototype.fireEvent = function(e,obj,evt,args)
  2. {
  3.         if(!e){e = window.event;}       if(obj && this.events)
  4.         {
  5.                 var evtel = this.events[obj];
  6.                 if(evtel)
  7.                 {
  8.                         var curel = evtel[evt];
  9.                         if(curel)
  10.                         {
  11.                                 for(var act in curel)
  12.                                 {
  13.                                         var action = curel[act].action;
  14.                                         if(curel[act].binding)
  15.                                         {
  16.                                                 action = action.bind(curel[act].binding);
  17.                                         }
  18.                                         action(e,args);
  19.                                 }
  20.                         }
  21.                 }
  22.         }
  23.  
  24. };

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

  1. var gEVENT = new Event();function TestObject()
  2. {
  3.         //this is a test object
  4. }
  5.  
  6. TestObject.prototype.initialize = function()
  7. {
  8.         gEVENT.fireEvent(null,this,'initialize');
  9. };
  10.  
  11. var test = new TestObject();
  12.  
  13. 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:

  1. /**
  2.  * Binds a function to the given object's scope
  3.  *
  4.  * @param {Object} object The object to bind the function to.
  5.  * @return {Function}   Returns the function bound to the object's scope.
  6.  */
  7. Function.prototype.bind = function (object)
  8. {
  9.         var method = this;
  10.        
  11.         return function ()
  12.         {
  13.                 return method.apply(object, arguments);
  14.         };
  15. };
  16. /**
  17.  * Create a new instance of Event.
  18.  *
  19.  * @classDescription    This class creates a new Event.
  20.  * @return {Object}     Returns a new Event object.
  21.  * @constructor
  22.  */
  23. function Event()
  24. {
  25.         this.events = [];
  26.         this.builtinEvts = [];
  27. }
  28.  
  29. /**
  30.  * Gets the index of the given action for the element
  31.  *
  32.  * @memberOf Event
  33.  * @param {Object} obj The element attached to the action.
  34.  * @param {String} evt The name of the event.
  35.  * @param {Function} action The action to execute upon the event firing.
  36.  * @param {Object} binding The object to scope the action to.
  37.  * @return {Number} Returns an integer.
  38.  */
  39. Event.prototype.getActionIdx = function(obj,evt,action,binding)
  40. {
  41.         if(obj && evt)
  42.         {
  43.                 var curel = this.events[obj][evt];
  44.                 if(curel)
  45.                 {
  46.                         var len = curel.length;
  47.                         for(var i = len-1;i >= 0;i--)
  48.                         {
  49.                                 if(curel[i].action == action && curel[i].binding == binding)
  50.                                 {
  51.                                         return i;
  52.                                 }
  53.                         }
  54.                 }
  55.                 else
  56.                 {
  57.                         return -1;
  58.                 }
  59.         }
  60.         return -1;
  61. };
  62.  
  63. /**
  64.  * Adds a listener
  65.  *
  66.  * @memberOf Event
  67.  * @param {Object} obj The element attached to the action.
  68.  * @param {String} evt The name of the event.
  69.  * @param {Function} action The action to execute upon the event firing.
  70.  * @param {Object} binding The object to scope the action to.
  71.  * @return {null} Returns null.
  72.  */
  73. Event.prototype.addListener = function(obj,evt,action,binding)
  74. {
  75.         if(this.events[obj])
  76.         {
  77.                 if(this.events[obj][evt])
  78.                 {
  79.                         if(this.getActionIdx(obj,evt,action,binding) == -1)
  80.                         {
  81.                                 var curevt = this.events[obj][evt];
  82.                                 curevt[curevt.length] = {action:action,binding:binding};
  83.                         }
  84.                 }
  85.                 else
  86.                 {
  87.                         this.events[obj][evt] = [];
  88.                         this.events[obj][evt][0] = {action:action,binding:binding};
  89.                 }
  90.         }
  91.         else
  92.         {
  93.                 this.events[obj] = [];
  94.                 this.events[obj][evt] = [];
  95.                 this.events[obj][evt][0] = {action:action,binding:binding};
  96.         }
  97. };
  98.  
  99. /**
  100.  * Removes a listener
  101.  *
  102.  * @memberOf Event
  103.  * @param {Object} obj The element attached to the action.
  104.  * @param {String} evt The name of the event.
  105.  * @param {Function} action The action to execute upon the event firing.
  106.  * @param {Object} binding The object to scope the action to.
  107.  * @return {null} Returns null.
  108.  */
  109. Event.prototype.removeListener = function(obj,evt,action,binding)
  110. {
  111.         if(this.events[obj])
  112.         {
  113.                 if(this.events[obj][evt])
  114.                 {
  115.                         var idx = this.actionExists(obj,evt,action,binding);
  116.                        
  117.                         if(idx >= 0)
  118.                         {
  119.                                 this.events[obj][evt].splice(idx,1);
  120.                         }
  121.                 }
  122.         }
  123. };
  124.  
  125. /**
  126.  * Fires an event
  127.  *
  128.  * @memberOf Event
  129.  * @param e [(event)] A builtin event passthrough
  130.  * @param {Object} obj The element attached to the action.
  131.  * @param {String} evt The name of the event.
  132.  * @param {Object} args The argument attached to the event.
  133.  * @return {null} Returns null.
  134.  */
  135. Event.prototype.fireEvent = function(e,obj,evt,args)
  136. {
  137.         if(!e){e = window.event;}
  138.         if(obj && this.events)
  139.         {
  140.                 var evtel = this.events[obj];
  141.                
  142.                 if(evtel)
  143.                 {
  144.                         var curel = evtel[evt];
  145.                         if(curel)
  146.                         {
  147.                                 for(var act in curel)
  148.                                 {
  149.                                         var action = curel[act].action;
  150.                                        
  151.                                         if(curel[act].binding)
  152.                                         {
  153.                                                 action = action.bind(curel[act].binding);
  154.                                         }
  155.                                         action(e,args);
  156.                                 }
  157.                         }
  158.                 }
  159.         }
  160.  
  161. };

Example

Initialize Test [1]

Here are the source files:

custom-event-listeners.html [2]
custom-event-listeners.js [3]


Source URL: http://www.josh-davis.org/2007/04/10/custom-event-listeners

Links:
[1] http://www.josh-davis.org/2007/04/10/custom-event-listeners#testlink
[2] http://www.josh-davis.org/files/uploads/2007/05/custom-event-listeners.html
[3] http://www.josh-davis.org/files/uploads/2007/05/custom-event-listeners.js