JavaScript Event Delegation and Advanced Events
This lesson goes deeper into events: how they travel through the page, and how one listener can handle many elements.
Event Bubbling
When you click a button, the click does not stay on the button. It first happens on the button, then on its parent, then on the parent's parent, all the way up to document. This is called bubbling.
Example
<!DOCTYPE html>
<html>
<body>
<div id="outer" style="padding:20px;background:#eee">
Outer box
<p id="inner" style="padding:10px;background:#f0db4f">
<button id="btn">Click me</button>
</p>
</div>
<p id="log"></p>
<script>
const log = document.getElementById("log");
document.getElementById("btn").addEventListener("click", () => log.innerHTML += "button<br>");
document.getElementById("inner").addEventListener("click", () => log.innerHTML += "paragraph<br>");
document.getElementById("outer").addEventListener("click", () => log.innerHTML += "outer div<br>");
</script>
</body>
</html>Try it Yourself »button paragraph outer div
One click on the button runs all three listeners, from the inside out.
target and currentTarget
Inside a listener, the event object tells you two different things:
| Property | Meaning |
|---|---|
event.target | The element that was actually clicked (the deepest one) |
event.currentTarget | The element whose listener is running now |
If you click the button in the example above, the outer listener still sees event.target as the button, while event.currentTarget is the outer div.
Event Delegation
Imagine a list of 100 products, each with a "Buy" button. You could add 100 listeners. A smarter way is to add one listener to the list, and check which button was clicked. This is event delegation.
Delegation has two big benefits:
- Fewer listeners, so less memory and simpler code
- It works for items added later, because the listener is on the parent, not on each item
Example
<!DOCTYPE html>
<html>
<body>
<h3>Ali's Fruit Stall</h3>
<ul id="fruits">
<li>Mango - Rs 300/kg <button data-name="Mango">Buy</button></li>
<li>Banana - Rs 180/dozen <button data-name="Banana">Buy</button></li>
</ul>
<button id="addItem">Add Guava</button>
<p id="msg"></p>
<script>
const list = document.getElementById("fruits");
list.addEventListener("click", (event) => {
const button = event.target.closest("button");
if (!button) return; // the click was not on a button
document.getElementById("msg").textContent = "You bought: " + button.dataset.name;
});
document.getElementById("addItem").addEventListener("click", () => {
const li = document.createElement("li");
li.innerHTML = 'Guava - Rs 200/kg <button data-name="Guava">Buy</button>';
list.append(li);
});
</script>
</body>
</html>Try it Yourself »Click "Add Guava", then click its new "Buy" button. It works, even though no listener was added to it.
event.target.closest("button") finds the nearest button around the clicked element (or the element itself). This matters when a button has an icon or a <span> inside it. The data-name attribute is read with button.dataset.name.
Stopping Bubbling
Sometimes you do not want a click to reach the parent. event.stopPropagation() stops the event from going any higher.
Example
card.addEventListener("click", () => openCard());
deleteButton.addEventListener("click", (event) => {
event.stopPropagation(); // do not open the card too
deleteCard();
});Do not confuse the two: preventDefault() stops the browser's normal action (like following a link), while stopPropagation() stops the event from bubbling to parents.
Listener Options
addEventListener takes a third argument, an options object:
| Option | What it does |
|---|---|
once: true | The listener runs one time, then removes itself |
capture: true | Runs on the way down (before bubbling) |
passive: true | Promises you will not call preventDefault(), so scrolling stays smooth |
Example
<!DOCTYPE html>
<html>
<body>
<button id="offer">Claim your free gift</button>
<p id="demo"></p>
<script>
document.getElementById("offer").addEventListener("click", () => {
document.getElementById("demo").textContent = "Gift claimed! This button only works once.";
}, { once: true });
</script>
</body>
</html>Try it Yourself »Custom Events
You can create your own events with new CustomEvent(name, { detail }) and fire them with dispatchEvent(). The detail property carries any data you like.
Example
<!DOCTYPE html>
<html>
<body>
<button id="add">Add to cart</button>
<p id="badge">Cart: 0</p>
<script>
let count = 0;
document.addEventListener("cart:added", (event) => {
count += event.detail.qty;
document.getElementById("badge").textContent = "Cart: " + count;
});
document.getElementById("add").addEventListener("click", () => {
document.dispatchEvent(new CustomEvent("cart:added", { detail: { qty: 1 } }));
});
</script>
</body>
</html>Try it Yourself »Custom events help different parts of a page talk to each other without knowing about each other.
Exercise
Exercise
You add one click listener to a <ul> and check event.target to see which <li> was clicked. What is this technique called?
