Events and forms

Intermediate

Introduction

A page that only runs once when it loads is not interactive. Events are how a page responds to people: a click, a key press, typing in a box, a form being sent. Your code does not sit waiting in a loop. It tells the browser what to do when something happens, and then gets out of the way.

How it works

el.addEventListener("click", handler) asks the browser to call handler every time that event happens on el. The handler is a callback, run by the event loop you met in the asynchronous lesson.

The handler gets an event object with details: event.target (the element it happened on), event.key for key presses, and methods such as event.preventDefault(), which stops the browser's own action (a form submit reloading the page, a link opening).

EventWhen
clicka button or element is clicked (or pressed with Enter or Space, for buttons)
inputthe text in a box changes
changea select, checkbox or box changed and lost focus
submita form is sent
keydowna key is pressed

Forms. input.value is the current text of a box, and it is always a string. Listen for submit on the form, not click on the button, so that pressing Enter works too.

Bubbling and delegation. An event on an element bubbles up: after the handlers on the clicked task run, the same event reaches its list, then the body, then the document. So one listener on a parent can handle all its children, even ones added later. This is called event delegation. event.target.closest("li") finds the item that was clicked, even if the click landed on something inside it. event.stopPropagation() stops the bubbling.

The traced example below is a tiny event system built from plain objects and arrays, the same idea the browser uses inside: listeners are stored in a list for each event name, and emitting an event calls each one in turn:

// A tiny event system: listeners in a list, called on emit.
const listeners = {};

function on(name, handler) {
  if (!listeners[name]) {
    listeners[name] = [];
  }
  listeners[name].push(handler);
}

function emit(name, detail) {
  const handlers = listeners[name] ?? [];
  for (const handler of handlers) {
    handler({ type: name, detail });
  }
  return handlers.length;
}

let clicks = 0;
on("click", (event) => {
  clicks++;
  console.log("clicked", event.detail);
});
on("click", () => console.log("total clicks:", clicks));

emit("click", "Add button");
emit("click", "Save button");
console.log(emit("keydown", "Enter"), "listeners for keydown");
Open in compiler

Syntax

button.addEventListener("click", (event) => {
  console.log("clicked", event.target);
});

form.addEventListener("submit", (event) => {
  event.preventDefault();
  console.log(input.value);
});

Example

Type a task and press Enter or Add, and click a task to mark it done. This uses everything so far: a form, a submit handler, the DOM, and a closure over the count variable.

console.log output from the page shows here.

Line by line

  1. Four querySelector calls find the parts of the page, and count holds the number of unfinished tasks.
  2. update writes the count, and picks the singular or plural word.
  3. The submit handler calls preventDefault, so that the page does not reload.
  4. It trims the text, and stops if nothing was typed.
  5. It makes the new li, updates the count and empties the box.
  6. The click listener sits on the list, not on the items. closest("li") finds the task that was clicked.
  7. toggle flips the class, and the count changes to match.

Expected result

At first the page says "0 tasks left". Adding a task lists it and increases the count, and clicking it crosses it out and decreases the count.

In the real world

Search boxes filter results on each input event, and drag-and-drop is built from mouse events. Delegation is especially useful for long lists and tables, because one listener costs far less than thousands of them.

Common mistakes

  • Writing addEventListener("click", handler()). That calls the handler at once and passes its result. Pass the function itself: handler.
  • Forgetting event.preventDefault() in a submit handler, so the page reloads and the work is lost.
  • Writing "onclick" as the event name. The name is "click".
  • Adding a listener to every item instead of one listener on the parent.

Best practices

  • Use real buttons and forms, so that keyboards work without extra code.
  • Use delegation for lists whose items come and go.
  • Keep handlers short, and let them call well-named functions.
  • Check what input.value holds before you trust it.

Summary

addEventListener connects an event to a function. The event object says what happened. preventDefault cancels the browser's own action. Events bubble, so a single listener on a parent can handle all of its children.

Quick check

1. Which call runs handler on every click of button?
2. What does event.preventDefault() do in a submit handler?
3. Why can one listener on a list handle clicks on all its items?
4. Which property says which element the event happened on?

Key takeaways

  • Pass the function itself to addEventListener, without calling it.
  • Use preventDefault in a submit handler to stay on the page.
  • One listener on a parent also handles children that are added later.

Practice problems for this lesson

My notes

Write down what you want to remember from this lesson. Notes are saved on this device as you type, and appear on your dashboard.