Are you sure you know how an event propagates in JavaScript?
Tags: javascript, webdev, programming, html
Published: 2021-02-17 13:33:38

**Events** are everywhere in web programming — input change, mouse move, 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0jf68va42ebsajhg5tx4.png)

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

```js
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](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#event_bubbling_and_capture), and it's a core part of the [W3C event model](https://dom.spec.whatwg.org/#introduction-to-dom-events).

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](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#event_bubbling_and_capture).

>💡 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.

```js
// 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.

```js
// 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 *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 visualise this easily: 

![Demonstrating the event flow in W3C event model](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mcvjpohj3sxjn7pafq94.png)

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 upward 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 the **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. 🧐


With this new knowledge in our bag, it's time to look back to our first example and try to analyse why the output was in reverse order. Here's the first example again so that you're not creating a `scroll` event 😛

```js
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](https://developer.mozilla.org/en-US/docs/Web/API/Event/eventPhase) property of the event. This helps you visualise which phase of the event is currently being evaluated.

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

Here's the [codepen demo](https://codepen.io/singharmani/pen/rNWyrPE) if you like to play with it. Or you can paste the code snippet below in your browser and see it yourself.

```js
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](https://developer.mozilla.org/en-US/docs/Web/API/Event/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: 

```js
// 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 element2 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](https://jsbin.com/zegabiqari/edit?js,console,output) 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: 
- [Introduction to "DOM Events"](https://dom.spec.whatwg.org/#introduction-to-dom-events) - (whatwg specs)
- [Introduction to events](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events) - (Mozilla documentation)
- [Event phases and stop propagation demo](https://jsbin.com/zegabiqari/edit?js,console,output) - (JSbin demo)