DataLens

Events and event listeners

React to clicks, typing, form submissions and key presses with addEventListener, use the event object, stop default browser behaviour, and handle many elements with one delegated listener.

Level: Intermediate · 10 min read

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

The idea

A web page mostly waits. Nothing happens until the user does something: clicks, types, submits, presses a key. Each of those is an event, and JavaScript runs your code when one happens through an event listener. "When this button is clicked, add the item to the cart" is the basic sentence of every interactive page.

As in the previous module, each example builds its own small page. button.click() and dispatchEvent simulate what a user would do, so the examples run without anyone clicking.

addEventListener

A page that only changes when it first loads isn't very interactive. You want things to happen when the customer does something: clicks "Add to cart", types in the search box, submits a form. You tell an element "when this happens, run this function":

document.body.innerHTML = `<button id="add">Add to cart</button> <span id="count">0</span>`;
const button = document.querySelector("#add");
const count = document.querySelector("#count");

button.addEventListener("click", () => {
  count.textContent = Number(count.textContent) + 1;
  console.log("Cart:", count.textContent);
});

button.click();
button.click();
Cart: 1
Cart: 2

element.addEventListener(type, handler) registers a function to run every time that event happens on that element. Pass the function itself; don't call it: addEventListener("click", addToCart), not addToCart().

You'll see onclick="..." attributes in old tutorials. Avoid them: they mix JavaScript into HTML, allow only one handler, and are blocked by many security policies.

The event object

Every handler receives an event object describing what happened:

document.body.innerHTML = `<button class="buy" data-id="7">Buy <strong>now</strong></button>`;
const buy = document.querySelector(".buy");

buy.addEventListener("click", (event) => {
  console.log(event.type);
  console.log(event.currentTarget.dataset.id);   // the element the listener is on
  console.log(event.target.tagName);             // the element actually clicked
});

buy.querySelector("strong").click();
click
7
STRONG

event.currentTarget is the element you attached the listener to; event.target is the innermost element that was actually clicked (here, the <strong> inside the button). The difference matters for the delegation pattern below.

Typing: the input event

A search box that updates the results as you type feels much faster than one where you have to press a button. The input event fires every time the field's value changes. The last two lines here simulate someone typing, since there's no keyboard in the example:

document.body.innerHTML = `<input id="search" aria-label="Search"> <p id="echo"></p>`;
const search = document.querySelector("#search");
const echo = document.querySelector("#echo");

search.addEventListener("input", () => {
  echo.textContent = `Searching for "${search.value.trim()}"`;
});

search.value = "  gayo ";
search.dispatchEvent(new Event("input"));
console.log(echo.textContent);
Searching for "gayo"

input fires on every change to a field's value (each keystroke, paste or delete); change fires when the user commits a change (leaves the field, picks an option). Read the current value from element.value, which is always a string.

Forms: submit and preventDefault

When someone submits a form, the browser's default behaviour is to send the data to the server and load a new page. For a newsletter sign-up, that's jarring: you'd rather show "Thanks!" right there on the same page. You catch the submit event and stop the default behaviour:

document.body.innerHTML = `
  <form id="newsletter">
    <input name="email" value="[email protected]" aria-label="Email">
    <button>Subscribe</button>
  </form>
  <p id="message"></p>`;
const form = document.querySelector("#newsletter");

form.addEventListener("submit", (event) => {
  event.preventDefault();                       // stay on this page
  const data = new FormData(form);
  document.querySelector("#message").textContent = `Thanks, ${data.get("email")}!`;
  console.log("prevented:", event.defaultPrevented);
});

form.requestSubmit();
console.log(document.querySelector("#message").textContent);
prevented: true
Thanks, [email protected]!

preventDefault works for other defaults too: following a link, checking a checkbox, scrolling with arrow keys.

One listener for many elements: delegation

A list of 50 products with 50 "Add" buttons doesn't need 50 listeners. Events bubble: after firing on the clicked element, they travel up through each parent. So one listener on the container can handle them all:

document.body.innerHTML = `
  <ul id="products">
    <li>Arabica Gayo <button data-id="1">Add</button></li>
    <li>Toraja Kalosi <button data-id="2">Add</button></li>
  </ul>`;
const list = document.querySelector("#products");
const added = [];

list.addEventListener("click", (event) => {
  const button = event.target.closest("button[data-id]");
  if (!button) return;                          // a click elsewhere in the list
  added.push(Number(button.dataset.id));
});

list.querySelectorAll("button")[1].click();
list.querySelector("li").click();               // clicking the text, not a button
list.querySelectorAll("button")[0].click();
console.log(added);
[2, 1]

event.target.closest(selector) walks up from the clicked element to the nearest match (or null). Delegation also works for elements added later, such as products rendered after a search, which separate listeners wouldn't.

Keyboard events

document.body.innerHTML = `<div id="dialog">Added to cart ✓</div>`;
document.addEventListener("keydown", (event) => {
  if (event.key === "Escape") {
    document.querySelector("#dialog").hidden = true;
    console.log("Dialog closed");
  }
});
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
Dialog closed

event.key holds the key's name: "Enter", "Escape", "ArrowDown", "a".

Key idea

el.addEventListener("click" | "input" | "submit" | "keydown", handler); the handler gets an event object (target, currentTarget, key). Handle forms on submit with preventDefault(). For many similar elements, use one delegated listener with event.target.closest(...).

Why this matters for clean code

Keep handlers short: read what happened from the event, call a well-named function that does the work (addToCart(id)), then update the page. Business logic inside a handler can't be reused or tested; in a function, it can. Delegation keeps the number of listeners constant no matter how big the page grows.

Recap

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

Next: Template literals and string handling →