Events in Javascript are a way to trigger a function when something happens on the webpage. For example, when a user clicks on a button, a click event is triggered. When a user presses a key on the keyboard, a keydown event is triggered. When a user hovers over an element, a mouseover event is triggered.
In this lesson, you'll learn about event propagation. Event propagation refers to the way that events travel through the DOM tree. Allowing you to capture events on parent elements, or even the document itself.
What is Javascript Event Propagation
Event propagation, in the context of web development, is a bit like a ripple effect that occurs when you interact with a webpage. Imagine you're clicking on a button on a site. This click triggers an event.
In the world of web pages, a click doesn't just affect the button; it sends ripples up and down the structure of the page. This ripple effect is event propagation. It means that when you click on that button, not only does the button know it's been clicked, but so do its parent elements, all the way up the tree.
This allows you to do things like attach a click event listener to the document itself, and capture all clicks on the page.
For reference, check out the following HTML structure as an example:
<body>
<div id="button-container" style="border: 1px solid black; padding: 20px;">
<button id="main-button">Click Me!</button>
</div>
</body>
Now imagine you click on the button. The button knows it's been clicked, but so does the div element that contains it, and so does the body element that contains the div. This is event propagation. To see this in action, add event listeners to all the elements in the tree:
const button = document.getElementById("main-button");
const container = document.getElementById("button-container");
const body = document.body;
button.addEventListener("click", () => console.log("Event heard on button"));
container.addEventListener("click", () =>
console.log("Event heard on container")
);
body.addEventListener("click", () => console.log("Event heard on body"));
Clicking on the button will trigger all three event listeners. The button, the container, and the body will all log to the console.
You can see this in action by clicking on the button in the following example on CodePen.
Event Propagation Phases
In terms of the DOM tree, the target of the event is the button element. Because that's the element that was clicked.
The event target, plus its ancestors, form a branch in the DOM tree. In this example, the branch is composed of the nodes.
button > div > body > HTML > document > window
The window is not considered a DOM node, and it doesn't function as a traditional 'parent' of the document in the DOM hierarchy. However, in the context of events, the window is central to event handling.
This branch is the path along which an event propagates.
Propagation actually occurs in both directions: like a ripple that bounces off a wall. The full propagation of an event can be divided into three phases:
- Capture Phase: Occurs along the branch, from the
windowto the event target's parent. - Target Phase: Occurs on the event target itself.
- Bubbling Phase: From the event target parent back to the window element.
In the next sections you'll dive into each of these phases in more detail.
The Capture Phase
In the capture phase, where the event travels from the window to the target's parent, only capturer listeners are called. Most listeners are not capturers, so they don't get invoked during this phase.
Capturer listeners are listeners that have been registered with the third parameter of .addEventListener() set to true:
button.addEventListener("click", clickHandler, true);
// ^^^^
The default value of the third parameter is false. As mentioned, most event listeners don't include this parameter, so most listeners don't get invoked during this phase.
The Target Phase
In this phase, all listeners registered on the event.target will be invoked. This is where you will see most listener functions be run.
The Bubbling Phase
During the event bubbling phase all the non capturing listeners are called. Event listeners registered like this:
button.addEventListener("click", clickHandler, false);
button.addEventListener("click", clickHandler);
That's why in the CodePen example, the order of the console logs is:
Event heard on button
Event heard on container
Event heard on body
The event goes through the capture phase, where nothing happens, then the target phase, where the button listener is invoked, and finally the bubbling phase, where the container and body listeners are invoked.
While all events flow down to the event target in the capture phase, focus, blur, load and some others, don’t bubble up. That is, their travel stops after the target phase. While focus and blur do not bubble, they can still be captured if a listener is set during the capturing phase. Similarly, the load event, when used with window, does bubble, but when attached to elements like img, it won't bubble.
So it's important to look up the event you're working with to see how it behaves in detail.
Event Propagation and Event Delegation
Event propagation is the reason why event delegation works. Event delegation a way to attach an event listener to a common parent, then using event.target to tell what element the event happened on.
For example, imagine you have a list of items, and you want to attach a click event listener to each item:
<ul id="button-array">
<li><button>Button 1</button></li>
<li><button>Button 2</button></li>
<li><button>Button 3</button></li>
</ul>
You could do this by looping through the list and attaching a listener to each item:
const buttons = document.querySelectorAll("button");
buttons.forEach((button) => {
button.addEventListener("click", (event) => {
console.log("button clicked!");
console.log(event.target);
});
});
This works, but you have another option.
Alternatively, you can attach a single event listener to the parent element, and then use event.target to tell which child element was clicked:
const buttonArray = document.getElementById("button-array");
buttonArray.addEventListener("click", (event) => {
console.log("Button clicked!");
console.log(event.target);
});
This can be more efficient, because you're only attaching a single event listener. It's also quite flexible, because you can add and remove items from the list without having to worry about attaching and removing event listeners.
Typically, when using event delegation, you'll want to check the event.target property to make sure the event was triggered on the element you want. For example, if you have a list of items, you might want to make sure the event was triggered on the element you want to react to:
const buttonArray = document.getElementById("button-array");
buttonArray.addEventListener("click", (event) => {
if (event.target.tagName === "BUTTON") {
console.log("Button clicked!");
console.log(event.target);
}
});
Otherwise, clicking anywhere in the list will trigger the event listener. Which can lead to behavior such as clicking on the spacing around the button triggers the listener, even though you only want to react to clicks on the button elements.
It's all about finding the right balance. Sometimes it's better to use event delegation, where you have one listener handling a bunch of similar events. Other times, you might want to stick to individual listeners for specific elements, especially if you need more control. Modern browsers can handle quite a few event listeners without breaking a sweat. But, just because they can, doesn't mean you should go wild with it. Slapping thousands of listeners on a page can be overkill and might slow things down, especially on older computers, phones or large applications.
Summary: What Is Javascript Event Propagation
You've delved into the intricacies of JavaScript event propagation, gaining a deeper understanding of how events interact within the DOM. In your exploration, you've learned:
- Event propagation is like a ripple effect in the DOM, indicating that actions on child elements can trigger responses on parent ones, and vice versa.
- There are two main phases of event propagation: the capturing phase (from parent to child) and the bubbling phase (from child to parent), including a target phase where the event is at the target element.
- By default, event listeners are not invoked during the capturing phase unless you explicitly set the third argument in
.addEventListener()totrue. - Event delegation leverages propagation by setting a single listener on a parent element and then determining the target child, making your code more efficient and manageable, especially with dynamic content.
- When using event delegation, you often need to verify that the actual target element is the one you're interested in by checking
event.targetor using methods likeElement.closest()to find the correct element up the DOM tree.