Multiple JS event handlers on single element

rhinds picture rhinds · Jun 27, 2013 · Viewed 72.3k times · Source

I am working with an existing web app, in the app there are a variety of submit buttons on different forms, some using regular http post, some defining an onClick function, and some binding a js event handler to the button using a class on the element.

What I want to do, is bind another event handler to these buttons by just adding a class to the buttons, but what I want to determine is will the new event handler be guaranteed to be executed, or could one of the form submit actions happen before it does meaning my new function isn't hit.

The example scenario is I want to add a class to these buttons that bimds them all to a common js function that simply logs usage to some api. Is there a risk that the logging function isn't called because the form submit has navigated away from the page?

I've not done loads of js development, and I could test this 100 times over and just get lucky with it firing.


Below is some code I have tested with for one of the examples - again, I'm not asking how to bind multiple events, the question is to about my understanding of the spec and whether execution of all handlers is guaranteed.

$(document).ready(function(){
    $('.testingBtn').click(function() {
        window.location.replace("http://stackoverflow.com");
    });
    $( ".testingBtn" ).click(function(){
        alert('submitting!');
    });
});


<input class="testingBtn" type="submit" id="submitform" value="Complete Signup" />

As seen above, I can bind the multiple events, and in this example, just directed to another url, but this could be a form.submit() etc. In my testing the alert has always fired first, but am I just getting lucky with the race conditions?

Answer

Elias Van Ootegem picture Elias Van Ootegem · Jun 27, 2013

In JS, you don't really have control over what order the event handlers are called, but with careful delegation and well-placed listeners, it is possible.

Delegation is one of the most powerful features of the event model. As you may or may not know: in JS, an event is handed to the top of the dom, from where it propagates down to the element onto which the event should be applied. It stands to reason, therefore, that an event listener attached to the global object will call its handler prior to a listener that has been attached to the element itself.

window.addEventListener('click',function(e)
{
    e = e || window.event;
    var target = e.target || e.srcElement;
    console.log('window noticed you clicked something');
    console.log(target);//<-- this is the element that was clicked
}, false);//<-- we'll get to the false in a minute

It's important to note we actually have access to the event object in the handlers. In this case, we left the event object untouched, so it'll just continue to propagate down to the target, on its way down, it might meet with something like this:

document.getElementById('container').addEventListener('click', function(e)
{
    e = e || window.event;
    var target = e.target || e.srcElement;
    if (target.tagName.toLowerCase() !== 'a' || target.className.match(/\bclickable\b/))
    {
        return e;//<return the event, unharmed
    }
    e.returnValue = false;
    if (e.preventDefault)
    {
        e.preventDefault();
    }
}, false);

Now, this handler will be called after the listener at the window level calls its helper. This time, the event is changed if the clicked element didn't have the clickable class, or the element is a link. The event is canceled, but it lives on, still. The event is still free to propagate further down the dom, so we might encounter something like:

document.getElmentById('form3').addEventListener('click',function(e)
{
     e = e || window.event;
     if (e.returnValue === false || e.isDefaultPrevented)
     {//this event has been changed already
         //do stuff, like validation or something, then you could:
         e.cancelBubble = true;
         if (e.stopPropagation)
         {
             e.stopPropagation();
         }
     }
}, false);

Here, by calling stopPropagation, the event is killed off. It can't propagate further down the dom to its target unless the event was already altered. If not, the event object travels further down the DOM, as if nothing happened.

Once it reaches its target node, the event enters its second phase: the bubble phase. Instead of propagating down into the deeps of the DOM, it climbs back up, to the top level (all the way to the global object, where it was dispatched... from whence it came and all that).

In the bubble phase, all the same rules apply as in the propagation phase, only the other way around. The event object will encounter the elements that are closest to the target element first, and the global object last.

There's a lot of handy, and clear diagrams for this here. I can't put it any better than good 'ol quirksmode, so I suggest you read what they have to say there.

Bottom line: when dealing with 2 event listeners, attach them both on a different level to sort-of queue them the way you like.

If you want to guarantee both are called, only stop the event from propagating in that handler that will be called last.

When you've got two listeners, attached to the same element/object for the same event, I've never come across a situation where the listener that was attached first, wasn't also called first.

That's it, I'm off to bed, hoping I made sense