Event Delegation : Capturing & Bubbling

Event Delegation : Capturing & Bubbling

Ahh, one more article on "Event capturing, bubbling and delegation".

Well, if you can deduce the output of the following code correctly then you can skip reading the rest of the article

What will be logged on console when you click on the '"greatgrandchild" div.

<div class="grandparent" style="width: 400px; height: 400px; background-color: red">
  <div class="parent" style="width: 300px; height: 300px; background-color: black">
    <div class="child" style="width: 200px; height: 200px; background-color: cyan">
      <div class="grandchild" style="width: 100px; height: 100px; background-color: yellow">
         <div class="greatgrandchild" style="width: 50px; height: 50px; background-color: purple">

          </div>
        </div>
      </div>
    </div>
  </div>
const parent = document.querySelector('.parent')
const grandparent = document.querySelector('.grandparent')
const child = document.querySelector('.child')
const grandchild = document.querySelector('.grandchild')
const greatgrandchild = document.querySelector('.greatgrandchild')

grandparent.addEventListener('click', () => console.log("grandparent capture event handler called"), true)
parent.addEventListener('click', () => console.log("parent bubble event handler called"))
child.addEventListener('click', () => console.log("child capture event handler called"), true)
grandchild.addEventListener('click', () => console.log("grandchild bubble event handler called"))
greatgrandchild.addEventListener('click', () => console.log("greatgrandchild capture event called"), true)

. . . . . . . . . .

Answer:

"grandparent capture event handler called"
"child capture event handler called"
"greatgrandchild capture event called"
"grandchild bubble event handler called"
"parent bubble event handler called"

If you got the output wrong then the following article will help you understand how events work in Javascript.

Phases of an Event

Whenever an event is fired by the user on the web app, there are two phases - capturing and bubbling. First the event goes through the capturing phase and then the bubbling phase.

Event Capturing

Flow of the event from parent element to child elements

Event Bubbling

Flow of the event from child element to parent elements

Lets understand this by example

<body>
  <div class="parent" style="width: 300px; height: 300px; background-color: black">
    <div class="child" style="width: 200px; height: 200px; background-color: cyan">
      <div class="grandchild" style="width: 100px; height: 100px; background-color: yellow">

      </div>
    </div>
  </div>
</body>

We have three boxes(divs) parent, child and grandchild. Let's add "click" event listeners to these boxes.

const parent = document.querySelector('.parent')
const child = document.querySelector('.child')
const grandchild = document.querySelector('.grandchild')

parent.addEventListener('click', () => console.log("parent bubble event handler called"))
child.addEventListener('click', () => console.log("child bubble event handler called"))
grandchild.addEventListener('click', () => console.log("grandchild bubble event handler called"))

We added event listeners to the bubble phase of each box. So if you click on the grandchild box the console output will be,

    "grandchild bubble event handler called"
    "child bubble event handler called"
    "parent bubble event handler called"

Now if you want to add event handlers for the capturing phase of the event then that can be done by adding a boolean argument "true" to the event listener.

parent.addEventListener('click', () => console.log("parent capture event handler called"), true)
child.addEventListener('click', () => console.log("child capture event handler called"), true)
grandchild.addEventListener('click', () => console.log("grandchild capture event handler called"), true)

Now we have total 6 event listeners, 3 for bubble phase and 3 for capture phase. Now the console will look like,

    "parent capture event handler called"
    "child capture event handler called"
    "grandchild capture event handler called"
    "grandchild event handler called"
    "child event handler called"
    "parent event handler called"

So if you were under the misconception that the capture flag enables/disables the capture phase of event listener. Then now it should be clear that capture and bubbling phase of an event are always present, the capture flag (true/false) just adds a particular event listener for a particular phase - capture for true and bubble for false.

You should now be able to deduce the output of the first code of the article.

Event Propagation

stopPropagation

Let's say for some reason you want to stop the flow of the event after the grandchild's capture phase. To achieve that you can use the "stopPropagation()" method of the event object. An event object is available as the parameter of an event handler. Now we will just change the event handler for grandchild as follows,

grandchild.addEventListener('click', (event) => {
    console.log("grandchild capture event handler called")
    event.stopPropagation()
}, true)

Because of above change the console will now look like follows when we click on the grandchild box,

    "parent capture event handler called"
    "child capture event handler called"
    "grandchild capture event handler called"

We stopped the propagation of the event after the grandchild's capture event handler. Hence we don't see those statements in the console.

stopImmediatePropagation

Wait, we have another method called 'stopImmediatePropagation()" ? What for ?

We can add multiple event handlers to a particular event of an element. While "stopPropagation()" stops the flow of event, it will still execute all the event handlers for the current element. Don't worry, the following example will help you understand.

Let the grandchild element have three event handlers for capture phase.

grandchild.addEventListener('click', (event) => {
    console.log("first grandchild capture event handler called")
    event.stopPropagation()
}, true)

grandchild.addEventListener('click', (event) => {
    console.log("second grandchild capture event handler called")
    event.stopImmediatePropagation()
}, true)

grandchild.addEventListener('click', (event) => {
    console.log(" third grandchild capture event handler called")
}, true)

Now even though we call "stopPropagation()" in the first event handler, all the event handlers for current element will still be executed. But the second event handler calls "stopImmediatePropagation()", so any event handler after the current one, will not be executed, even for the current element.

    "parent capture event handler called"
    "child capture event handler called"
    "first grandchild capture event handler called"
    "second grandchild capture event handler called"

Event Delegation

Event delegation is a side effect of event bubbling. In other words, event delegation makes use of the bubbling phase of an event.

Consider the following example where there are 6 paragraph elements and we want to handle the click event on each of the "p" elements.

<div id="root">
    <h1>Avengers</h1>
    <p class="avenger">Iron man</p>
    <p class="avenger">Captain America</p>
    <p class="avenger">Hulk</p>
    <p class="avenger">Thor</p>
    <p class="avenger">Black Widow</p>
    <p class="avenger">Hawkeye</p>
</div>

We want to print the text content of the clicked avenger on console. So that sounds simple, just add click event listener to all the "p" tags. Let's say there are 100 "p" tags. Now with 100 event listeners there might be a chance of memory leak, meaning some event listener might not be removed by garbage collector. Though the browsers are very efficient in cleaning up the unused memory but still we should try to optimize the solution if possible.

We know that the events go through the capturing and bubbling phase. Hence when any "p" will be clicked the event will bubble up to the "div" tag. So why don't we add just one event listener to this "div" tag and if an click event is coming from a "p" tag then handle that event.

const rootDiv = document.querySelector('#root')

const eventHandler = event => {
  if(Array.from(event.target.classList).includes('avenger') && event.target.tagName === "P"){
    console.log(event.target.textContent)
  }
}

rootDiv.addEventListener('click', eventHandler)

Here first we add an event listener for bubble phase of the root div. In the handler function we check for 2 conditions

  1. The target element should have a class named "avenger"
  2. The target element should be a paragraph "p" element

We could have used any one of the above conditions and it would still work, just FYI.

Now whenever a user clicks on any avenger's name, that name will be logged in the console.

That's all about events in Javascript. Thanks for reading.