Document Object Model (DOM) event propagation, capturing, and bubbling.

What is the DOM?

The DOM is a tree of objects that is created by the browser and is a W3C (World Wide Web Consortium) standard. It defines the standards for accessing documents.

From W3 Schools (continued)

The W3C DOM standard is separated into 3 different parts:

  • Core DOM — model for all document types
  • XML DOM — model for XML documents
  • HTML DOM — model for HTML documents

The HTML DOM is a standard object model and programming interface for HTML.

It defines:

  • The HTML elements as objects
  • The properties of all HTML elements
  • The methods to access all HTML elements
  • The events for all HTML elements

In other words: The HTML DOM is a standard for how to get, change, add, or delete HTML elements.

I paraphrased a lot of this section from W3 Schools since this posts isn’t about what the DOM is. I like to explain everything mentioned for anyone that isn’t already aware of what it is.

What is Event Propagation?

Event propagation is how events propagate or travel through the DOM tree to reach its target and what happens to it afterward.

Examples of HTML events:

  • Mouse click
  • Page loaded
  • Image loaded
  • Move over/hover
  • Input field changes
  • Form submitted
  • Keystrokes

Events propagate through other elements.

This means that if there was an event listener farther up the DOM tree then both will be affected. Here is an example.

<div class="hello">
<div class="world">
<script src="hello-world-listener-demo.js"></script></body>

And if we apply the event listeners

const hello = document.querySelector('.hello');
const world = document.querySelector('.world');
const funcH = function() {
const funcW = function() {
hello.addEventListener('click', funcH);
world.addEventListener('click', funcW);

Now if we clicked on “world!” what do you think would happen?

Demonstrating what would happen with 2 nested event listeners.

Two alerts pop up saying “world!” then “Hello”.

This happened because both of the listeners were triggered from that one click. Thus two different events happened. Resulting in both alerts showing up.

I will explain how to fix this later, let’s talk about the next parts of this topic first.

Event Capturing

In event capturing, an event propagates from the outermost element (the document/window) to the target element (the world div).

In our previous example the event went up like this:

document html <— body <— hello div <— world div

So do we make the reverse happen so that it will log “Hello” and then “world!” when “world!” is clicked on?

We can simple add a true boolean to the end of the hello click listener.

const hello = document.querySelector('.hello');
const world = document.querySelector('.world');
const funcH = function() {
const funcW = function() {
hello.addEventListener('click', funcH, true);
world.addEventListener('click', funcW);
Hello world! Demonstration with event capturing.
Hello world! Demonstration with event capturing.
Demonstrating what would happen with 2 nested event listeners while using event capturing.

Now the event propagated from the outermost element element (the document/window) to the innermost element (the world div).

document —> html —> body —> hello div —> world div

By default the propagation always starts at the target and out to the document or window.

Now how can we bubble, or isolate, these two nested events. What if I want “Hello” and “world!” to always alert separately without changing the way the divs are nested? Well let’s get into that!

Event Bubbling

The easiest fix for my example, without changing the div nesting, would be to pass the event as a parameter into the funcW function and tell JavaScript to stop propagation at that event.

const funcW = function(event) {

So the new javascript file looks like this:

const hello = document.querySelector(‘.hello’);
const world = document.querySelector(‘.world’);
const funcH = function() {
const funcW = function(event) {
hello.addEventListener(‘click’, funcH);
world.addEventListener(‘click’, funcW);

If we try clicking “world!” again with this updated code let’s see what happens.

Demonstrating what would happen with the updated code while the event listeners are still nested and one is stopping the propagation.

Voila! We separated them!

What are the Use Cases of Bubbling?

This could have obviously been fixed by taking the world div outside of the hello div, but sometimes that’s not as practical.

Imagine you were listing users in a table. You add an event listeners to their name so that when the name is clicked on it takes the client to the target user’s profile. Let’s say that you also have other information on that same row like an email address or phone number that is supposed to open up a mailto: or tel:. Now you're in the conflict of that a tag firing the event listener for that row. Thus taking the client to the profile by mistake.

Other use cases

Some other use cases could be ‘looping’ through a list of those users to find which one was the target, or a to-do list to mark that task off.

<div class="todo">
<h1>David's Todo's</h1>
<ul class="todo-list">
<li class="item">Wash dishes</li>
<li class="item">Walk the dog</li>
<li class="item">Fold the laundry</li>
<li class="item">Proofred the blog post</li>
<li class="item">Submit the blog post</li>

Since it would be a little tedious to add a listener to every single item in the list and we can instead add 1 event listener to the entire list and act on that item that was clicked on.

Here is an example:

const list = document.querySelector(".list");

list.addEventListener("click", e => {"completed")
Todo List Demostration
Todo List Demostration
Demonstration of the todo list bubbling.

This wasn’t actually a loop through the elements, but it still highlights the bubbling aspect. Since the listener is on the list as a whole, but acting upon the item that was clicked on instead of that entire list.

Ideally it would be best to avoid any such instance of stacked listeners by separating them and not nesting them. However, it wouldn’t be bad practice to add event.stopPropagation() to all your event function just to make sure they are all bubbled and isolated. After all it becomes harder to keep track of all of the event listeners as pages grow and become more complex. There are also some unavoidable conflicts such as a tags that would also trigger other stacked events.

Software Engineering student at Operation Spark and WebDev hobbyist.