JS Event Propagation

Understanding Event Propagation

Event propagation refers to how events move through the DOM tree to reach their target and what happens to them afterward.

Let's illustrate this with an example: imagine you've set a click event handler on a hyperlink (<a> element) nested within a paragraph (<p> element). When you click the link, the handler executes. If you instead assign the click event handler to the paragraph containing the link, clicking the link will still trigger the handler. This happens because events don't just impact the element that triggered them—they traverse up and down the DOM tree to find their target. This is known as event propagation.

Modern browsers handle event propagation in two phases: capturing and bubbling. Before we delve deeper, take a look at this illustration:

Event Propagation Demo

The image above shows how events travel through the DOM tree during different phases of event propagation when an event is triggered on an element with parent elements.

The concept of event propagation was introduced to manage scenarios where multiple elements in the DOM hierarchy, with parent-child relationships, have event handlers for the same event (like a mouse click). The question arises: when the user clicks the inner element, which element's click event is handled first—the outer or the inner element's?

In the subsequent sections of this chapter, we'll explore each phase of event propagation in detail and answer this question.

 

Note: Event propagation formally consists of 3 phases: capture, target, and bubble. However, in modern browsers, the 2nd phase—the target phase (when the event reaches the target element that triggered it)—is not handled separately. Handlers registered for both capturing and bubbling phases are executed during this phase.

The Capturing Phase

In the capturing phase, events move from the Window down through the DOM tree to the target node. For instance, when a user clicks on a hyperlink, the click event traverses through the <html> element, the <body> element, and the <p> element containing the link.

If any ancestor (such as a parent or grandparent) of the target element, as well as the target itself, has a specifically registered capturing event listener for that event type, those listeners are triggered during this phase. See the example below:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Event Capturing Demo</title>
<style type="text/css">
div, p, a{
padding: 15px 30px;
display: block;
border: 2px solid #000;
background: #fff;
}
</style>
</head>
<body>
<div id="wrap">DIV
<p class="hint">P
<a href="#">A</a>
</p>
</div>

<script>
function showTagName() {
alert("Capturing: "+ this.tagName);
}

var elems = document.querySelectorAll("div, p, a");
for(let elem of elems) {
elem.addEventListener("click", showTagName, true);
}
</script>
</body>
</html>

Here's a straightforward demonstration that illustrates how event capturing functions. Click on any element and observe the order in which the alert pop-ups appear.

<div id="wrap">

<p class="hint"> <a href="#">Click Me</a> </p>

</div>

Event capturing is not universally supported by all browsers and is seldom used. For example, Internet Explorer versions prior to 9.0 do not support event capturing.

Additionally, event capturing only functions with event handlers registered using the addEventListener() method when the third argument is set to true. Traditional methods of assigning event handlers, such as using onclick, onmouseover, etc., do not apply here. Refer to the JavaScript event listeners chapter to delve deeper into event listeners.


The Bubbling Phase

In contrast to the capturing phase, during the bubbling phase, events propagate or bubble back up the DOM tree from the target element to the Window, visiting all ancestors of the target element along the way. For instance, if a user clicks a hyperlink, the click event would pass through the <p> element containing the link, then the <body> element, followed by the <html> element, and finally reach the document node.

Furthermore, if any ancestor of the target element, including the target itself, has event handlers assigned for that type of event, those handlers are executed during this phase. In modern browsers, all event handlers are registered by default in the bubbling phase. Here's an example:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Event Bubbling Demo</title>
<style type="text/css">
div, p, a{
padding: 15px 30px;
display: block;
border: 2px solid #000;
background: #fff;
}
</style>
</head>
<body>
<div onclick="alert('Bubbling: ' + this.tagName)">DIV
<p onclick="alert('Bubbling: ' + this.tagName)">P
<a href="#" onclick="alert('Bubbling: ' + this.tagName)">A</a>
</p>
</div>
</body>
</html>

Here's a basic demonstration we've created to illustrate how event bubbling works. Click on any element and observe the sequence in which alert pop-ups appear.

<div id="wrap">

<p class="hint"> <a href="#">Click Me</a> </p>

</div>

Event bubbling is universally supported by all browsers and applies to all handlers, regardless of how they are registered (e.g., using onclick or addEventListener()), unless specifically registered as a capturing event listener. Hence, the term "event propagation" is often used synonymously with event bubbling.


Accessing the Target Element

The target element refers to the DOM node that initiated the event. For instance, if a user clicks on a hyperlink, the target element would be the hyperlink itself.

The target element can be accessed using event.target, and it remains unchanged throughout the event propagation phases. Additionally, the this keyword refers to the current element (i.e., the element to which the currently executing handler is attached). Here's an example:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Event Target Demo</title>
<style type="text/css">
div, p, a{
padding: 15px 30px;			
display: block;
border: 2px solid #000;
background: #fff;
}
</style>
</head>
<body>
<div id="wrap">DIV
<p class="hint">P
<a href="#">A</a>
</p>
</div>

