How do event bubbling, and event delegation work?
Event bubbling, or propagation, refers to how an event “bubbles up” to parent objects when triggered. For example, consider this code:
<p>
<span>Click me~!</span>
</p>
The p
element here would be considered the parent of the span
element.
When you click on the span
element, like you are instructed to, the span
element becomes the target of a click
event. That event, however, also bubbles up to the parent – the p
element can receive and consume that event as needed.
But what does this actually mean? Well, you could attach an event listener to the p
element:
const p = document.querySelector("p");
p.addEventListener("click", (event) => {
console.log(event.target);
});
Then, when you click on the span
element you will see the text Click me~!
logged to the console.
The event propagates to the parent p
element, which consumes it in an event listener to display the target of the event.
Notice how the target is still the span
element. This is because the span
element received the initial click.
Just to be sure how things are working, let’s expand our code:
const p = document.querySelector("p");
const span = document.querySelector("span");
p.addEventListener("click", (event) => {
console.log("P listener: ");
console.log(event.target);
});
span.addEventListener("click", (event) => {
console.log("Span listener: ");
console.log(event.target);
});
To give you an idea of how the event bubbles up, here’s what you’ll see in the console after clicking the span
element:
"Span listener: "
<span>Click me~!</span>
"P listener: "
<span>Click me~!</span>
Now let’s see what happens when you prevent the propagation of an event with stopPropagation()
. We’ll call it in our span
’s event listener:
const p = document.querySelector("p");
const span = document.querySelector("span");
p.addEventListener("click", (event) => {
console.log("P listener: ");
console.log(event.target);
});
span.addEventListener("click", (event) => {
console.log("Span listener: ");
console.log(event.target);
event.stopPropagation();
});
And then click our span
again:
"Span listener: "
<span>Click me~!</span>
This time, we don’t see our p
listener trigger. The event never fires for the p
element, because we told it to stop propagation while it was being processed for the child span
element.
Event delegation can be thought of as the opposite. It’s the process of taking a captured event, and delegating it to another element.
Going back to our code, let’s update it so clicking on a span
element changes it to red:
const p = document.querySelector("p");
const span = document.querySelector("span");
p.addEventListener("click", (event) => {});
span.addEventListener("click", (event) => {
event.target.style.color = "red";
});
But what if you have twenty span
elements? Or maybe you use JavaScript to create more span
elements on the fly?
Rather than having to attach an event listener to every single span
element, you can actually use the listener on the p
element for all of them. In other words, you can delegate the handling of the span
clicks to the parent p
element.
Our code might now look something like this:
const p = document.querySelector("p");
p.addEventListener("click", (event) => {
event.target.style.color = "red";
});
Notice how we no longer have any listener attached to the span
element at all. You have properly delegated the event handling to the p
element. But does it work?
Let’s generate a few extra span
elements and see:
<p>
<span>Click me~!</span>
<span>Click me~!</span>
<span>Click me~!</span>
<span>Click me~!</span>
</p>
Now, each time we click on a span
, that element’s text will become red.
And just like that, with a single event listener we’ve properly allowed a click
event to bubble up from span
elements to the parent p
, and delegated the logic for that click
event to the p
element.
Event propagation and delegation can be a complex topic, especially as you get into heavily nested elements like tables. You are encouraged to explore this further and experiment with some of the code we’ve written here.