If you read the previous article about Event Bubbling, you probably know that event propagation in HTML is done from the innermost element to all of its parents.
But what if I told you that it is done the other way around?
Let me explain - there are three phases of event propagation in the HTML DOM:
The following image illustrates all three phases perfectly:
When the td element is clicked, an event first goes down (Event Capturing), reaches the target element and triggers there (Target Phase) and goes up (Event Bubbling).
Event Capturing is a process of executing event handlers from the outermost element all the way down to the target.
Most of the time when we talk about event propagation phases, we only mention Event Bubbling and the reason for this is a very rare usage of Event Capturing, in most cases it is not visible to us.
Usually, when attaching an event listener to the element, we pass only two arguments: event name and event handler:
button.addEventListener("click", handleClick);
And such handlers know nothing about Event Capturing, they only run Target and Event Bubbling phases.
To run an event in the Capturing phase, we need to pass the third argument:
// Run an event on the Capturing phase
button.addEventListener("click", handleClick, { capture: true );
// Shorthand
button.addEventListener("click", handleClick, true);
By default, capture option is set to false.
To understand the phase better, let's look at the following example:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Event Capturing</title>
</head>
<body>
<div>
<button>Click me</button>
</div>
<script>
const nodes = document.querySelectorAll("*");
for (const node of nodes) {
node.addEventListener(
"click",
(e) => console.log(`Capturing: ${node.tagName.toLowerCase()}`),
true
);
}
</script>
</body>
</html>
We select all DOM nodes and attach a listener running in the Capturing phase.
The output:
Notice how clicking on the button element triggers clicks on the parent elements first.
The Capturing phase is always executed before the Bubbling phase:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Event Capturing</title>
</head>
<body>
<div>
<button>Click me</button>
</div>
<script>
const nodes = document.querySelectorAll("*");
for (const node of nodes) {
node.addEventListener(
"click",
(e) => console.log(`Capturing: ${node.tagName.toLowerCase()}`),
true
);
node.addEventListener("click", (e) =>
console.log(`Bubbling: ${node.tagName.toLowerCase()}`)
);
}
</script>
</body>
</html>
The output:
One important thing to remember when removing event handlers added in the Capturing phase is to not forget to pass the phase:
// Add
button.addEventListener("click", handleClick, true);
// Remove
button.removeEventListener("click", handleClick, true);
If we don't pass the phase, an event handler will not be removed:
button.addEventListener("click", handleClick, true);
// This will not work!
button.removeEventListener("click", handleClick);
Event propagation in the HTML DOM consists of three phases, executed in the given order: Capturing, Target and Bubbling.
By default, when we attach an event to an element, it is executed in the Bubbling phase, and this is the reason why when we talk about the phases of event propagation, we usually only mention Event Bubbling.
To execute an event in the Capturing phase, we need to pass the third argument to the event listener.
You probably won't use this phase too often, but that doesn't mean you shouldn't know about it.