Listening for events
Programs so far ran top to bottom and exited. A page cannot work that way, since it must sit idle and respond whenever the user acts, at any time and in any order.
Events invert control, so the browser watches for actions and calls your functions when they happen.
An event is anything that happens in the page, such as a click, a keypress, or a form submit. Reacting means registering a listener, a function the browser calls whenever the event fires.
const button = document.querySelector("#save"); button.addEventListener("click", () => { button.textContent = "Saved!"; });
| Event | Fires when |
|---|---|
click | the element is clicked |
input | on every keystroke in a field |
change | an edited field loses focus |
submit | a form is submitted |
keydown | a key goes down |
The listener receives an event object carrying the details. One of its methods matters immediately, since calling event.preventDefault() on a form's submit stops the browser's built-in submission, which would reload the page, and leaves the data to JavaScript.
form.addEventListener("submit", (event) => { event.preventDefault(); // validate, then decide what happens });
That is exactly how the capstone's form works in lesson 9-3.
Simulating the listener registry
The browser keeps a list of listeners per event name and calls each one when that event fires. This small simulation is the mental model.
const listeners = {}; function addEventListener(name, fn) { if (!listeners[name]) listeners[name] = []; listeners[name].push(fn); } function fire(name) { for (const fn of listeners[name] || []) fn(); } addEventListener("click", () => console.log("first listener")); addEventListener("click", () => console.log("second listener")); fire("click"); fire("hover"); fire("click");
Output
first listener second listener first listener second listener
| Call | Listeners run |
|---|---|
fire("click") | both, in registration order |
fire("hover") | none |
fire("click") | both again |
Two listeners on the same name both run, which is why adding a listener never replaces an existing one. The || [] guard is what makes an unregistered name harmless rather than a crash, and the real DOM behaves the same way.
Keeping state between events
Listeners can keep state between events using a variable from the outer scope, which is how a counter survives across firings.
const listeners = {}; function addEventListener(name, fn) { if (!listeners[name]) listeners[name] = []; listeners[name].push(fn); } function fire(name) { for (const fn of listeners[name] || []) fn(); } let count = 0; addEventListener("click", () => { count++; console.log("clicks: " + count); }); fire("click"); fire("click"); fire("click");
Output
clicks: 1 clicks: 2 clicks: 3
| Firing | count after it |
|---|---|
| first | 1 |
| second | 2 |
| third | 3 |
The listener is an arrow function doing two things, incrementing and logging.
count lives outside the listener, so it survives between calls. That is a closure, and it is how real click counters work as well. A count declared inside the listener would reset to 0 on every event and always log 1.
A click counter on a real element
The smallest complete example of an event listener, with the count written back into the page.
JavaScript
const button = document.querySelector("#clicker"); const count = document.querySelector("#count"); let clicks = 0; button.addEventListener("click", () => { clicks++; count.textContent = clicks; });
| Line | Job |
|---|---|
two querySelector calls | find the button and the readout |
let clicks = 0 | state that outlives each call |
addEventListener("click", ...) | register the reaction |
count.textContent = clicks | make the change visible |
The shape matches the simulation above, with addEventListener living on the button element rather than on a hand-written registry.
Incrementing clicks alone would change nothing on screen, since the variable and the page are separate. Writing it into textContent is the step that makes state visible, and that division is the whole job of DOM code.
The event for a live character counter
A character counter under a textarea that updates as the user types listens for input.
input fires on every keystroke and on paste, so the counter stays live.
| Event | Fires |
|---|---|
input | on every keystroke and paste |
change | once the field loses focus after editing |
keydown | on key presses, including keys that type nothing |
change would leave the counter stale while typing, which defeats the purpose. keydown fires too early to see the new value and also fires for arrow keys, so input is the one that matches what a counter needs.
What preventDefault cancels
Inside a submit listener, event.preventDefault() cancels the browser's built-in action, which is the page-reloading form submission.
Many events carry a default browser action.
| Event | Default action |
|---|---|
submit | submit the form and reload |
a link click | navigate to the href |
a checkbox click | toggle the box |
preventDefault cancels only that default and leaves the listener in full control. It does not stop other listeners from running, which is a separate method, so a form can still be validated by one listener and logged by another.