What you'll be able to do
By the end of this page you should be able to attach one listener on a list (or table) parent, detect which row or button was clicked via event.target, and add new rows without wiring new handlers.
Previous post: event bubbling and capturing.
Who this is for
- People who loop
querySelectorAll("button")and addListener on every render - Beginners with dynamic todo lists
- Anyone whose handler count grows with list length
You can skip this if delegation is already your default for lists. Come back when re-rendering duplicates listeners.
Pattern: listen on parent, filter target
const list = document.querySelector("#tasks");
list.addEventListener("click", function (event) {
const btn = event.target.closest("button.delete");
if (!btn) return;
const row = btn.closest("li");
row.remove();
});
Scenario A — delete buttons on dynamic rows. One listener on ul.
Scenario B — clicks on icons inside button. closest finds the button even if target is a span inside.
closest: walk up to a match
event.target.closest("[data-action]");
Scenario A — data-action="edit" on a button (see post #49).
Scenario B — ignore clicks on padding — if target is ul, closest("button") returns null; you return early.
Why delegation beats 100 listeners
| Approach | Dynamic rows | Memory |
|---|---|---|
| Listener per row | re-bind after each render | grows with N |
| One delegated listener | new rows work automatically | constant |
Bubble brings the event to the parent — you already learned why that happens.
A tiny practice file
Save as delegate.html.
<!DOCTYPE html>
<html>
<body>
<button id="add">Add row</button>
<ul id="list"></ul>
<script>
let n = 0;
document.getElementById("add").addEventListener("click", function () {
n += 1;
const li = document.createElement("li");
li.innerHTML = 'Item ' + n + ' <button type="button" class="del">×</button>';
document.getElementById("list").append(li);
});
document.getElementById("list").addEventListener("click", function (e) {
const del = e.target.closest("button.del");
if (del) del.closest("li").remove();
});
</script>
</body>
</html>
Mistakes I see a lot
1. Not checking closest result before using it.
2. Delegating on document too early — fine for apps, but start with the smallest container.
3. stopPropagation on child that breaks delegation upstream.
4. Forgetting type="button" inside forms — default submit surprises.
What to try before the next post
- One listener removes any
.delrow. - Add rows after load; delete still works.
- Log when click misses a button (early return).
- Build
delegate.html.
Next in this series: DOMContentLoaded, defer, and async — script order without guessing.
Try this next outside the series
Browser JavaScript stops feeling isolated once you connect it to forms and component-oriented UI work.
- React forms without pain — see modern form handling after you understand the DOM version first
- HTML to JSX Converter — move tiny HTML snippets into React-style markup when you're ready