3/31/2007

My javascript addEvent

In the end of 2005 several web programming gurus, Peter-Paul Koch (ppk), Dean Edward and Scott Andrew LePera host a "javascript recoding contest" in which they asked participants to submit their code for adding and removing events in javascript. A template page was designed to test the code. The winner was John Resig, whose original code is here. The final code was slightly modified version:

function addEvent( obj, type, fn )
{
if (obj.addEventListener)
obj.addEventListener( type, fn, false );
else if (obj.attachEvent)
{
obj["e"+type+fn] = fn;
obj[type+fn] = function() { obj["e"+type+fn]( window.event ); }
obj.attachEvent( "on"+type, obj[type+fn] );
}
}
function removeEvent( obj, type, fn )
{
if (obj.removeEventListener)
obj.removeEventListener( type, fn, false );
else if (obj.detachEvent)
{
obj.detachEvent( "on"+type, obj[type+fn] );
obj[type+fn] = null;
obj["e"+type+fn] = null;
}
}

I personally don't feel comfortable with these code. First of all, as one of the comments mentioned (#2 Posted by Tino Zijdel on 18 October 2005) that in cases of multiple-eventhandlers in IE, the eventhandler added last will be executed first:

... since this is for IE a wrapper around attachEvent, the execution order of the events is different ('random' according to Microsofts documentation, but LIFO in practice against FIFO using the W3C event model)

A simple test reveals this problem:

function init(){
var btn=document.createElement('input')
btn.value = 'Click me'
btn.type = 'button'
document.body.appendChild( btn )
addEvent( btn, "click", function(){alert("#1")} )
addEvent( btn, "click", function(){alert("#2")} )
}

Run the init() function as the onload eventhandler (<body onload="init()">) and load with browser. Firefox alerts "#1" then "#2". But IE alerts "#2" first then "#1", which is not what it should be.

Another part making me uncomfortable is that it uses the entire function content as the key name of a hash:

   obj["e"+type+fn] = fn;
obj[type+fn] = function() { obj["e"+type+fn]( window.event ); }

Although none obvioiusly problem is mentioned or discussed, it could result in extremely long and complex key name, which, IMHO, is not what a hash is supposed to be.

I therefore came up with my own version:

function addEvent( obj, type, fn ) 
{
obj.eventHooks = obj.eventHooks || {} // set .eventHooks for obj if not exists
var evs = obj.eventHooks[type] || [] // set .eventHooks[type] if not exists
obj.eventHooks[type] = evs.concat( fn.concat? fn:[fn]) // this allows for multiple fns added
obj['on'+type] = function (e) // like addEvent(obj,"click",[f1,f2])
{
for (var i=0;i<this.eventHooks[type].length;i++)
{
this.tmp = this.eventHooks[type][i] // attach to the obj such that the this
this.tmp(e) // inside the function points to the obj correctly
this.tmp = undefined
}
}
obj = null
}
function removeEvent( obj, type, fn )
{
if (obj.eventHooks && obj.eventHooks[type])
{
var evs = obj.eventHooks[type]
for (var i=0; i<evs.length;i++)
{
if (evs[i]==fn)
{
obj.eventHooks[type] = evs.slice(0,i).concat(evs.slice(i+1))
break
}
}
}
obj = null
}

Unlike most addEvent inventions, in which either addEventListener or attachEvent is used, the above code simply stores eventhandlers into a buffer that has the following structure:

  obj.eventHooks = { click    : [f1, f2]
, keyup : []
, mouseover: [f3]
....
}

For each event, say, click, a function is assigned to the .onclick. This function carries out the following steps:
  1. Check if obj.eventHooks exists. If not, create one
  2. Check if obj.eventHooks.click exists. If not, create one
  3. Loop through each functions in obj.eventHooks.click
  4. For each function, attaches it to obj as a temporary obj.tmp, then executes obj.tmp(e). This makes sure that any this keyword inside that function point to the obj correctly.
A test page, using the contest template, is here. I am sure that there will be some downside along with this code, but at least at this moment it seems to solve the problems came with the winning code.