Course outline · 0% complete

0/30 lessons0%

Course overview →

Events

lesson 8-3 · ~9 min · 26/30

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!";
});
EventFires when
clickthe element is clicked
inputon every keystroke in a field
changean edited field loses focus
submita form is submitted
keydowna 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.

the userclicksthe browser looks uplisteners for "click"first listener runssecond listener runsAdding a listener never replaces an existing one, so both functions run on every click.
Events invert control: the user acts, the browser looks up the listeners registered for that event name, and calls each one in registration order.

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
CallListeners 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
Firingcount after it
first1
second2
third3

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;
});
LineJob
two querySelector callsfind the button and the readout
let clicks = 0state that outlives each call
addEventListener("click", ...)register the reaction
count.textContent = clicksmake 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.

EventFires
inputon every keystroke and paste
changeonce the field loses focus after editing
keydownon 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.

EventDefault action
submitsubmit the form and reload
a link clicknavigate to the href
a checkbox clicktoggle 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.