Javascript Events Bubbling and Capture: The Weird Parts

A few weeks ago, I was trying to fix a small bug on our mobile website related to facebook login. This involved navigating through our front end code (something which is not my area of expertise). After a bit of poking around I figured out the bug was caused by some incorrect handling of a jquery event and had an easy fix. More interestingly I came across concepts like Event Delegation, Event Bubbling during this exercise. This seemed worth digging into and it was a slow week, so I started exploring more about javascript event propagation.

As one expects with everything javascript, things were not as straight forward as they looked on first glance. The event propagation in browsers has some weird, sometimes non-intuitive behaviors; in part due to the fact that back in the day different browsers had different, incompatible ways to handle event propagation. After reading through a bunch of things online and experimenting with code, I learnt a few things which I think could help a developer avoid some weird gotchas (not necessarily related to the more commonly used paradigms but useful nonetheless).

Event propagation in Javascript

Javascript allows a single event to be handled by multiple event handlers defined at different levels of DOM hierarchy.

Example

<div>  
    <ul>
        <li></li>
    </ul>
</div>  

In the above example, if we have a click event on <li>, and event listeners on both <ul> and <li>, both the handlers will be triggered. The order of invocation will depend on the propagation method.

What are these propagation methods?

HTML DOM API provides two methods for event propagation. One method is capturing where event trickles down from top of the DOM tree to the target element, and the other is bubbling where the event bubbles up from the target element to the top. So the order of event handling in those two methods will be respectively :-

  1. Capture method :- First the handler on <ul> will be called and then the handler on <li>

  2. Bubble method :- First the handler on <li> will be called followed by handler on <ul>

W3C supports both the modes. According to w3c specification, an event goes through 3 phases :- Capture, Target, and Bubble.

First an event trickles down the DOM tree in capture phase, hits the target, and finally bubbles back up. The addEventListener method has argument useCapture which is used to decide whether the handler will be called during capture phase or bubble phase. By default, an event listener is triggered during bubble phase.

Cool, so what's all the fuss about?

So far everything seems nice and dandy. But it wouldn't be javascript without some caveats. We saw above that an event would go through both capturing and bubbling phases. And by default a listener handles the event during bubbling phase. And that works fine for browser events like click. But once we start using custom events, things could get a bit more complicated.

Custom Events

Javascript also provides us with a way to create custom events using new Event [1] and dispatch them using dispatchEvent function. Handlers for these events can be added using addEventListener same as the in-built events. So one would expect them to have same 3 phases. But by default, a new type of event doesn't go through bubbles phase. It has to be mentioned explicitly.

For example if we want to create a new event type user_login and we write

           new Event("user_login")

the event won't go through bubbles phase. To enable bubbles phase, we'll have to write following [2]

           new Event("user_login", {bubbles : true})

So to be more precise with W3C specification of event phases, every event will have capture and target phase. But bubbles phase has to be explicitly defined [3] for each event type (even though the listeners work by default in bubbles phase). One has to be careful about this whenever we create a new custom event.

Wasn't the bug in jQuery code?

Yes, and some of you would know that jQuery doesn't support event capturing [4] [5], just bubbling for cross-browser compatibility. Unlike native DOM events, in jQuery any new event we create will by default always bubble up. So unlike vanilla javascript, we don't have to worry about bubbling for a custom event.

What if I don't want my jQuery custom event type to bubble up ever, not just stop propagation for a certain instance?

I don't know why anyone would want that. Most of the times we'll be using jQuery event delegation to bind handlers on dynamically inserted elements, which bubbles the event up the DOM hierarchy [6]. So it's unlikely that we'll ever need to create a new event type which would never bubble. But I still tried to figure this out, mostly as an academic exercise. If we really want to do this, we can use jQuery special events.

$.event.special.user_login = {noBubble : true}

If we set noBubble to true for an event using special events, the bubbling for these type of events will be turned off [7].

Can I mix jQuery events and native DOM events?

It's inadvisable. I tried some code where I wrote addEventListener to handle jQuery trigger. It doesn't necessarily work.
A jQuery event trigger will not necessarily be caught by an addEventListener [8]. It usually works for browser events because triggering a browser event would normally cause native DOM event to be fired too, but we shouldn't rely on that behavior. A jQuery event object has a field originalEvent which can be a native DOM event. But in a general case when we trigger a custom event, that field is undefined [9] i.e. jQuery doesn't create native event and fire that [10].

But I really really want to mix the two

Once again special events come to rescue. Using special events, we can add custom logic to an event trigger [11]. This trigger will execute before all event handlers are called [12]. So that can be used to fire a native event if required.

     $.event.special.user_login = {
          trigger : function() {
               var e = new Event("user_login", {bubbles : true});
               $(this).get(0).dispatchEvent(e);
          }
     };

At least that's the only way I could figure out to do this reliably. Maybe there are other, cleaner approaches. If someone knows any such method, please let me know.

That's it for this post. You can find details of all the things summarized above in the references. If you have any questions, suggestions, please let us know in the comments.

References

https://developer.mozilla.org/en-US/docs/Web/API/Event/Event

https://developer.mozilla.org/en-US/docs/Web/API/Event/bubbles

https://www.w3.org/TR/DOM-Level-3-Events/#event-types

http://stackoverflow.com/questions/7163616/why-does-jquery-event-model-does-not-support-event-capture-and-just-supports-eve

https://bugs.jquery.com/ticket/14953

http://jqfundamentals.com/chapter/events

https://github.com/jquery/jquery/blob/master/src/event/trigger.js#L76

https://bugs.jquery.com/ticket/11047

https://bugs.jquery.com/ticket/8055

https://github.com/jquery/jquery/issues/2476#issuecomment-121690946

http://benalman.com/news/2010/03/jquery-special-events/

https://github.com/jquery/jquery/blob/master/src/event/trigger.js#L70

comments powered by Disqus