Event Delegation

The Course
•
4 min read

One listener can serve changing rows

When a row is rebuilt, its old DOM element is gone. Creating a new listener callback and registering it on a long-lived container on every render accumulates handlers. Re-registering the exact same function with the same event type and capture setting is ignored, but a newly created arrow function is a different listener. A useful pattern is event delegation: register one listener on the list and identify the clicked row’s action.

Click events normally bubble from the target up through its ancestors. event.target identifies the originating node; event.currentTarget is the element whose listener is currently running. closest("button[data-id]") finds a matching button at or above a nested target, such as a span inside the button. A data-id HTML attribute is read as button.dataset.id, always text.

Guard the target before calling element methods: event.target instanceof Element asks whether it is an element. instanceof checks an object’s relationship to a constructor, not a string type name. Our capstone uses this on DOM events and Error values; it does not replace validation of unknown JSON.

Use the data id to look up a validated record in state. Do not read a title out of the row and treat it as the record. Titles may repeat, text may change, and displayed values are formatted. On removal, move focus to a stable control so keyboard users are not left on a disappeared button. Keep user text in textContent.

Predict and trace

Write the output before running. Follow the values through each call; if your prediction differs, identify the first step that surprised you.

Predict, then run JavaScript
function findAction(books, dataset) {
  return books.find(book => book.id === dataset.id);
}

const books = [
  { id: "js", title: "Notes" },
  { id: "web", title: "Notes" }
];

console.log(findAction(books, { id: "web" }).id);
console.log(findAction(books, { id: "missing" }) === undefined);
Check your prediction
web
true

Two books have the same title, so the id determines which one the action belongs to.

Practise on a changing list

Press Run webpage to preview. Each run starts fresh. Edits are not saved automatically.

Press Run webpage. Click the nested Remove text on the first row, then remove the second row using Tab and Enter. Reset the list, then remove again. The same list listener should still work after rows are rebuilt. The heading and status are outside the list; clicking them must not remove anything.

button[data-id] is a selector for a button carrying a data-id attribute. closest searches the clicked element and its ancestors for that match. The HTML contains a span inside each button so you can see why the click target is not always the button itself. event.currentTarget remains the list that owns the listener.

The render function creates nodes and assigns textContent, as the beginner planner did. Keep the removal rule from your console exercise separate from those DOM operations. As a variation, add the restored-row count to the reset message and verify repeated resets do not multiply the number of handled actions.

Build it yourself

Exercise

Event Delegation: practise

Write removeById(books, id), preserving the input. Print the remaining id and original length. Treat an unknown id as a no-op in value.

Your working space JavaScript
const books = [{ id: "js" }, { id: "web" }];
Hint 1: the idea

Filter based on identity, not title or position.

Hint 2: a first step

Keep records whose id differs from the requested id.

Show one solution
function removeById(books, id) {
  return books.filter(book => book.id !== id);
}

const books = [{ id: "js" }, { id: "web" }];

console.log(removeById(books, "js")[0].id);
console.log(books.length);
web
2

Remove from an empty array, then try a missing id. In the final lab, click Save and Remove using Tab and Enter, and inspect where focus lands.

Test a changed requirement

Remove from an empty array, then try a missing id. In the final lab, click Save and Remove using Tab and Enter, and inspect where focus lands.

Explain before moving on

Where should a delegated listener be registered? Why does closest handle a nested button label better than reading target.dataset directly?

Was this article helpful?

Your response is saved on this device.