Browser History
intermediateModel back/forward/push navigation with an index and a stack — the core of every router.
Implement delegate(root, selector, handler) in JavaScript with closest() and containment checks — bubbling mechanics, the focus/blur trap, and why frameworks delegated for years.
A list has 1,000 rows, each with a delete button. Attaching 1,000 listeners is wasteful — and rows added later have no listener at all. Implement
delegate(root, eventType, selector, handler): one listener on the container that fires the handler as if it were attached to each matching descendant.
This is the DOM question with the best effort-to-signal ratio: the implementation is ten lines, and every line probes a piece of event-system knowledge — bubbling, target vs currentTarget, closest(), and the short list of events that don't bubble. It's also living history: jQuery's .on(sel, fn), and React's synthetic event system, are exactly this pattern.
Events dispatched on a node travel down (capture phase), hit the target, then bubble back up through every ancestor. A listener on the container therefore hears clicks from every descendant — event.target tells you the actual clicked node, event.currentTarget the node the listener is on. Delegation is just: listen high, inspect target, act if it matches.
function delegate(root, eventType, selector, handler) {
const listener = (event) => {
// target may be a deep child (the <svg> INSIDE the button) —
// closest() walks UP to the nearest selector match
const match = event.target.closest(selector);
// Guard: closest() can walk PAST root to an outside match
if (match && root.contains(match)) {
// Call with `this` = the matched element, like a direct listener
handler.call(match, event, match);
}
};
root.addEventListener(eventType, listener);
return () => root.removeEventListener(eventType, listener); // teardown
}const undelegate = delegate(
document.querySelector("#todo-list"),
"click",
"button.delete",
function (event) {
this.closest("li").remove(); // `this` = the matched button
}
);
// Rows added later Just Work — no listener bookkeeping:
list.append(newRow);
// Cleanup on unmount:
undelegate();1. closest(), not === or matches() alone. The click's target is whatever was literally under the cursor — the icon <svg> inside your button, or a <span> inside that. event.target.matches(".delete") fails for the icon click; closest walks up to the nearest matching ancestor, which is the semantics a per-element listener would have had. This is the planted bug: an implementation that works when you click the button's padding and fails when you click its icon.
2. root.contains(match). closest doesn't stop at your container — it walks to the document root. If root is .sidebar and a matching .delete exists outside the sidebar in the ancestor chain (unusual but possible with portals/nesting), you'd fire for foreign elements. The containment check scopes the walk. Rarely hit, cheap to add, and knowing why it's there reads as having shipped this.
3. handler.call(match, ...). Direct listeners run with this = the element; matching that contract makes delegation a drop-in replacement (the this-binding thread). Passing match as an argument too spares arrow-function users.
Delegating focus, blur, mouseenter, or mouseleave silently does nothing — they don't bubble. The fixes:
focus/blur → use their bubbling twins focusin/focusout.mouseenter/mouseleave → delegate mouseover/mouseout and filter with relatedTarget, or reconsider.scroll on elements, media events (play, pause), and load also don't bubble — delegating them needs { capture: true } (capture phase visits ancestors even without bubbling).This is the highest-yield follow-up in the question — "delegate focus for form validation" is the concrete scenario, and focusin is the password.
.on(event, selector, fn) — this exact function, for a decade the dominant UI pattern.document (pre-17) then to the root container (17+); your synthetic onClick never attached a real listener to your button. The React 17 change (document → root) exists because two React apps on one page fought over document-level delegation — a great trivia-with-a-reason.[data-track] beats instrumenting every component.The honest 2026 caveat: with component frameworks managing listeners and engines making addEventListener cheap, manual delegation is less common in app code — its value cases are dynamic lists, non-framework surfaces, and analytics layers. Calibrated relevance beats evangelism.
closest line; the core test.stopPropagation in a descendant's own listener — the event never reaches your container: delegation depends on propagation, and third-party widgets calling stopPropagation are the classic "delegation mysteriously broken" ticket. (This is also the argument against sprinkling stopPropagation casually.)target intact; a second delegated handler running later sees match still valid but detached — root.contains(match) incidentally guards this too..delete.armed after a class toggle needs no re-wiring: matching happens per-event. The whole point, worth stating.event.target becomes the host); delegation across shadow roots sees the host, not the inner element — event.composedPath() is the escape hatch. One sentence of this is rare and impressive.event.target.matches(selector) without closest (fails on child clicks).focus/blur and concluding delegation "doesn't work."target/currentTarget when explaining — the vocabulary is the assessment.(selector, handler) pairs iterated per event — you're now building jQuery's internal event store; connects to the emitter's registry design.scroll, media), and intercepting before target handlers (analytics that must see cancelled events).input events for a form library?" — input/change bubble — yes; combine with debounced validation and you've sketched how form libraries actually wire up.Model back/forward/push navigation with an index and a stack — the core of every router.
Implement on, off, once, and emit — the pub/sub pattern behind Node streams and countless libraries.
Turn real DOM into a plain-object tree — the first half of understanding how React represents UI.
Rebuild real DOM from the virtual tree, completing the render pipeline.