<script>
// Selecting the div element
var div = document.getElementById("wrap");

// Attaching an onclick event handler
div.onclick = function(event) {
event.target.style.backgroundColor = "lightblue";

// Let the browser finish rendering of background color before showing alert
setTimeout(() => {
alert("target = " + event.target.tagName + ", this = " + this.tagName);
event.target.style.backgroundColor = ''
}, 0);
}
</script>
</body> 
</html>

Here's a straightforward demonstration we've created based on the example above. Click on any element to see the tag name of the target element and the current element.

<div id="wrap">

<p class="hint"> <a href="#">Click Me</a> </p>

</div>

The fat arrow (=>) used in the example above is an arrow function expression. It offers a concise syntax compared to traditional function expressions and ensures proper behavior of the this keyword. For more details on arrow functions, refer to the tutorial on ES6 features.


Stopping Event Propagation

You can halt event propagation if you need to prevent any ancestor element's event handlers from receiving notification about the event.

For instance, if you have nested elements with each having an onclick event handler that displays an alert dialog box, clicking on the inner element would normally trigger all handlers simultaneously as the event bubbles up the DOM tree.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Event Propagation Demo</title>
<style type="text/css">
div, p, a{
padding: 15px 30px;
display: block;
border: 2px solid #000;
background: #fff;
}
</style>
</head>
<body>
<div id="wrap">DIV
<p class="hint">P
<a href="#">A</a>
</p>
</div>

<script>
function showAlert() {
alert("You clicked: "+ this.tagName);
}

var elems = document.querySelectorAll("div, p, a");
for(let elem of elems) {
elem.addEventListener("click", showAlert);
}
</script>
</body>
</html>

Here's a straightforward demonstration we've created based on the example above. If you click on any child element, event handlers on parent elements are also triggered, potentially displaying multiple alert boxes.

<div id="wrap">

<p class="hint"> <a href="#">Click Me</a> </p>

</div>

To prevent this situation, you can stop the event from bubbling up the DOM tree using the event.stopPropagation() method. In the following example, the click event listener on the parent elements will not execute if you click on the child elements.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Stop Event Propagation Demo</title>
<style type="text/css">
div, p, a{
padding: 15px 30px;
display: block;
border: 2px solid #000;
background: #fff;
}
</style>
</head>
<body>
<div id="wrap">DIV
<p class="hint">P
<a href="#">A</a>
</p>
</div>

<script>
function showAlert(event) {
alert("You clicked: "+ this.tagName);
event.stopPropagation();
}

var elems = document.querySelectorAll("div, p, a");
for(let elem of elems) {
elem.addEventListener("click", showAlert);
}
</script>
</body>
</html>

Here's the updated demonstration. Now, if you click on any child element, only one alert will appear.

<div id="wrap">

<p class="hint"> <a href="#">Click Me</a> </p>

</div>

Additionally, you can prevent any other listeners attached to the same element for the same event type from being executed using the stopImmediatePropagation() method.

In the following example, we've attached multiple listeners to the hyperlink, but only one listener for the hyperlink will execute when you click the link, and you will see only one alert.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Stop Immediate Propagation Demo</title>
<style type="text/css">
div, p, a{
padding: 15px 30px;
display: block;
border: 2px solid #000;
background: #fff;
}
</style>
</head>
<body>
<div onclick="alert('You clicked: ' + this.tagName)">DIV
<p onclick="alert('You clicked: ' + this.tagName)">P
<a href="#" id="link">A</a>
</p>
</div>

<script>
function sayHi() {
alert("Hi, there!");
event.stopImmediatePropagation();
}
function sayHello() {
alert("Hello World!");
}

// Attaching multiple event handlers to hyperlink
var link = document.getElementById("link");
link.addEventListener("click", sayHi);  
link.addEventListener("click", sayHello);
</script>
</body>
</html>

Note: When multiple listeners are attached to the same element for the same event type, they are executed in the order they were added. However, if any listener calls the event.stopImmediatePropagation() method, subsequent listeners for that event type on the same element will not be executed.


Preventing the Default Action

Some events come with default actions. For instance, clicking on a link typically navigates to its target, and clicking on a form submit button submits the form. You can override these default actions using the preventDefault() method available on the event object.

It's important to note that preventing the default action does not stop event propagation. The event will still propagate through the DOM tree as usual. Here's an example:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Prevent Default Demo</title>
</head>
<body>
<form action="/examples/html/action.php" method="post" id="users">
<label>First Name:</label>
<input type="text" name="first-name" id="firstName">
<input type="submit" value="Submit" id="submitBtn">
</form>

<script>
var btn = document.getElementById("submitBtn");

btn.addEventListener("click", function(event) {
var name = document.getElementById("firstName").value;
alert("Sorry, " + name + ". The preventDefault() won't let you submit this form!");
event.preventDefault(); // Prevent form submission
});
</script>
</body>
</html>