Events, Event flow and Event delegation in Javascript

supraja
6 min readJan 1, 2024

--

image credit: link

Events:

Events in JavaScript enable the execution of code in response to specific occurrences, such as a user clicking a button.

The essence of events lies in their ability to transport data. When an event occurs, an object containing information about the event is created. This object is then passed as an argument to the event handler function. This mechanism facilitates easy access to event data, empowering developers to respond effectively to various user interactions.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Basic Event Handling</title>
</head>
<body>

<button id="myButton">Click me!</button>

<script>
// Event handling
document.getElementById('myButton').addEventListener('click', function (event) {
alert('Button clicked!');
});
</script>

</body>
</html>

In this example, we have a button (<button>) with the ID myButton. We use JavaScript to attach a click event listener to the button. When the button is clicked, the anonymous function inside the addEventListener method is executed, showing an alert.

addEventListener Parameters:

The addEventListener method in JavaScript has three parameters:

  1. The event type to listen for.
  2. The function to be called when the event occurs.
  3. An optional boolean value (useCapture):
  • If true, the event uses the capturing phase.
  • If false or omitted, the event uses the bubbling phase.

Don’t worry if you don’t know about these phases, you will learn about them in this blog :)

Accessing Event Properties:

  • event.target: Represents the element where the event will ultimately be dispatched.
  • event.currentTarget (or this): The current element handling the event.
  • event.eventPhase: Indicates the current phase (1 for capturing, 2 for target, 3 for bubbling).
<!DOCTYPE HTML>
<html>
<body>
A click shows both <code>event.target</code> and <code>this</code> to compare:

<form id="form">FORM
<div>DIV
<p>P</p>
</div>
</form>

<script>
form.onclick = function(event) {
setTimeout(() => {
alert("target = " + event.target.tagName + ", this=" + this.tagName);
event.target.style.backgroundColor = ''
}, 0);
};
</script>
</body>
</html>

In the above example, with “onclick” event of <form> element:

  • When clicked on<p>(form’s child), the output is: “target = P, this=FORM”
  • When clicked on<div>(form’s child), the output is: “target = DIV, this=FORM”
  • When clicked on<form>, the output is: “target = FORM, this=FORM”

Stopping the Event:

  • Event handlers can stop the event propagation using event.stopPropagation(). However, it's generally not recommended unless absolutely necessary, as it can interfere with other parts of the application.

Event Flow Phases:

(1) Capturing Phase:

  • Events start from the root of the DOM hierarchy and move down to the target element. This phase is known as the capturing phase.
  • Handlers assigned with addEventListener(..., true) (or {capture: true}) are called along the way.

(2) Target Phase:

  • The event reaches the target element. This is the phase where the event type is associated with the target element.
  • Handlers are called on the target element itself.

(3) Bubbling Phase:

  • After the target phase, the event bubbles up from the target element to the root of the DOM hierarchy.
  • It is a process where, upon an event (e.g., a click) on a specific DOM element, the event doesn’t halt at the target element. Instead, it continues to propagate upward through its ancestor elements, reaching the root of the DOM. This upward propagation allows multiple elements in the hierarchy to respond to the same event.
  • Handlers assigned using on<event>, HTML attributes, and addEventListener without the 3rd argument or with the 3rd argument set to false ({capture:false}) are called.

In summary, the event starts at the most nested element (the target) and follows a path down to the target during the capturing phase(calls handlers if useCaptureis true), then calls handlers on the target itself during the target phase, and finally, it bubbles up, calling handlers along the way during the bubbling phase(if handlers exists with useCapture as false or without it’s value assigned).

In the example above, for a click on <td> the event first goes through the ancestors chain down to the element (capturing phase), then it reaches the target and triggers there (target phase), and then it goes up (bubbling phase), calling handlers on its way.

Example of Event Capturing and Bubbling:

<form>FORM
<div>DIV
<p>P</p>
</div>
</form>

<script>
for(let elem of document.querySelectorAll('*')) {
elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}`), true);
elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}`));
}
</script>

The above code sets click handlers on every element in the document to see which ones are working.

If you click on <p>, then the sequence is:

  1. HTMLBODYFORMDIV -> P (capturing phase, the first listener):
  2. PDIVFORMBODYHTML (bubbling phase, the second listener).

