Section 4 · JavaScript in the Browser
Events and Event Delegation
17 min3 min readCode playground
Lesson at a glance
Respond to clicks and key presses with addEventListener, read the event object, understand bubbling, and handle a whole list with one delegated listener that keeps working as items come and go.
- 1Listening for an event
- 2Data first, then render
- 3Bubbling: events travel up the tree
- 4Event delegation: one listener for the whole list
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
Pocket can draw its list. Now people want to do things to it: delete an expense, mark one as reviewed, filter by category. Each of those starts with the user doing something, such as clicking or typing, and your code reacting. The browser reports everything users do as events.
Listening for an event
You ask an element to tell you about an event with addEventListener:
const clearButton = document.querySelector("#clear");
clearButton.addEventListener("click", (event) => {
console.log("Clear was clicked", event.type);
});Read it as: “when a click happens on this button, call this function”. Your function does not run now. The browser stores it and calls it later, once per click, passing an event object with details about what happened. This is the callback idea from the functions lesson, and the browser is the code calling you back.
The event types you will use most: click, input (a text field’s value changed), change (a value was committed, for example a select), submit (a form was sent), and keydown (a key was pressed; event.key says which, such as "Enter" or "Escape").
Data first, then render
The most important habit with events: do not treat the page as the place your data lives. Keep the data in a variable, change the variable in your handler, and call the render function from the last lesson to redraw:
let expenses = [
{ id: "exp-1", label: "Coffee", amount: 450 },
{ id: "exp-2", label: "Train", amount: 1220 },
];
clearButton.addEventListener("click", () => {
expenses = [];
renderExpenses(expenses);
});The handler never touches <li> elements directly. It changes the data, and the render function makes the page match. However many buttons you add, there is still one place that draws the list, so the total and the list can never disagree.
Bubbling: events travel up the tree
When you click a button inside a list item inside a list, you are clicking all of them: the button, the <li>, the <ul>, the <body>. The browser models this by delivering the event first to the element you clicked, the target, and then to each ancestor in turn, all the way up to document. This is called bubbling.
Inside a handler, event.target is the element where the event started, and event.currentTarget is the element whose listener is running now. A listener on the <ul> therefore hears clicks on every button inside it and can still find out which button it was.
Event delegation: one listener for the whole list
Suppose every expense has a delete button. You could attach a listener to each button, but the render function throws those buttons away and makes new ones every time the list changes, and the new buttons have no listeners. Instead, attach one listener to the list, which is never replaced, and work out what was clicked:
const list = document.querySelector("#expense-list");
list.addEventListener("click", (event) => {
const button = event.target.closest("button.delete");
if (!button) {
return; // the click was somewhere else in the list
}
const id = button.closest("li").dataset.id;
expenses = expenses.filter((e) => e.id !== id);
renderExpenses(expenses);
});closest(selector) starts at an element and walks up through its ancestors until it finds one that matches, or returns null. That handles clicks on an icon inside the button, and the if (!button) guard ignores clicks on the rest of the item. The id comes from the data-id you set while rendering, and the data change is a filter, exactly as in section 3.
This pattern is called event delegation. It means fewer listeners, and it keeps working for items added after the listener was set up, because their clicks bubble to the same list.
To stop a click from bubbling further you can call event.stopPropagation(), but you will rarely need it, and using it often breaks delegation somewhere else. event.preventDefault() is different and far more common: it cancels the browser’s default action, such as following a link or submitting a form. That second one is the star of the next lesson.
Key takeaways
addEventListener(type, handler)registers a function the browser calls later, with an event object describing what happened.- Pass the handler itself, without parentheses; writing
handler()calls it immediately and registers its return value instead. - Events bubble from the element that was clicked up through its ancestors, so a listener on a parent hears clicks on its children.
- With event delegation, one listener on a container uses
event.target.closest(...)to find which item was clicked, including items added later. - Keep data in variables, change the data in handlers, then call your render function; never treat the page as the source of truth.