learn
JavaScript

The DOM and events

Capturing, bubbling, delegation, and safe dynamic lists.

Events travel down to their target and bubble back up — and one parent listener using that bubble beats a hundred individual ones. Interviewers ask this because delegation, propagation control and dynamic rows are daily DOM work.

Real-life situation

A to-do list adds rows all day. Binding each row's button misses every future row, so deletes silently stop working after the first add. One listener on the list, reading event.target.closest("li"), handles present and future rows alike — try it live below, including the add button.

Seen in: jQuery's .on() unified direct and delegated binding this way (jQuery 1.7, 2011, on() docs).

Visual

Down and back upbrowser

Capture path

card
page
document
window

Target

buy (target)

Bubble path

empty

Console

—

Click on Buy. The click is born at the button — but listeners on ancestors get their turn too, in two passes.

Step 1 of 4
scenario.js
<main id="page">
  <section id="card">
    <button id="buy">Buy</button>
  </section>
</main>

Try it

Delegated to-do list
javascriptTab moves to the next control
const list = document.querySelector("#todo");
const status = document.querySelector("#status");
let added = 0;
list.addEventListener("click", function (event) {
const item = event.target.closest("li");
if (!item) {
  return;
}
item.remove();
status.textContent = "left: " + list.children.length;
});
document.querySelector("#add").addEventListener("click", function () {
added = added + 1;
const item = document.createElement("li");
item.textContent = "task " + added;
const remove = document.createElement("button");
remove.textContent = "done";
item.appendChild(remove);
list.appendChild(item);
});
console.log("ready:" + list.children.length);
Console output appears here.

Isolated browser frame. Network and imports are disabled.

Click a done button, then add a task and delete it too — no new listeners were ever bound. Then try removing the closest line and click the list padding to see why the guard matters.

When to delegate, when to bind directly

Delegate dynamic lists and repeated controls from a stable parent. Bind directly for one-off controls with no future siblings. Stop propagation only when ancestors must not react; prevent default to cancel the browser action.

No DOM here — guess first, then reveal.

predict.js
console.log(typeof document);

Interview questions

What is the difference between capturing and bubbling?basic

An event travels TWICE: down from window to the target (capturing phase), then back up to window (bubbling phase). Listeners default to bubble-phase; pass { capture: true } to catch the way down. Almost all everyday code uses bubbling (delegation depends on it); capturing is for intercepting events before descendants see them, like global shortcuts or modal traps.

Interviewers also ask

  • How do you listen on the way down?
What is event delegation and why is it better than many listeners?basic

One listener on a parent, using event.target to find which child was actually hit — instead of a listener per child. It is faster to set up, uses constant memory, and automatically covers children added LATER (the killer feature for dynamic lists). The classic demo: one <ul> listener handling delete buttons for rows that don't exist yet. Pair it with closest() to find the relevant ancestor.

Interviewers also ask

  • How do later-added rows work with no new listeners?
What is the difference between stopPropagation and preventDefault?intermediate

stopPropagation halts the event's JOURNEY (no more listeners up or down the tree); preventDefault cancels the browser's DEFAULT ACTION (follow the link, submit the form) while the event keeps traveling. They compose: a validated form calls preventDefault always, stopPropagation only when ancestors must not react. Mixing them up either breaks delegation or lets the page navigate away mid-handler.

Interviewers also ask

  • When would you call both?
What is event.target versus event.currentTarget?basic

target is where the event STARTED (the clicked button); currentTarget is whose listener is RUNNING right now (the <ul> you delegated to). They coincide only without delegation. The standard delegation line reads e.target.closest('li') inside a currentTarget handler — target finds the item, currentTarget anchors the list. Confusing them breaks every delegated handler.

Interviewers also ask

  • Which one do you use inside a delegated handler?
Why do dynamically added elements miss their handlers, and what is the fix?intermediate

Because listeners attach to elements that exist at bind time — rows added later were never bound. Fixes, best first: delegate from a stable parent (future rows included automatically); re-bind after each render (fragile, leaks duplicates); or bind at creation inside the row factory. If an interview demo "works until I add a row", delegation is the expected diagnosis and the expected fix.

Interviewers also ask

  • What goes wrong with re-binding after render?
How do you safely add user content to the page?basic

With textContent (or createElement + textContent), never innerHTML with user strings — innerHTML parses markup, so a username containing script markup or an onerror image runs attacker code (XSS). textContent treats everything as inert text. The rule is absolute for beginners: innerHTML only with developer-written, static markup; everything from users goes through textContent.

Interviewers also ask

  • What attack does innerHTML enable?
What is the difference between input, change and click for form fields?intermediate

input fires on EVERY keystroke (live search, validation-as-you-type); change fires on COMMIT (blur for text, selection for dropdowns and checkboxes — the "final answer" event); click is for buttons and toggles. Debounce belongs on input, saves belong on change/submit. Using click on a text field or change for live feedback are the two classic mismatches.

Interviewers also ask

  • Which event drives a live search box?
How do timers and events interact when the thread is busy?intermediate

They wait. Handlers and timer callbacks are tasks like any other: while your code runs, clicks queue silently and timers go overdue, then everything fires in order once the stack clears. Users experience this as frozen buttons and late updates during heavy work — the same single-thread story as blocking, wearing an events costume. Keep handlers short or yield.

Interviewers also ask

  • Where do clicks go during a long task?
What does { once: true } do on addEventListener?basic

The listener removes ITSELF after its first run — perfect for one-shot onboarding tips, first-click analytics, and any "do this exactly once" wiring. It also prevents the classic leak of forgotten listeners on temporary UI. For repeated handling with automatic cleanup on teardown, pair a normal listener with AbortController's signal instead.

Interviewers also ask

  • How do you remove many listeners at once?
How does the AbortController signal option replace removeEventListener?intermediate

Pass { signal } at bind time; controller.abort() removes every listener sharing that signal in one call — no function-reference bookkeeping, no missed removals. It is the modern teardown for components: one controller per mount, abort on unmount. Bonus: the same signal can cancel the component's fetches, unifying DOM and network cleanup.

Interviewers also ask

  • What else can one signal cancel?

Key takeaways

  • Events capture down, then bubble up; listeners default to bubbling.
  • Delegate from stable parents; read event.target, anchor currentTarget.
  • textContent for user strings; innerHTML only for static markup.

References

Last verified: 2026-10-12.

On this page