Please note, the P shows up twice, because we’ve set two listeners: capturing and bubbling. The target triggers at the end of the first and at the beginning of the second phase.

There’s a property event.eventPhase that tells us the number of the phase on which the event was caught. But it’s rarely used, because we usually know it in the handler.

To remove the handler, removeEventListener needs the same phase:
If we addEventListener(..., true), then we should mention the same phase in removeEventListener(..., true) to correctly remove the handler.

Listeners on the same element and same phase run in their set order:
If we have multiple event handlers on the same phase, assigned to the same element with addEventListener, they run in the same order as they are created:

elem.addEventListener("click", e => alert(1)); // guaranteed to trigger first
elem.addEventListener("click", e => alert(2));

The event.stopPropagation() during the capturing also prevents the bubbling:
The event.stopPropagation() method and its sibling event.stopImmediatePropagation() can also be called on the capturing phase. Then not only the futher capturing is stopped, but the bubbling as well.
In other words, normally the event goes first down (“capturing”) and then up (“bubbling”). But if event.stopPropagation() is called during the capturing phase, then the event travel stops, no bubbling will occur.

Let’s understand event.stopPropagation()with examples below: (you can skip to “Event delegation” section below, if you wish to)

<!DOCTYPE html>
<html lang="en">
<body>
<div id="outer" onclick="handleClick('outer')">
Outer Div
<div id="middle" onclick="handleClick('middle')">
Middle Div
<div id="inner" onclick="handleClick('inner')">
Inner Div
</div>
</div>
</div>

<script>
function handleClick(elementId) {
console.log(`Clicked on ${elementId}`);
}

document.getElementById('outer').addEventListener('click', function (event) {
handleClick('outer capturing');
event.stopPropagation(); // Stops further propagation, including bubbling
}, true);

document.getElementById('middle').addEventListener('click', function (event) {
handleClick('middle capturing');
}, true);

document.getElementById('inner').addEventListener('click', function (event) {
handleClick('inner capturing');
}, true);
</script>

</body>
</html>

For above example, output is always: “Clicked on outer capturing”, for all the <div> “on click” events. Because outer<div> stops propagation and bubbling for all it’s child elements as well.

<script>
function handleClick(elementId) {
console.log(`Clicked on ${elementId}`);
}
document.getElementById('outer').addEventListener('click', function (event) {
handleClick('outer capturing');
}, true);
document.getElementById('middle').addEventListener('click', function (event) {
handleClick('middle capturing');
event.stopPropagation(); // Stops further propagation, including bubbling
}, true);
document.getElementById('inner').addEventListener('click', function (event) {
handleClick('inner capturing');
}, true);
</script>

In the updated script above, whereevent.stopPropagation() is present in middle <div>, event propogation stops at middle div:
1. on clicking on outer <div>, the output is:
Clicked on outer capturing
Clicked on outer”

2. on clicking on middle <div>, the output is:
Clicked on outer capturing
Clicked on middle capturing

3. on clicking on inner <div>, the output is:
Clicked on outer capturing
Clicked on middle capturing”

Note: If event.stopPropagation() is not present in handler,
on clicking on middle <div>, the output is:
“Clicked on outer capturing
Clicked on middle capturing
Clicked on middle
Clicked on outer”

Event Delegation:

Event delegation is a technique where a single event listener is attached to a common ancestor of multiple elements. Instead of attaching event listeners to each individual element, you leverage event bubbling to handle events on a higher-level ancestor. This is especially useful when dealing with dynamically created elements or a large number of similar elements.

Example of Event Delegation:

<!DOCTYPE html>
<html lang="en">
<body>
<ul id="taskList">
<li>Task 1</li>
<li>Task 2</li>
<li>Task 3</li>
<li>Task 4</li>
</ul>
<script>
// Event delegation with a single event listener on the common ancestor
document.getElementById('taskList').addEventListener('click', function (event) {
if (event.target.tagName === 'LI') {
// Check if the clicked element is an <li> element
console.log('Task clicked:', event.target.textContent);
// you can add your handling logic here
}
});
</script>
</body>
</html>

In this example, we attach a single click event listener to the <ul> element, which is the common ancestor of the <li> elements. When a <li> element is clicked, the event bubbles up, and the listener checks if the clicked element is an <li>. If true, it logs the task content to the console. This way, we handle multiple <li> elements with a single event listener.

--

--