CodingNic

Events

Event Delegation

Events 20 min read

Event Delegation

Objectives

By the end of this chapter, you should be able to:

  • Explain the problem event delegation solves
  • Attach one listener to a parent that handles events for its children, including children added later
  • Use event.target and .closest() to identify which child was actually involved

💡 Why this matters: Real pages add and remove elements constantly, a new list item, a new row, a new card. A listener attached directly to one of those elements only works for elements that already existed when you attached it. Delegation fixes that.

The Problem

Given this HTML:

text
<ul id="list">
  <li class="item">Apples</li>
  <li class="item">Bread</li>
</ul>

You could attach a click listener to each .item individually:

javascript
document.querySelectorAll(".item").forEach((item) => {
  item.addEventListener("click", () => {
    console.log("Clicked:", item.textContent);
  });
});

This works for the two items that exist right now. But if you later add a third <li class="item"> to the list, it has no listener attached, clicking it does nothing, since the .forEach() loop already finished running before that element existed.

The Fix: Listen on the Parent

Because events bubble (previous lesson), a click on any <li> inside #list also fires on #list itself. Instead of attaching a listener to every item, attach one listener to the parent, and use event.target to figure out which child was actually clicked.

javascript
const list = document.getElementById("list");

list.addEventListener("click", (event) => {
  const item = event.target.closest(".item");
  if (!item) return;

  console.log("Clicked:", item.textContent);
});

event.target is the exact element the click happened on, which might be the <li> itself, or something nested inside it. .closest(".item") walks upward from event.target until it finds an element matching .item, so this works correctly even if an <li> contains other markup. The if (!item) return; guard matters too, without it, a click anywhere inside #list (including empty space, if there is any) would try to run the handler with item as null.

Now, adding a new item after the listener was attached still works, since the listener lives on #list, not on the item:

javascript
const newItem = document.createElement("li");
newItem.className = "item";
newItem.textContent = "Milk";
list.appendChild(newItem);

newItem.click();
// Clicked: Milk

No new listener was added for newItem. Clicking it still logs correctly, because the click bubbled up to #list, where the one listener has been waiting the whole time.

Try It

Starter HTML for all three exercises:

text
<ul id="tasks">
  <li class="task">Write code</li>
  <li class="task">Test code</li>
</ul>

<div id="gallery">
  <img class="thumb" src="cat.jpg" alt="Cat">
  <img class="thumb" src="dog.jpg" alt="Dog">
</div>
  1. Attach one click listener to #tasks that logs the clicked task’s .textContent, using event.target.closest(".task").
  2. Add a new <li class="task"> to #tasks after attaching the listener, then click it and confirm the listener still fires.
  3. Attach one click listener to #gallery that logs the clicked thumbnail’s src attribute, using event.target.closest(".thumb").

Recap

  • Attaching a listener to every individual element misses any element added later.
  • Delegation attaches one listener to a stable parent and relies on bubbling: a click on any current or future child still reaches the parent.
  • event.target is the exact element clicked. .closest(selector) walks upward from it to find the meaningful element you actually care about, guarding against null when the click didn’t land on a match.

Next lesson: dispatching your own custom events, for when the built-in ones don’t cover what you need.