AniUI Academy

Reacting to Clicks

Your code stops running top to bottom and starts waiting to be called. Handle clicks, typing and form submits, and cover a whole list with one listener.

12 min read

A page that only changes the instant it loads is a poster. What people actually do is click, type and submit, and none of that is something your code can go looking for. It has to sit and wait.

Your code waits to be called

Up to now your programs ran top to bottom and finished. This is different. You hand a function to the browser and say "call this when someone clicks that button". Then your script ends.

const button = document.querySelector("#save");
 
button.addEventListener("click", handleSave);
 
function handleSave() {
  console.log("Saved");
}

Run that and nothing is logged. The script did its job — it registered the function and stopped. handleSave now sits there unused. It might run in three seconds, or forty times, or never, and none of that is up to you.

A function you hand over for someone else to call is a callback. You have already used the idea without noticing: the function you pass to map is a callback too. The difference here is who calls it and when.

Pass the function, do not call it

This is the mistake almost everybody makes once.

button.addEventListener("click", handleSave()); // wrong
button.addEventListener("click", handleSave);   // right

The brackets mean "run this now". So the first line runs handleSave immediately, as the page loads, and then registers its return value as the listener. That value is usually undefined, so clicking does nothing at all.

The symptom is distinctive and worth recognising: your handler fires once when the page loads, then never again. Whenever you see that, look for a stray pair of brackets.

When your handler needs an argument, wrap it in another function instead:

button.addEventListener("click", () => deleteLesson(12));

Now the arrow function is the listener, and deleteLesson only runs when the arrow function is called.

The event object

The browser passes your handler one argument: an object describing what happened.

<button id="buy" data-price="199">Buy for ₹199</button>
const button = document.querySelector("#buy");
 
button.addEventListener("click", (event) => {
  console.log(event.type);                 // "click"
  console.log(event.target);               // the button element
  console.log(event.target.dataset.price); // "199"
});

event.target is the element the event started on. That one property does a lot of work, as you will see shortly.

The other essential is event.preventDefault(). Some elements come with built-in behaviour: a link navigates, a submit button reloads the page. If you are handling it yourself, cancel theirs first.

link.addEventListener("click", (event) => {
  event.preventDefault();
  console.log("Handling this myself");
});

Beyond click

The same method handles everything, only the event name changes.

<input id="search" type="text" placeholder="Search lessons" />
const search = document.querySelector("#search");
 
search.addEventListener("input", (event) => {
  console.log(event.target.value);
});

input fires on every keystroke, which is what you want for live search or a character counter. change fires only when someone finishes — leaving a text field, or picking a different option in a dropdown.

.value is how you read what was typed, and it is always a string. Type 3 into a number field and you get "3", not 3. Wrap it in Number() before doing arithmetic, or your prices start concatenating instead of adding.

const quantity = Number(quantityInput.value);
const total = quantity * 199;

keydown gives you the key through event.key:

search.addEventListener("keydown", (event) => {
  if (event.key === "Enter") {
    console.log("Searching for", event.target.value);
  }
});

And submit belongs on the form, never on the button — pressing Enter inside a field submits the form without the button being touched at all.

Try it yourself

There is no page in the playground, so instead it models the shape of the thing: register a function, then have something else call it later. Watch that handleClick is passed by name and only runs when fire decides to run it.

Try it yourself
Loading playground...

Clicks travel upwards

A click on a button is also a click on the list item around it, and the list around that, and so on up to the document. The event starts at the deepest element and bubbles up through every ancestor. Any listener along that path hears it, and event.target still reports where it began.

That sounds like trivia. It is actually the most useful thing in this lesson.

One listener for a whole list

Say you build a to-do list from an array, each row with its own Remove button. Attaching a listener to each button is the obvious move, and it is the wrong one — rows added later have no listener, because they did not exist when the code ran.

Listen on the parent instead.

<ul id="todo-list">
  <li>Finish the DOM lesson <button class="remove">Remove</button></li>
</ul>
const list = document.querySelector("#todo-list");
 
list.addEventListener("click", (event) => {
  const button = event.target.closest(".remove");
  if (!button) return;
 
  button.closest("li").remove();
});

closest walks up from the clicked element until it finds something matching the selector, or returns null. You need it because the click might land on an icon or a stray span inside the button rather than the button itself. The guard clause handles clicks that hit the list but no button.

This is called event delegation. One listener, any number of rows, including rows that do not exist yet. The list is always there, so the listener is too.

Removing a listener

Occasionally you want to stop listening.

function onScroll() {
  console.log("scrolled");
}
 
window.addEventListener("scroll", onScroll);
window.removeEventListener("scroll", onScroll);

You must pass the same function, which means an inline arrow function can never be removed — you have no name to refer to it by. If you only want a handler to run once, addEventListener("click", handler, { once: true }) does the cleanup for you.

Putting it together

Input, button, growing list. Both lessons in one small feature.

<form id="todo-form">
  <input id="todo-input" type="text" placeholder="What needs doing?" />
  <button type="submit">Add</button>
</form>
<ul id="todo-list"></ul>
const form = document.querySelector("#todo-form");
const input = document.querySelector("#todo-input");
const list = document.querySelector("#todo-list");
 
form.addEventListener("submit", (event) => {
  event.preventDefault();
 
  const text = input.value.trim();
  if (text === "") return;
 
  const item = document.createElement("li");
  item.textContent = text;
 
  const remove = document.createElement("button");
  remove.textContent = "Remove";
  remove.classList.add("remove");
 
  item.append(remove);
  list.append(item);
 
  input.value = "";
  input.focus();
});
 
list.addEventListener("click", (event) => {
  const button = event.target.closest(".remove");
  if (!button) return;
 
  button.closest("li").remove();
});

Two listeners, however long the list grows. Everything else is the DOM work you already know: create, set text, append, remove.

What to remember

  • You register a function and the browser calls it later. Your script does not wait around.
  • Pass the function by name; brackets run it immediately and register nothing.
  • event.target is where it started, preventDefault() cancels the browser's own behaviour.
  • .value reads what was typed and is always a string.
  • Events bubble, so one listener on a parent can serve every row inside it.

Check yourself

4 questions · pass 3/4 to unlock Asynchronous JavaScript

up to 50
  1. 1.What actually happens when you write button.addEventListener("click", handleSave())?

  2. 2.A form reloads the whole page every time it is submitted. What is the fix?

  3. 3.You render 200 to-do rows from an array, each with its own delete button, and new rows appear as people type. Why put one click listener on the list instead of one on every button?

  4. 4.Inside that one listener on the list, what does event.target refer to?

4 left to answer