Handling Events in JavaScript

Photo by Toa Heftiba on Unsplash

Handling Events in JavaScript

Event Propagation, Bubbling, Capturing, Trickling, Delegation. Everything under one roof.

Β·

6 min read

But first, back to basics ! Back to basics

What are events and event handlers ?

An event is a signal that something has happened. Often, when events happen, you may want to do something. There are many events that JavaScript provides to interact with the DOM. And to react to those events, event handlers are required. It takes a "event", and a callback function that runs in case of an event.

What is Event Propagation ?

Now you know. Think of a HTML code structure where there are multiple nested elements. Event Propagation determines in which order the elements receive the event. Event propagation is a way to describe the β€œstack” of events that are fired in a web browser. It is method that describes how the browser handles events targeted at nested elements.

What is Event Capturing and Event Bubbling ?

Event Capturing and Bubbling are the two ways of event propagation in the DOM tree.

Bubbling

When an event happens on an element, it first runs the handlers on it, then on its parent, then all the way up on other ancestors.

Suppose we have the following nested HTML structure πŸ‘‡

<div id="outer" onClick="outer()">Outer
    <div id="parent" onClick="parent()">Parent
        <div id="child" onClick="child()">Child</div>
    </div>
</div>

On clicking on child, in what sequence the methods will be called ?

On clicking the child element, event bubbling takes place. It is the default behavior. Think of it like when is bubble is born, it grows and expands itself. So, child -> parent -> outer, this is the sequence of the entire click events that will take place. Bubbling starts from the clicked element.

Similarly, on clicking the parent element, parent -> outer will be the sequence of the click events that will take place.

Event Capturing/ Trickling

When an event happens on an element, it first runs on the outer most parent, then all the way down to other descendants. So it is capturing down the DOM tree.

Again, for the following code πŸ‘‡

<div id="outer" onClick="outer()">Outer
    <div id="parent" onClick="parent()">Parent
        <div id="child" onClick="child()">Child</div>
    </div>
</div>

On clicking the child element, event capturing takes place. The sequence of the entire click events will be : outer -> parent -> child. Capturing starts from the outer most parent and ends up on the clicked element.

Syntax πŸ‘‡

child.addEventListener("click", callback, useCapture);

useCapture is an optional parameter that is to be provided to the event listener. It's value is boolean. true -> capturing and false -> bubbling. By default, the value is false or in the case this parameter is not provided.

Now, let's see bubbling in action.

HTML code πŸ‘‡

<div id="parent">
      Parent
      <div id="child">
        Child
        <div id="grandchild">Grand Child</div>
      </div>
</div>

JavaScript code πŸ‘‡

const parent = document.querySelector("#parent");
      const child = document.querySelector("#child");
      const grandChild = document.querySelector("#grandchild");
      const output = document.querySelector("#output");

      parent.addEventListener(
        "click",
        () => {
          let para = document.createElement("p");
          para.innerHTML = `Parent clicked`;
          output.appendChild(para);
        }
      );

      child.addEventListener(
        "click",
        () => {
          let para = document.createElement("p");
          para.innerHTML = `Child clicked`;
          output.appendChild(para);
        }
      );

      grandChild.addEventListener(
        "click",
        () => {
          let para = document.createElement("p");
          para.innerHTML = `Grand Child clicked`;
          output.appendChild(para);
        }
      );

As I said earlier, by default, events will bubble up. Hence, clicking on grand child will result into grandChild -> child -> parent as the sequence of events.

image.png

Same output is obtained, if we provide optional third parameter as false, in all the event listeners.


Capturing/ Trickling in action

Provide the third parameter, true. Your code should look like : πŸ‘‡

parent.addEventListener(
        "click",
        () => {
          let para = document.createElement("p");
          para.innerHTML = `Parent clicked`;
          output.appendChild(para);
        },
        true
      );

      child.addEventListener(
        "click",
        () => {
          let para = document.createElement("p");
          para.innerHTML = `Child clicked`;
          output.appendChild(para);
        },
        true
      );

      grandChild.addEventListener(
        "click",
        () => {
          let para = document.createElement("p");
          para.innerHTML = `Grand Child clicked`;
          output.appendChild(para);
        },
        true
      );

Events will capture down. Hence, clicking on grand child will result into parent -> child -> grand child.

