Wednesday, July 11, 2007

Handling multiple events in Javascript

This is perhaps going to be my first initiative to write a technical post, the effort mainly driven by my long hours of pondering over a problem that I faced earlier today. It was then that I decided that it is definitely not worth having anybody spend their valuable time trying to figure this out by themselves.I know that my usual reader's would find this a bit out of place, but I hope that they wouldn't mind this....

Events are one of the most frequently used features in the world of Javascript. They provide you with the ability to capture many user events that can help in designing more intuitive and user friendly interaction with any application. Some of the events that we might want to capture could be keystorkes, mouse clicks, mouse movements, etc.. All that we need to do to achieve this is to register the event-handler with the browser. On occurance of the event, the browser will call the corresponding event handler. This event handler can contain any logic built into it depending on the event that triggered it. Okay.. that should give you the sufficient overview of what you can achieve by using events. So lets get going with an example.

The most common and traditional method of attaching events with elements in a web page is as shown below.

elt.onClick = handler();
elt.onFocus = handler();

whereas the newer methods of using events and handlers are as shown below

document.addEventListener('',event_handler,false);
document.removeEventListener('',event_handler,false);

function_handler()
{
//do something here....
}

where event could be a 'click', 'keypress'

The race for winning the web-war between the two giants Netscape and Microsoft has had its impact on their way of handling events too. The above method is the one used by Netscape to register events. Whereas Microsoft uses the format shown below.

element1.attachEvent('onClick',event_handler1)
element2.attachEvent('onClick',event_handler2)


To detach an listener is equally easy..,

element1.detachEvent('onClick',event_handler2)

If you are unsure of the browser then you might want to use something like this..

function addEvent (elt, eventType, handler, captureMode){
if(docuement.addEventListener){
elt.addEventListener(eventType,handler,captureMode)
}
else{
elt.attachEvent('on'+eventType,handler);
}
}

Okay.. If you had observed the above example carefully, you might be wondering how this would work with multiple elements being registered for the same event. That's where the borwsers are smart. They keep track of the various handlers registered for that particular event, and they notify them all. But hey.., hold on, it ain't so simple.. you never know in which order the events are notified to the handlers. IN earlier versions it was not even possible to determine the order in which the handlers will get triggered. This can cause a lot of confusion when we have multiple handlers with the same target.

Now that we are familiar with the basics of handling events let get into the intricacies. Say for example that we have two elements e1 and e2 where e2 is deeper in the hierarchy. Both these are registered for the same event say 'keyDown'. Now when the event transpires, what order do the listeners follow in calling the corresponding handlers. Again, the titans opted for varying solutions and w3c ended up on a median. Nestcape said that the event should proceed from e1 to e2 (called as event Capturing), whereas Microsoft went for the opposite. Event proceeds from e2 to e1 which is called as 'Bubbling'. This is what we specified as the fourth parameter 'captureMode' in our addEvent() function earlier. Now w3c had to take a middle course by adoption both these models. For this purpose, we have a capturing phase and a bubbling phase.

e1.addEventListener('keyDown',handler1,false)
e2.addEventListener('keyDown',handler2,false)


where 'false' indicates a bubbling mode.

When the event on e2 is triggered, it starts off in the capturing mode where in it checks if the ancestor has any listeners in the capturing mode. Finding none, it will proceed to call handler2 and the bubbling mode starts off then. During the bubbling mode it will accordingly call the handler1 and traverses further till the top of the DOM is reached. Now consider the other case,

e1.addEventListener('keyDown',handler1,true)
e2.addEventListener('keyDown',handler2,false)


As usual it begins in the capturing mode on e2, and finding that the ancestor of e2 ie., e1 is in the capturing mode executes the handler1 before moving on to the handler2. Now if there were an element e0 which is the ancestor of e1 in the bubbling mode with handler0 then hanler0() would be the last to be called in the bubbling phase. Thus providing a feature to attach each listener with their own mode, it provides micro-control the order in which handlers should be called.

Now the next natural question is can we stop this bubbling ? The answer is yes..imagine a scenario where e2's handler should determine whether e1 should receive the event at all. We can stop the event from bubbling by event.cancelBubble = true which would prevent the next handler in the hierarchy from receiving the event. This could prove to be useful feature when the DOM is rich and you would want to stop the event from propagating up to the top of the hierarchy when there are no events attached to any of them. Please note that this is the method used by Microsoft. w3c suggests another implementation to do exactly the same

e1.stopPropagation();

so what do we do to ensure cross browser compatibility..? Its the plain old way of doing it..

function stopEvents(event){

if(event.stopPropagation){
event.stopPropagation();
}
else {
event.cancelBubble;
}
}


So are there any pitfalls here..? yes .. A couple of things that I can think of. Please do correct me if I am wrong.

There is no way of determining whether event handlers are registered to an element (in older implementations elt.onclick() would give you the handlers registered with it). So if we want to ensure that there are no other eventhandlers to an element we should probably use elt.removeEventListener(). Another issue that I faced was that the propagation in the capturing phase cannot be stopped. The only way to control could be in the bubbling phase, so one should be using the mode pretty carefully depending on the design. One other problem that I came across was something like this...

e1.addEventListener('click',handle1,true)
e2.addEventListener('click',handle1,true)


So now I had to determine what was the originator of the event. The solution was pretty simple, use the 'this' operator to determine the source. Recently I also realised that w3c had another variable dedicated for the purpose called 'currentTarget' which contains the reference of the element. But then I found this to work only with MOZ and not with IE. Perhaps IE doesnt have a 'currentTarget' implementation. However make a note that the 'srcElement' or 'target' would still be pointing to the initial element where the event originated.

And now if you might have any doubts, suggestions or clarifications, please post them as comments and I would be more than pleased to have a look at it.

6 comments:

Pete said...

Thanks for the examples provided for cross browser compatibility.

- Pete

Simon said...

Hi Titan,

Is it possible for us to prevent the event from being passed over to a higher element ? thanks in advance..

Simon

Mighty Titan said...

Hi Simon,

I dont think it is possible for us to prevent this from happening, but then as i mentioned we can cut off the event by removeEvent() as I had mentioned in my post. I shall let you know in case I happen to find any way of doing this.

Thanks,
Titan

Cary said...

Well written article.

Anonymous said...

Here is how you find the parent of an event in both browsers

if(window.addEventListener){
eventParent = e['target'].parentNode;
} else { // IE
eventParent = e['srcElement'].parentNode;
}

Mighty Titan said...

Thanks Cary!

@Anonynmous..

Thanks for the scriptlet. Will definitely try it out.

~Titan.