Joel Hassan

JavaScript Event Propagation - Capturing & Bubbling

2019-07-03

Introduction

The ideas of event capturing and bubbling have to do with event handling patterns in the browser. Events fired for a particular element in the browser go on to fire in other elements that are part of the nested DOM structure. Understanding this behaviour is important when dealing with event delegation between parent and children elements. In this post, we'll take an overview of event bubbling and capturing.


Easing in

Before going into capturing and bubbling, let's define what we mean by event propagation.

Event Propagation

In the DOM, events can propagate. By propagation, we mean spreading. As such, event propagation has to do with event handling spreading from one element to another.

A reminder is needed here. When talking about the DOM (or think the web browser), we have to remember that elements are nested within each other. The DOM is a tree structure. Here's an example form it could take:

html
  head
    link
    script
  body
    div
      h1
      div
        ul
          li
          li
          li
      form
        input
        input

What does it imply?

That when we fire off an event for one element — say by clicking — that is nested within other elements (and all elements are, just to different degrees), then that event 'ripples' to the other elements that are part of the whole tree structure.

Let's use an example to solidify this. Say we have the following HTML:

<html lang="en">
  <body class="doc-body">
    <!-- Here we have 3 div elements, nested  -->
    <div class="divOne">
      <div class="divTwo">
        <div class="divThree"></div>
      </div>
    </div>
  </body>
</html>

And the following script:

// a function that logs the class names of each element
function inspectEvent(e) {
  console.log(this.classList.value);
}

// let's select all the div elements
const divs = document.querySelectorAll('div');
// on clicking any of the divs, the function is called
divs.forEach(div => div.addEventListener('click', inspectEvent));

What would happen if we click any of the three divs? Enter bubbling.


Bubbling

Bubbling is the default way the events propagate from one element to another. When we click (or when any other event is triggered), the following process happens:

  1. Capturing - the event goes to the element where the interaction occurs - the div we clicked. The other elements are kept note on on the way
  2. Target is found - the target element (event.target) is reached
  3. Bubbling - action at event.target and then at each element going outward in the nested structure

All the elements are first noted by the browser, the event is then handled by the innermost element and then propagated in the outward direction toward the document root. As such, we start with the clicked element (the event.target) and go (or 'bubble') all the way to up to the document body element, which also happens to have the event fire on it — take a look at the tree structure.

Back to our divs example - what'd happen if we were to click on, say, elementThree (the innermost div)? This is what the console would output:

divThree
divTwo
divOne

Meaning, that the event is fired for divThree element first, then divTwo, and finally divThree.

What about clicking the middle div?

divTwo
divOne

We clicked divTwo, so its event handler fired. Then divOne's, which is the outermost div. The innermost div (divOne) had no event fired on it due to the direction if the bubbling.

That's bubbling in short. We start with the element most deeply nested and move up the DOM tree from there, firing the event on each element as we go.

We can also reverse the direction of propagation — make the events trickle down from top to bottom. This is capturing.


Capturing

Capturing is quite rare, which is why bubbling is the default behavior. With capturing, the event is first captured by the outermost element and propagated to the inner elements.

It's also called "trickling", so you can remember the propagation order by:

trickle down, bubble up

How to enable it?

We can take our script from above, and pass an options object as the third argument to the addEventListener method.

divs.forEach(div =>
  div.addEventListener("click", logText, {
    capture: false, // when true, causes the handling function to be run on trickle-down instead
  })
)

In sum, capturing is just the reverse of bubbling with regard to event propagation.


A Few More Things

Stopping Event Propagation

You might want to stop bubbling all together, and that's achieved with:

e.stopPropagation() // does what it says

You would stick that in the function that handles the event. Now, the event would only fire for the event.target - only the one that was clicked.

Setting the event to fire only once

We might want to limit the number of times the event handling can actually occur for a given element. For example, we might only want an action for a button to happen once, and only once, regardless of the number of times the user clicks on it.

Here's how that would be done:

button.addEventListener(
  "click",
  () => {
    console.log("Click!!")
  },
  {
    once: true, // here's the magic
  }
)

We've passed an options argument again. This time we've set the once property to true — i.e. we're telling the event handler that that's exactly the number of times the action can happen. This can be very useful in say, limiting the number of times a button click causes something to be submitted to a server.


Wrap up

There you go. That's a brief overview of the concepts, and I've included some links and resources below for more digging.


Resources & sources to check out

I'd also highly recommend checking out Wes Bos' js30 video video series:

Other useful links: