Saturday, January 15, 2005

Multiple Event Handlers in JavaScript (daisy-chaining event handlers)

 

Problem

 

A distinct drawback of the traditional JavaScript event handling model is that the onclick property can contain only one function. This becomes a problem when you want to register multiple event handlers for one event.

 

For instance, suppose you’ve written a module that makes it possible to drag and drop a layer. The module registers an onclick event handler to an element so that clicking on it starts the drag and drop. You have also written a module that silently keep track of user clicks and sends this information to the server onunload, so you can find out how your pages are used. This module, too, registers an onclick event handler to an element.

So what you’d really like to do is

 

element.onclick = startDragDrop;
element.onclick = spyOnUser;

 

However, it’s here that things start to go wrong. The second registration of a function to onclick overwrites the first one so that only spyOnUser() is executed when the user clicks on the element.

Solution

Register an anonymous function that executes both other functions:

element.onclick = function () {startDragDrop(); spyOnUser()}

But suppose that you don’t use both modules on every page in your site. Now if you’d do

 element.onclick = function () {startDragDrop(); spyOnUser()}

 you could get error messages because one of the two functions might be undefined. So you have to be more careful in registering your event handlers. When we want to register spyOnUser() while startDragDrop() may (or may not) be registered, we do:

var old = (element.onclick) ? element.onclick : function () {};
element.onclick = function () {old(); spyOnUser()};

First you define a variable old. If the element currently has an onclick event handler, put this event handler in old, if it hasn’t, put an empty function in old. Now you register a new event handler to element div. It is a function that first executes old() and afterwards spyOnUser().
Now the new event handler is added to the element, while previously registered handlers (if any) are preserved.

 Andrew's Addendum

The code above works great if you only need to replace the event  handler of one element. If you are dealing with multiple elements in a loop though, you have a new problem. This code:

for (i = 0; i < textboxes.length; i++) {
     var old = (textboxes[i].onclick) ? textboxes[i].onclick : function () {};
    element.onclick = function () {old(); spyOnUser()};
}

does not work because when onclick is finally executed old() actually has the last value that was assigned to it (in this case the original event handler of the last textbox in the list). In other words, all onclick events will execute the same old() function, and not the one that was originally assigned to each textbox.
 
The solution is to construct an array of event handlers (which of course has to be a global variable):
 
var eventHandlers = new Aarray();


...  
     
           eventHandlers.length = textboxes.length;
for (i = 0; i < textboxes.length; i++)
{
    eventHandlers[i] = (textboxes[i].onchange) ? textboxes[i].onchange : function() {};
    eval("textboxes[i].onchange = function() {eventHandlers[" + i + "](); spyOnUser();}");
}

 

3 Comments:

Blogger Andrew said...

As noted by Jignesh, there is a different way to accomplish the same thing in IE:

http://www.quirksmode.org/js/events_advanced.html

However, this is not cross-browser compatible.

9:56 AM  
Anonymous Anonymous said...

How about a function to do it transparently and cross-browser:

function addEvent(obj, event, func) {
var cur = obj['on'+event];
if (cur != null) obj['on'+event] = function () {cur(); func()};
else obj['on'+event] = func;
}

addEvent(window, 'load', startDragDrop);
addEvent(window, 'load', spyOnUser);

6:37 AM  
Blogger Andrew said...

There are other browsers besides IE? :-)

Great solution, thanks!

10:10 AM  

Post a Comment

Subscribe to Post Comments [Atom]

<< Home