image.png

Mix-match case - What if you want certain events to fire up afterwards and certain early ? See the following code : πŸ‘‡

parent.addEventListener(
        "click",
        () => {
          let para = document.createElement("p");
          para.innerHTML = `Parent clicked`;
          output.appendChild(para);
        },
        true // capturing 
      );

      child.addEventListener(
        "click",
        () => {
          let para = document.createElement("p");
          para.innerHTML = `Child clicked`;
          output.appendChild(para);
        },
        false // bubbling
      );

      grandChild.addEventListener(
        "click",
        () => {
          let para = document.createElement("p");
          para.innerHTML = `Grand Child clicked`;
          output.appendChild(para);
        },
        true // capturing
      );

Here, the child element won't be covered in the capturing phase. When the target element is reached, the phase is changed. The following output is obtained when clicking on the grand child element.

image.png

Stop Bubbling or Capturing

But, consider a scenario in which there are multiple nested elements. We need to stop propagation, at some point of the chain as these operations could be costly in such cases. How we can stop propagation up/down the hierarchy ? Using e.stopPropagation().

Code πŸ‘‡

parent.addEventListener(
        "click",
        () => {
          let para = document.createElement("p");
          para.innerHTML = `Parent clicked`;
          output.appendChild(para);
        },
        true // capturing
      );

      child.addEventListener(
        "click",
        (e) => {
          let para = document.createElement("p");
          para.innerHTML = `Child clicked`;
          output.appendChild(para);
          e.stopPropagation();
        },
        true // capturing
      );

      grandChild.addEventListener(
        "click",
        () => {
          let para = document.createElement("p");
          para.innerHTML = `Grand Child clicked`;
          output.appendChild(para);
        },
        true // capturing
      );

When clicked on the grand child, the following will be the output.

image.png

What to use ? Bubbling ? Capturing ? πŸ‘‡

It depends on what you want to do. There is no better. The difference is the order of the execution of the event handlers.

Bubbling and capturing lay the foundation for β€œevent delegation” – an extremely powerful event handling pattern.

Event Delegation

Event delegation is based on event bubbling. When working with events in a larger applications, there comes a point when there are many event handlers. Consider a situation, where you have a render a list of elements or something like infinite scroll. There, on clicking each of the element under the list, you want certain action to be taken. Hence, `number of event listeners ∝ to number of elements. Hence, resulting in bad performance and can be costly too.

Event delegation states that, "Instead of attaching the event handlers to all the child elements, attach to the only parent of all those child elements."

Traditional Aprroach. πŸ‘‡

<ul id="products-list">
        <li class="product" id="shoes">Shoes</li>
        <li class="product" id="jacket">Jacket</li>
        <li class="product" id="clothes">Clothes</li>
      </ul>
<div id="output"></div>

<script>

      const productsList = document.querySelectorAll(".product");
      const output = document.querySelector("#output");

      productsList.forEach((product) =>
        product.addEventListener("click", () => {
          output.innerHTML = `${product.innerHTML} is clicked`;
        })
      );

    </script>

Delegation in action. Replace the script, with this : πŸ‘‡

const productsList = document.querySelector("#products-list");

      productsList.addEventListener("click", (e) => {
        output.innerHTML = `${e.target.id} is clicked`;
      });

Now, there is single event listener(on the parent element) instead of many. When clicked on the clothes, the following output is obtained.

image.png

This is done using the concept of event bubbling. Child(shoes) is clicked, it is bubbled up to the parent(products-list). Because of the single event handler attached, it listens to each and every click happens inside of it.

Pros of Event Delegation:

  • It consumes less memory
  • As there is single event handler instead of many, it leads to better performance.
  • Less code

Cons of Event Delegation:

  • Not all events are bubbled up.
    • For eg: blur, focus

CSB's

Play around with the code ! 🎯

-> Event Bubbling and Capturing: codesandbox.io/s/event-bubbling-and-capturi..

-> Event Delegation (Eg 1): codesandbox.io/s/event-delegation-eg-1-uh6q..

-> Event Delegation (Eg 2 - Tree Menu): codesandbox.io/s/event-delegation-eg-2-v6gj..

Bubble Up, Trickle Down

That's it for this article. If you liked and learned something new, do consider sharing β€οΈπŸ‘

Β