A Good Enough addEvent

Several years ago, PPK of Quirksmode sponsored a contest to come up with a new version of the trusty JavaScript addEvent function. The original addEvent was created by Scott Andrew LePera in 2001 as a way to merge Internet Explorer’s attachEvent with the W3C’s addEventListener. Both addEventListener and attachEvent allow you to attach a JavaScript event to a DOM object, but they differ in important ways. In particular, IE’s attachEvent doesn’t maintain the scope of the this keyword: this refers to the window object instead of the object on which you’re attaching the event, as in the case of addEventListener.

PPK’s contest itself ended up falling flat, as even the winner, John Resig (who later created the jQuery library), later repudiated it himself. That’s probably because PPK’s contest requirements were like asking for all three of good, fast, and cheap.

So seven years later, there’s no widely-adopted replacement to the original addEvent that:

  1. Is short
  2. Maintains the this scope in IE
  3. Has a corresponding removeEvent

The various libraries do a good job of 2 and 3, but not 1, and since I often find myself needing 1 and 2 but not 3, I came up with my own good-enough version of addEvent:

var addEvent = function( obj, type, fn ) {
        if (obj.addEventListener)
                obj.addEventListener(type, fn, false);
        else if (obj.attachEvent)
                obj.attachEvent('on' + type, function() { return fn.apply(obj, new Array(window.event));});
}
 

It’s short, which is just what I need when I’m trying to keep the JavaScript size low. (Whenever size isn’t so much of an issue, such as on the administrative side of a website, I’m more likely to use a library which will have a much more robust way of assigning events to objects.) And my addEvent also makes this refer to object in question, even for IE.

