7.5 — Bubbling, delegation & preventDefault
Last lesson ended on a cliffhanger: an event doesn't just hit its target — it travels through the whole tree on the way there and back. Today: the full journey, and the single most useful trick it quietly enables — delegation: one listener instead of a hundred.
Plus a separate tool that gets confused with it constantly: preventDefault(), which stops the browser's own reaction to an event — a link navigating, a form submitting — and has nothing to do with the event's travel at all.
const list = document.querySelector("#list");
list.addEventListener("click", (event) => {
console.log("heard on: " + event.currentTarget.id);
console.log("actually clicked: " + event.target.tagName);
if (event.target.matches(".delete")) {
event.target.closest("li").remove();
}
});
// <ul id="list">
// <li>milk <button class="delete">×</button></li>
// <li>eggs <button class="delete">×</button></li>
// </ul>A click lands on the delete button, nested inside an li, inside a ul. Does ONLY the button react — or do its ancestors get a say too?
The deeper story, with the real names for things — this part is what turns “I saw it” into “I can explain it.”
Precision for interviews: addEventListener's rare third argument (true or { capture: true }) opts a listener INTO the capture phase instead of bubble. Almost every real listener uses the default — bubble — which is exactly why delegation works at all: it needs the event to still be traveling upward when it arrives.
event.currentTarget is only meaningful WHILE the handler is actively running — the DOM reassigns it fresh at each node the event visits. Save the whole event object for later and read currentTarget then, and you'll find it's gone (usually null). Read it synchronously, inside the handler, always.
Job note: Playwright's .click() dispatches a real, trusted click event that bubbles exactly like a human's would — which is precisely why delegated listeners in the app you're testing keep firing correctly under automation, with zero special handling.
⌨️ delegate, don’t multiply
Build the delegation pattern in miniature: ONE handler that receives which child fired and removes it — modeled with plain objects and arrays (no real DOM in this sandbox).
requirements:
- An array
itemsof objects:{ id: "milk" },{ id: "eggs" },{ id: "bread" }. - A function
handleDelegatedClick(items, clickedId): return a NEW array with the matching item removed (4.9’s filter — one function, works for ANY id). - Call it with
"eggs", print the remaining ids joined by", ". Then call it AGAIN on that result with"bread"— the SAME function handling a completely different click, proving delegation needs no per-item logic. Print the final ids.
when you press RUN, the console must show exactly:
✏️ Quick check 1
In the handler, event.target is the button. Type exactly what event.target.tagName prints:
✏️ Quick check 2
Does stopPropagation() also stop the browser’s default action (like a link navigating)? Type yes or no.
✏️ Quick check 3
A brand-new <li> is added to the list AFTER the listener was wired to the ul. Does clicking its delete button still work, with no new listener? Type yes or no.
🗣️ Now teach it back
Explain to a friend the three phases every event travels through, what delegation is and why it actually works, and the difference between stopPropagation and preventDefault.
Write it as if your friend is sitting next to you. Saved to your journal — future-you will use these notes to teach others.