Aman Explains

Settings

Are you sure you know how event propagates in JavaScript?

February 17, 2021• ☕️ 4 min read

Events are everywhere in web programming — input change, button click, and page scroll are all forms of events. These are the actions that get generated by the system so that you can respond to them however you like by registering event listeners. This results in an interactive experience for the user. Understanding how the event model works in modern web browsers can help you build robust UI interactions. Get it wrong, and you have bugs crawling around.

My aim through this article is to elaborate some basics around the event propagation mechanism in the W3C event model. This model is implemented by all modern browsers.

Let’s get started ⏰.


Event propagation

Imagine If we have two HTML elements, element1 and element2, where element2 is the child of element1 as shown in the figure below:

Nested elements with click handlers

And we add click handlers to both of them like this:

element1.addEventListener('click', () => console.log('element1 is clicked'));
element2.addEventListener('click', () => console.log('element2 is clicked'));

What do you think will be the output when you click element2? 🤔

The answer is element2 is clicked, followed by element1 is clicked. This phenomenon is known as Event bubbling, and it’s the core part of the W3C event model. In event bubbling the innermost target element handles the event first and then it bubbles up in the DOM tree looking for other ancestor elements with registered event handlers.

In event bubbling the innermost target element handles the event first and then it bubbles up in the DOM tree

Now, the interesting bit is that event flow is not uni-directional, as you might have assumed. The event flow mechanism in the W3C event model is Bi-directional. Surprise Surprise! 😯. We mostly have been dealing with event bubbling when working with frameworks like React and never think much of another phase which is Event Capturing.

Event bubbling is just one side of the coin; Event capturing is the other.

In the event capturing phase, the event is first captured until it reaches the target element (event.target). And you, as a web developer, can register your event handler in this phase by setting true as the third argument inside the addEventListener method.

// With addEventListener() method, you can specify the event phase by using `useCapture` parameter.
addEventListener(event, handler, useCapture);

By default, it’s false indicating that we are registering this event in the bubbling phase. Let’s modify our example above to understand this better.

// Setting "true" as the last argument to `addEventListener` will register the event handler in the capturing phase.
element1.addEventListener('click', () => console.log('element1 is clicked'), true);

// Whereas, omitting or setting "false" would register the event handler in the bubbing phase. 
element2.addEventListener('click', () => console.log('element2 is clicked')));

We have added true for useCapture parameter indicating that we are registering our event handler for element1 in the capturing phase. For element2, omitting or passing false will register the event handler in the bubbling phase. Now, if you click the element2, you will see element1 is clicked is printed first followed by element2 is clicked. This is the capturing phase in action.

In the event capturing phase, the event is first captured until it reaches the target element

Here’s the diagram to help you visualize this easily:

Demonstrating the event flow in W3C event model

The event flow sequence is:

  1. The “click” event starts in capturing phase. It looks if any ancestor element of element2 has onClick event handler for the capturing phase.
  2. The event finds element1, and invokes the handler, printing out element1 is clicked.
  3. The event flows down to the target element itself(element2) looking for any other elements on its way. But no more event handlers for the capturing phase are found.
  4. Upon reaching element2, the bubbling phase starts and executes the event handler registered on element2, printing element2 is clicked.
  5. The event travels upwards again looking for any ancestor of the target element(element2) which has an event handler for the bubbling phase. This is not the case, so nothing happens.

So, the key point to remember here is that the whole event flow is the combination of event capturing phase followed by the event bubbling phase. And as an author of the event handler, you can specify which phase you are registering your event handler in. 🧐

Armed with this new knowledge, it’s time to look back to the first example and try to analyze why the output was in reverse order. Here’s the first example again so that you’re not creating a scroll event 😛

element1.addEventListener('click', () => console.log('element1 is clicked'));
element2.addEventListener('click', () => console.log('element2 is clicked'));

Omitting the useCapture value registered the event handlers in the bubbling phase for both the elements. When you clicked element2, the event flow sequence was like:

  1. The “click” event starts in capturing phase. It looks if any ancestor element of element2 has onClick event handler for capturing phase and doesn’t find any.
  2. The event travels down to the target element itself(element2). Upon reaching element2, the bubbling phase starts and executes the event handler registered on element2, printing element2 is clicked.
  3. The event travels upwards again looking for any ancestor of the target element(element2) which has an event handler for the bubbling phase.
  4. This event finds one on element1. The handler is executed and element1 is clicked is printed out.

Another interesting thing you can do is logging out the eventPhase property of the event. This helps you visualize which phase of the event is currently being evaluated.

element1.addEventListener('click', (event) => console.log('element1 is clicked', {eventPhase: event.eventPhase}));

Here’s the codepen demo if you like to play with it. Or you can paste the code snippet below in your browser and see it yourself.

const element1 = document.createElement("div");
const element2 = document.createElement("div");

// element1: Registering event handler for the capturing phase
element1.addEventListener(
  "click",
  () => console.log("element1 is clicked"),
  true
);

// element2: Registering event handler for the bubbling phase
element2.addEventListener("click", () => console.log("element2 is clicked"));

element1.appendChild(element2);

// clicking the element2
element2.click();

Stopping the event propagation

If you wish to prevent further propagation of current event in any phase, you could invoke stopPropagation method available on the Event object.

So, it means invoking the event.stopPropagation() inside the element1 event handler (in capturing phase), would stop the propagation. And if even if you click element2 now, it won’t invoke its handler.

The following example demonstrates that:

// Preventing the propagation of the current event inside the handler
element1.addEventListener(
  "click",
  (event) => {
    event.stopPropagation();
    console.log("element1 is clicked");
  },
  true
);
// The event handler for the element1 will not be invoked.
element2.addEventListener('click', () => console.log('element2 is clicked'));

Note that event.stopPropagation stops the propagation only. It does not, however, prevent any default behaviour from occurring. For example, clicking on links are still processed. To stop those behaviours, you can use event.preventDefault() method.

Finally here’s another cool JSbin demo if you like to play along and see how can you stop the event propagation via event.stopPropagation.

I hope this article was helpful and has given you some insights. Thanks for reading 😍


Useful resources:


Amandeep Singh

Written by Amandeep Singh. Developer @  Avarni  Sydney. Tech enthusiast and a pragmatic programmer.