10 Comments

  1. The slightly shorter fn.call(obj, window.event) should suffice.

  2. One other point that this solution does not fix, since it is still a wrapper around MSIE’s propriety (and sub-optimal) eventmodel, is the fact that addEventListener will ignore it when you try to add the same handler to the same event on the same object. attachEvent doesn’t.

    This solution does suffice for most cases though, but then Scott Andrews original addEvent also worked for most apart from the scope-issue.

    In some cases however correctness is more important than brevity, and if you already include a library that’s several kilobytes in size than I prefer a solution that’s more correct and flexible than one that is only short and may backfire on me.

    And all things considered you can count yourself lucky if you don’t have to support browsers that support neither the W3C DOM2 eventmodel nor MSIE’s eventmodel ;)

  3. @molily: I don’t know why I didn’t think of that.

    @Tino Zijdel: It depends on what you mean by a library of “several” kilobytes. Most of the time I just want to run some code when the window has loaded or attach a click event, and then it’s “good enough”–no need to add kilobytes.

  4. What I meant is: even when you create websites enhanced with JS behaviour but don’t use any of the big frameworks/libraries such as jQuery or prototype you’ll usually have some sort of script with utilities (a ‘toolbox’ of some sort) that you often use and that contains more than a generic function for adding events. My ‘toolbox’ contains f.i. a light-weight Ajax class, class-dealing functions, cross-browser Array extra’s/generics etcetera.

    If you only want to add an eventhandler to some object why use this solution instead of just doing object.onclick=handler – that also ‘fixes’ the ‘this’-issue and by your own reasoning: how often do you find yourself needing to add multiple handlers to the same event on the same object? ;)

    If you insist on a generic function for that, here it is:

    var addEvent = function( obj, type, fn ) {
    obj['on'+type] = fn;
    }

  5. I guess I have another, tacit criterion: I like to know that my script won’t interfere with or be interfered by other scripts.
    If you do something like the following

    window.onload = function1;
    window.onload = function2;

    function2 will override function1, but

    addEvent(window, 'load', function1 );
    addEvent(window, 'load', function2 );

    works fine.

  6. This is good and makes perfect sense. I also came up with a very short and concise addEvent in my article to write better JavaScript. It even does a simple branch so you’re not always detecting the object in question (W3 vs IE).
    All in all, you make a good case. And I too rarely ever use removeEvent… it seems like a waste of time for smaller websites :)

  7. I think it’s a bad sign if you don’t know what other scripts are doing on your page (they just as well might overwrite your addEvent function!) ;)

    Besides that I personally never use window.onload; I have a DomLoaded class (in my toolbox) and register onload functions to that using DomLoaded.addLoadEvent()

  8. I think it’s a bad sign if you don’t know what other scripts are doing on your page

    Try working on a site like where multiple departments with multiple developers own discrete sections of a large website. It’s rare that one developer “owns” the window.onload event.

    Being able to overload event handlers safely is far more valuable than the ability to detach handler functions. If you’re attaching so many handlers that you have to detach them to prevent leakage, I’d say that’s a poor design to begin with.

  9. It’s rare that one developer “owns” the window.onload event.

    No-one should ‘own’ that event in such circumstances; you’ll need global conventions such as ‘onload events should be registered using this-and-such interface’.

    I was just trying to illustrate that even though it is good that you try to play nice, other scripts might not and can screw up yours (or each other) anyway.

    My more general ‘criticism’ is about the ‘good enough’ part of this article: I argued that for most people your original addEvent wrapper might still be ‘good enough’ and even just using object.onevent=function is ‘good enough’ in most cases. It just all depends on what you think is important and on what you actually need. You just need to be aware of the limitations of the solution you implement, and in my humble opinion most people are just not knowledgeable enough for that.

    In general I think that ‘brevity’ should not be such a heavy factor when evaluating cross-browser eventhandling solutions, especially not if you already include several KB of script for other generic purposes.

  10. I’ve actually been working on this “problem” of getting a good add/remove event toolset (not framework based).

    Mine is admittedly a bit longer in terms of code and less “graceful” looking, but I think it’s got coverage for a lot more of the “problems” that people complain about with event handler adding/removing.

    The strategy I used to be able to chain any number of handlers for an event, even duplicate ones, and have the ordering and remove-ability preserved, was using a function closure chain.

    Here’s the claims that it makes:
    1. Preserves the “this” in handlers to refer to the object the event was fired on.
    2. Works on pretty much any browser/version (and doesn’t require any browser sniffing/detection to do so, since it falls back to the old-school onXXX style)
    3. Allows mutliple chained event handlers (even duplicate handlers), which execute in a predictable FIFO order, regardless of browser. But duplicate handlers are removed in LIFO order, as I think would be expected.
    4. does use expando properties (and closures) on the target object, but manages them in such a way that if an author properly calls unbindEvent() for their events when the page unloads, things should not leak (IE).

    Here’s also a working example of the code: http://www.getify.com/test-events.html.

    And here’s the code:

    var UNDEF = "undefined", JSFUNC = "function";

    function bindEvent(obj,eventName,handlerFunc) {
    eventName = eventName.toLowerCase();
    if (typeof obj[eventName] === JSFUNC) {
    (function(ename) { // recursive function to move the chain of signatures down one link to make room for the new handler
    if (typeof obj[ename+"_"] === JSFUNC) { // *next* item already a chain
    arguments.callee(ename+"_"); // recurse to keep shifting the chain links down
    }
    obj[ename+"_"] = obj[ename];
    })(eventName+"_");

    obj[eventName+"_"] = handlerFunc; // store handler signature for removal purposes
    var prev_chain = obj[eventName];
    obj[eventName] = function() { // insert new link into the chain
    prev_chain(); // 'recursively' (through closure wrap-up) call previous link/chain -- FIFO execution
    handlerFunc(); // call new link
    };
    }
    else { // just old-school register handler, and store its signature to create the first link of the chain
    obj[eventName+"_"] = (obj[eventName] = handlerFunc);
    }
    }

    function unbindEvent(obj,eventName,handlerFunc) {
    eventName = eventName.toLowerCase();
    if (typeof obj[eventName] === JSFUNC) {
    var last_ename = null, ename = eventName+"_"; // start looking for signatures at first "_" level hung off the end of the event name
    while (typeof obj[ename]!==UNDEF && obj[ename]!==null) { //loop through all signatures to find the target handler to remove
    if (obj[ename] === handlerFunc) { // target handler found, so remove its signature from the chain
    while (typeof obj[ename+"_"] === JSFUNC) { // keep going as long as the next higher link in the signature chain is defined
    obj[ename] = obj[ename+"_"]; // move the next higher link down one level to replace the existing link
    last_ename = ename; // used to keep track of which level/signature-name is the new highest one in the chain
    ename += "_"; // move up one link level in the signature chain
    }
    obj[ename] = null; // the last link of the chain needs to be null'd
    try { delete obj[ename]; } catch (err) { } // and deleted

    if (last_ename !== null) { // if any chain of signature links still exists, it needs to be re-wrapped into a ball of closure function calls
    var chain = obj[last_ename]; // start by capturing the function signature from the last link of the signature chain
    ename = last_ename.replace(/_$/,""); // start at the next to last element of the signature chain
    while (ename!==eventName) { // work backwards, rolling up the closures level by level, until you reach the beginning of the chain
    (function(){ // needs a "local scope block" for proper closure
    var prev_chain = chain, handlerFunc = obj[ename];
    chain = function() { // re-create the closure wrap-up function representing this link of the signature chain, save for next while-iteration
    prev_chain(); // FIFO execution
    handlerFunc();
    };
    })();
    ename = ename.replace(/_$/,""); // move back down one level of the signature chain
    }
    obj[ename] = chain; // entire closure chain re-created, so attach it to the main event handler property so it'll be called directly on event
    }
    break;
    }
    else { // target handler not found yet, step up one level of the signature chain
    last_ename = ename;
    ename += "_";
    }
    }
    if (typeof obj[eventName+"_"] === UNDEF || obj[eventName+"_"] === null) { // clean up -- if the signature chain is now empty (all events removed), remove the main event handler too.
    obj[eventName] = null;
    try { delete obj[eventName]; } catch (err) { }
    }
    }
    }

Post a Comment

Your email is never shared. Required fields are marked *

*
*

4 Trackbacks