A table of two hundred rows, a delete button on each one. Placing two hundred listeners works, but it costs memory, and it does not cover the row added three seconds later.
There is a solution that fits in a single listener, and it rests entirely on the way events bubble.
Definition
Event delegation means listening on a shared ancestor rather than on each element, then identifying at trigger time the element actually aimed at. It works because an event climbs from its target all the way to the root of the document.
const table = document.querySelector("#orders");
table.addEventListener("click", (event) => {
const button = event.target.closest(".delete");
if (!button) return;
const row = button.closest("tr");
row.remove();
});One listener, whatever the number of rows. Rows inserted later are covered with no extra code at all, since nothing is ever wired to them.
closest, the key piece
event.target names the deepest element touched by the gesture, often an icon or a piece of text inside the button. Comparing that node directly with the expected selector therefore fails half the time.
closest climbs from an element toward its ancestors and hands back the first one matching the selector, the starting element included. It hands back null when no ancestor matches, which makes a natural filter.
// <button class="delete"><span class="icon"></span> Delete</button>
table.addEventListener("click", (event) => {
console.log(event.target.tagName); // "SPAN" sometimes
console.log(event.target.closest(".delete")); // always the button
});What it buys
- Dynamic content works with no re-registration after each added row.
- Memory stops growing with the number of elements on display.
- Cleanup comes down to a single listener to remove when the view goes away.
The data-* attribute, read through dataset, completes the picture: it carries on each row the identifier to send to the server, which the listener picks up at click time.
Its limits
The technique only applies to events that bubble. focus and blur stay on their target: their twins focusin and focusout do bubble and can be delegated normally.
Listen on the nearest container, not on document. A global listener sees every click in the page go by, and one stopPropagation call placed lower down is enough to silence it with no explanation.
Frequently asked questions
Should everything be delegated?
No. For a single button, a direct listener is shorter and reads better. Delegation takes the lead on a collection of similar elements, or as soon as the content changes while the page is in use.
How do you handle several actions in the same container?
One listener is still enough. Test the possible targets one after another with closest, or read a data-action attribute and route the work with a switch. That second form stays readable even with a dozen actions.
Does it cover the keyboard?
It does when the HTML is correct. A real <button> fires a click event on the Enter key as well as on the space bar, and that event bubbles. A <div> turned clickable offers nothing of the sort and shuts out part of the audience.