Our Backwards DOM Event Libraries

The Browser APIs

A brief review of what the browsers give us to attach event listeners to DOM elements so we can do fancy stuff when the user interacts with the page.

Internet Explorer gives us element.attachEvent allowing us to attach a listener function to an element.

document.body.attachEvent(
    'onclick',
    function() {
        alert('body clicked');
    });

The other browsers give us element.addEventListener. The use with which we are most familiar is supplying a listener function as the second argument.

document.body.addEventListener(
    'click',
    function() {
        alert('body clicked');
    },
    false);

Many JavaScript programmers don’t know that it is also possible to send a listener object as the second argument to addEventListener. When an event is fired, the object’s handleEvent method is called.

document.body.addEventListener(
    'click',
    {
        handleEvent: function() {
            alert('body clicked');
        }
    },
    false);

An important feature of using a listener object is that the function value of it’s handleEvent property is only looked up when the event is fired. This means that if the function value of the handleEvent property changes between events then it is always the current value of the handleEvent property that is called. This is late binding. For example,

var obj = {};

document.body.addEventListener('click', obj, false);

// click body will error in some browsers because
// no handleEvent method on obj

obj.handleEvent = function() {alert('alpha');};

// click body and see alert "alpha"

obj.handleEvent = function() {alert('beta');};

// click body and see alert "beta"

document.body.removeEventListener('click', obj, false);

// click body and see nothing

Cross-Browser Libraries

Our cross-browser applications should not be cluttered with repetitive code to work with the different APIs provided by the different browsers, so we’ve abstracted the different APIs away in event libraries. This has been a very good choice.

Different libraries have different APIs but every library has something like the following.

LIB_addEventListener(
    document.body, 
    'click', 
    function() {
        alert('body clicked');
    });

The JavaScript this Wrinkle

A common problem with these library abstractions is when we want a method of an view object to be called when the event is fired. For example, in the following code, the value of this in the handleClick method is the global window object. The alert will show undefined when we may have expected to see the alert show "alpha".

function ViewObject() {
    this.data = 'alpha';
    LIB_addEventListener(
        document.body, 
        'click', 
        this.handleClick);
}
ViewObject.prototype.handleClick = function() {
    alert(this.data);
};

The workaround the libraries have given us (so that we can also still easily remove event listeners) is to specify the object we want as this when the handler is called as an extra argument to the event library function. For example, the alert in this example will show "alpha".

function ViewObject() {
    this.data = 'alpha';
    LIB_addEventListener(
        document.body, 
        'click', 
        this.handleClick,
        this);
}
ViewObject.prototype.handleClick = function() {
    alert(this.data);
};
ViewObject.prototype.destroy = function() {
    LIB_removeEventListener(
        document.body,
        'click',
        this.handleClick,
        this
    );
};

One problem with this API is that the listener function is bound when LIB_addEventListener is called. This causes trouble when the function value of handleClick is changed and then also when an attempt is made to remove the listener.

var vo = new ViewObject();

// click on the body and see alert "alpha"

vo.handleClick = function() {
    alert('beta');
};

// click on the body and still see "alpha"

vo.destroy();

// click on the body and still see "alpha"!

Another problem is that we are writing the program in an object-oriented style but we are focusing on listener functions rather than listener objects. This mismatch is a clue to find a better solution.

A Library API for Listener Objects

Since we are frequently writing our programs in an object-oriented style, it makes sense to write LIB_addEventListener so that it can accept listener objects (as well as listener functions.)

var obj = {
    handleEvent: function() {
        alert('click handler');
    }
};
LIB_addEventListener(document.body, 'click', obj);

Frequently we have one view object handling multiple types of events for various elements. A fourth parameter specifying the method name would allow that easily and still keep late binding.

var obj = {
    handleMouseDown: function() {
        alert('mouse down handler');
    },
    handleMouseUp: function() {
        alert('mouse up handler');
    }
};
LIB_addEventListener(document.body, 'mousedown', obj, 'handleMouseDown');
LIB_addEventListener(document.body, 'mouseup', obj, 'handleMouseUp');

This API still allows for listener functions by looking at the type of the third argument.

LIB_addEventListener(document.body, 'mousedown', function() {
    alert('mousedown');
});

But now there is no need for specifying the this object that will be used when the listener function is called because we have something better suited to the task: listener objects.

Comments

Have something to write? Comment on this article.

Patrick Mueller March 4, 2012

Wouldn’t it be cool if the DOM accepted an [object, functionName] array object, instead of just a function or handler object, for addEventListener()?

Have something to write? Comment on this article.