Skip to content
wannacode

Events

In short: Events tell JavaScript that the user clicked or typed: addEventListener listens, most events bubble up the tree, and delegation uses one listener for many elements.

Problem

The new product is not added

The catalogue shows two products. "Show more" adds a third one. "Add" on the first two raises the cart, but "Add" on the new product does nothing. The console has no error.

HTML, CSS, JS

Click "Add" on the bread, then "Show more", then "Add" on the cheese. The cart stays at 1. The cheese button looks like the others and has the same text. What is it missing?

The idea

Event and listener

When the buyer clicks, presses a key or types into a field, the browser creates an event: an object that says what happened and where. The code waits for it with a listener:

const button = document.querySelector('#plus');
button.addEventListener('click', (event) => {
console.log(event.type, event.target);
});
  • The first argument is the kind of event: click, keydown, input, submit.
  • The second is a function. The browser calls it on every event and passes it the event object. Arrow functions come from the lesson on lambdas in the JavaScript course.
  • You pass the function, you do not call it: addEventListener('click', save), without parentheses. save() would call it right away.

The event object has fields:

  • event.target is the element where the event happened: the button that was clicked.
  • event.currentTarget is the element whose listener is running now.
  • event.key is the key on keydown: 'Enter', 'Escape', 'a'.

Now it is clear what is missing in the problem. The listeners are added once, while the page loads, to the buttons that exist at that moment. The cheese button is created later and has no listener.

Try it

Plus and minus

Make the quantity buttons: "+" raises the quantity by 1, and "−" lowers it by 1, but not below 1. The quantity is shown in #qty.

HTML, CSS, JS

The idea

The event bubbles up

The button is inside <li>, <li> is inside <ul>, and <ul> is inside <main>. A click on the button is also a click on each of them. So the event does not stay in the button. It starts at event.target and bubbles up through the parents to document and window. Every parent with a click listener hears it.

document.querySelector('ul').addEventListener('click', (event) => {
console.log(event.target); // the button that was clicked
console.log(event.currentTarget); // <ul>: the element with the listener
});

Most events bubble: click, keydown, input, submit. Some do not: focus and blur. For them there are focusin and focusout, which do bubble.

See it

See the click bubble up

On the left is the tree of the catalogue. The listener is on <ul>, and the click is on the milk's button.

const list = document.querySelector('#products');
list.addEventListener('click', (event) => {
const button = event.target.closest('button');
if (!button) return;
const item = button.closest('li');
console.log('Add', item.dataset.id);
});
Step by step, in words
  1. There is one listener, and it is on the list <ul>, not on the buttons. It waits for click.
  2. The buyer clicks "Add" on the milk. The event starts at the innermost element under the cursor: the button. That is event.target.
  3. Then it bubbles up: from the button to its parent <li>, then to <ul>. Every parent hears the click too.
  4. <ul> has a listener, and the function runs. event.currentTarget is <ul>, the element with the listener. event.target is still the button.
  5. closest('button') starts at event.target itself and goes up. The target is a button, so it is found right away.
  6. closest('li') goes up from the button and stops at the first <li>: the milk. Its data-id says which product it is.
  7. dataset.id is "2". The cart knows what to add.
  8. The event goes on up to <main>, <body>, document and window. There are no listeners there, and nothing else happens.
  9. A click on the name of the bread also bubbles to <ul>. But the target is <h2>: closest('button') finds no button above it and returns null, and the function stops.
  10. One listener serves every button in the list, even the ones added later: they are inside <ul> too. This is called delegation.

A click on the name of the bread also reaches <ul>. That is why the listener first checks that a button was clicked.

The idea

Delegation

Instead of one listener per button, you put one on the common parent. It works out from event.target which button was clicked. This is called delegation.

list.addEventListener('click', (event) => {
const button = event.target.closest('button');
if (!button) return;
const id = button.closest('li').dataset.id;
// …add product id
});
  • closest('button') starts at the element itself and goes up to the first one that matches the selector. With no match it returns null. Here it guards against a click on the name or the empty space in <li>.
  • The product data is on <li> (data-id), not on the button. closest('li') gets to it.

There are two gains. New products work with nothing extra: they are in the list too. And there is one listener, however many products there are.

Try it

Fix the catalogue

Fix the catalogue with one listener on #products. "Add" on every product, including the one added with "Show more", raises the cart and writes "product added" and the data-id of the product to the console. A click on the name of a product adds nothing.

HTML, CSS, JS

The idea

The default action

Some events have an action that the browser does by itself, after the listeners. A click on a link opens its address. submit on a form sends the fields and loads the new page. This is the default action.

The search form here shows the result in the page. Try it.

HTML, CSS, JS

The listener writes "You are looking for: milk", but then the browser sends the form. In a real browser the page loads again with ?q=milk in the address, and the result is gone. The sandbox is a single page: it stops the sending and writes "The form was sent" to the console.

Add event.preventDefault(); at the start of the function and run it again. The line in the console is gone: the action is stopped, and the listener still does its job. The same goes for a link that opens something inside the page instead of going to another address.

event.stopPropagation() stops the bubbling. You rarely need it, and it is usually a bad sign. It also stops listeners the code does not know about: the delegation above, closing a menu on a click outside it, counting clicks. If a listener "hears too much", check event.target, the way closest does in delegation.

Try it

Find the bug

The listener shows the result, but the form is still sent.

Pick the line with the bug.

The idea

input or change

Fields have two events for a change:

  • input comes with every change of the value: every letter, deletion, paste.
  • change comes when the buyer is done. For a text field that is when they leave it. For a checkbox, a radio button and a drop-down it is right after the choice.

For a search that filters while you type, use input. For a "Sort by price" drop-down, use change.

More precisely, if you are curious

A third argument. addEventListener('click', f, { once: true }) listens only once. removeEventListener('click', f) removes the listener, but only if you pass the same function. An arrow function written again is a different function.

onclick. button.onclick = f also works, but it is a single field: a second onclick erases the first. addEventListener allows many listeners. The onclick="…" attribute in HTML mixes the code with the page, and a strict security policy (CSP) blocks it.

Bubbling and capturing. The path of the event has another direction too: first from window down to the target (capturing), then up (bubbling). addEventListener('click', f, { capture: true }) listens on the way down. You almost always want bubbling.

The keyboard. A <button> gets click from Enter and Space too. That is why click on a button works with the keyboard. keydown on a <div> does not replace it (see the accessibility lesson).

event.key versus event.code. key is the character ('a' on a QWERTY keyboard, 'q' for the same key on a French AZERTY one), and code is where the key is ('KeyA' on both). For shortcuts like Escape and Enter, use key.

Exercise

Live search

Make the search filter the list while the buyer types: only the products that contain the text are shown, whatever the case of the letters. A hidden product has hidden. #found shows how many are shown ("Found: 2"). Escape in the field clears it and shows them all. Enter does not send the form.

HTML, CSS, JS

Test

Check yourself

1. The listener is on <ul>, and the buyer clicks a button inside an <li>. What are event.target and event.currentTarget?
2. What is wrong with button.addEventListener('click', save())?
3. Why does delegation also work for products added after the page loaded?
4. A search form has a submit listener that shows the result. Why do you need event.preventDefault()?
5. A search that filters the list while the buyer types. Which event?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·