Events
The page answering back: listening for clicks, reading what happened, and keeping state between events.
Listening for something to happen
Up to now the script ran once, at load, and stopped. An event listener keeps it alive: button.addEventListener("click", handler) says "when this button is clicked, run this function" — as many times as it is clicked.
The name of the event is a string, and there are dozens: click, input while typing, submit, keydown, mouseover. The same element can listen to several at once, and several elements can listen to the same one.
The listener is registered, not run. The call to addEventListener returns immediately, and the function sits waiting until the event fires. That is the difference between a script that runs once at load and one that keeps answering for as long as the page is open.
A button that counts
const button = document.querySelector("button");
const output = document.querySelector(".output");
let count = 0;
button.addEventListener("click", () => {
count = count + 1;
output.textContent = `Clicked ${count} times`;
});State lives outside the handler
The counting variable is declared out in the open, not inside the handler. A variable created inside the function would be born fresh on every click and always read 1 — the distinction between the function (which runs many times) and the value (which persists between runs).
That is what "state" means: the values a program remembers as things happen. A counter, the open item, the current filter — all of them are just variables declared where the handler can see them.
Declaring the counter outside also makes the handler reusable: the same function can be attached to two buttons and they will share the count, which may or may not be what you want. If each button needs its own count, each needs its own variable, or the count has to be stored on the element.
The event object
The handler can take a parameter: the event itself. It carries what happened — which key was pressed, the element that was clicked, the position on screen — and one method to know early: event.preventDefault(), which stops the browser’s default behaviour for that event.
That method is what a form handler always starts with, and the next chapter uses it.
Events travel through the page
A click on a button does not stop at the button. The event travels down to it and then back up through its ancestors, calling listeners on the way. That trip is called bubbling, and it is why a listener on a list can answer a click on any item inside it.
Inside the handler, event.target is the element that actually received the event, while event.currentTarget is the element whose listener is running. They are the same on a button with no children, and different the moment the click lands on a span inside it. event.target.closest("button") walks up to the thing you meant.
The handler is a value
addEventListener takes a function, not the result of calling one. button.addEventListener("click", bake) passes the function and works; button.addEventListener("click", bake()) calls it immediately and passes whatever it returned, which is usually undefined.
A listener also stays attached until it is removed or the page goes away. Adding the same function twice attaches it twice, so a counter can jump by two per click. removeEventListener needs the same reference you added, which is one good reason to give the handler a name.
A worked click, traced
Suppose the button and the output were found when the script loaded, and count starts at 0. The listener is attached and the script ends; nothing else happens until a click. On the first click the browser calls the handler, which reads count, adds one and writes Clicked 1 times into the paragraph. The function returns, but count is still 1 because it was declared outside.
A second click runs the same function again, and this time count is 1, so it becomes 2. The listener was never re-attached; it was attached once and the browser keeps calling it. That is why the state has to live where both calls can see it.
Three common slips: writing handler() instead of handler, which runs the function once at setup and attaches nothing; attaching a listener inside another listener, so it stacks on every event; and assuming event.target is the button when the click may land on a child — closest is the reliable way back to the element you mean.
Try these next
- Log
event.target.tagNameandevent.currentTarget.tagNamefrom the same handler. - Use a named function, attach it, then remove it and confirm clicks stop counting.
- Add a second button and let one listener on the container answer both.
A button that answers
The button does nothing. Make it count: each click should update the paragraph with how many times it has been pressed.
3 left to do
- The button listens for clicks
- A count is kept between clicks
- The paragraph is updated
A let count = 0; outside the handler; inside, add one and set output.textContent with a template literal.
One listener, three buttons
Attach a single click listener to the container, not to each button. When a button is clicked, read its label and write which loaf was chosen into the paragraph.
3 left to do
- The container listens for clicks
- The clicked button is found with closest
- The choice is written into the paragraph
Use event.target.closest("button") to find the button from whatever was clicked, then read its textContent. Ignore the click when closest finds nothing.
If a listener seems to do nothing, check that the element was found before the listener was attached. A script with a querySelector that returned null has nothing to listen on.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does button.addEventListener("click", handler) do?
