RizTech Academy logo
RizTech Academy
The DOMLesson 3 of 730 min

Events and event listeners

A page that cannot respond to a person is a document. Events are how JavaScript finds out that somebody clicked, typed, submitted or scrolled — and module 3's callbacks are exactly the mechanism.

Listening

const button = document.querySelector('#add');

button.addEventListener('click', () => {
  console.log('clicked');
});

Three parts: the element, the event name, and a function you are handing over for the browser to call later. That is a callback, and module 3's warning applies:

button.addEventListener('click', handleClick());

The brackets call it immediately and register its return value. Pass the function, do not call it.

The event object

Your handler is given one argument describing what happened:

button.addEventListener('click', (event) => {
  console.log(event.type);
  console.log(event.target);
});
click
<button id="add">Add</button>

The properties worth knowing:

Property Is
type 'click', 'input', 'submit' …
target The element the event started on
currentTarget The element the listener is attached to
preventDefault() Cancel the browser's default behaviour
stopPropagation() Stop it travelling to ancestors
key On keyboard events — 'Enter', 'a', 'Escape'

target and currentTarget differ constantly, and the difference is the whole of the next lesson. Click a span inside a ul that has the listener:

target=SPAN currentTarget=UL

The event started on the span and is being handled on the list.

The events you will actually use

Event Fires when
click Clicked or tapped
input An input's value changes — on every keystroke
change The value is committed — on blur, or immediately for a checkbox or select
submit A form is submitted. On the form, not the button
keydown A key goes down. event.key names it
focus / blur An element gains or loses focus
DOMContentLoaded The document is parsed

input versus change is the one to get right. For live search or a running total, you want input — it fires as they type. For "they have finished with this field", change.

Removing a listener, and the bug

function handleClick() {
  console.log('clicked');
}

button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick);

That works. This does not:

button.addEventListener('click', () => console.log('clicked'));
button.removeEventListener('click', () => console.log('clicked'));

The listener stays attached. Two identical-looking arrow functions are two different objects — module 4's {} === {} being false, in its most expensive form. removeEventListener needs the same function, so it silently removes nothing.

Run it and the handler still fires after the removal. Nothing errors.

So keep a reference to any handler you intend to remove. If you never remove it, an inline arrow is fine and usual.

Two shorthands for common cases:

button.addEventListener('click', handleClick, { once: true });

once: true removes it automatically after the first call — the clean way to stop a double-submitted order.

Why this matters: listeners keep things alive

function showOrder(order) {
  button.addEventListener('click', () => {
    console.log(order.customer);
  });
}

Call that a hundred times and you have a hundred listeners on one button, each closing over an order — module 3's closure cost, now with a page attached. The button fires all hundred, and none of the orders can be freed.

This is the commonest cause of a page that gets slower the longer it is open. Attach listeners once, or remove them when you are done. The next lesson's delegation avoids the problem entirely.

this in a handler

parent.addEventListener('click', function (event) {
  console.log(this.tagName);
  console.log(event.currentTarget.tagName);
});
UL
UL

In an ordinary function, this is currentTarget. In an arrow function it is whatever this was outside — at the top level of a script, window:

parent.addEventListener('click', (event) => {
  console.log(this);
});
Window

Module 3's rule, in the place it bites most. Use event.currentTarget rather than this — it says what it means, and it works in both kinds of function.

preventDefault

Some events have browser behaviour attached: a form submits and reloads, a link navigates, a checkbox ticks.

form.addEventListener('submit', (event) => {
  event.preventDefault();
  console.log('handled in JavaScript');
});

Without that line the page reloads and your code never finishes. A form handler that seems to do nothing, with the page flashing, is a missing preventDefault.

Do not reach for it reflexively. Cancelling default behaviour on a link breaks opening in a new tab; cancelling keydown can break accessibility. Cancel only what you are genuinely replacing.

Check your work

addEventListener(event, fn) takes the function, not a call. Brackets run it immediately and register undefined.

event.target is where the event started; event.currentTarget is where the listener is. Clicking a span inside a listening ul gives target=SPAN, currentTarget=UL.

input fires on every keystroke; change fires when the value is committed.

removeEventListener with a new anonymous function removes nothing — the handler stays attached and keeps firing, with no error. Keep a named reference.

{ once: true } removes the listener after one call.

Attaching a listener inside a repeatedly-called function accumulates listeners, each holding its closure alive. That is why long-lived pages get slow.

In an ordinary handler this is currentTarget; in an arrow it is the surrounding this, usually window. Prefer event.currentTarget.

submit belongs on the form, not the button, and without preventDefault the page reloads.

Practice

  1. Add a click listener to a button that logs a message. Then make the brackets mistake and work out from the behaviour what went wrong.
  2. Log the whole event object and explore it in devtools. Find type, target and currentTarget.
  3. Put a listener on a list and click a nested element inside it. Print target and currentTarget and make sure you can predict both.
  4. Attach input and change to the same text box, logging each. Type, then click away. Watch which fires when.
  5. Write the removal bug. Add an anonymous arrow listener, remove it with an identical-looking one, and confirm it still fires. Then fix it with a named function.
  6. Use { once: true } on a button and confirm the second click does nothing.
  7. Compare this in an ordinary handler and an arrow handler on the same element.
  8. Forget preventDefault. Make a form with a submit handler that logs something, without it. Watch the page reload and the log vanish. Then add it.
  9. Add a keydown listener to an input that logs event.key. Press Enter, Escape and a letter.
  10. Harder. Build a plate counter: a number, a plus button and a minus button, with the total in rupees updating live at ₹80 a plate. It must not go below zero, and the ₹ total must use module 6's Intl formatter. Attach exactly two listeners, and keep the count in a variable rather than reading it back out of the page — reading state out of the DOM is a habit worth not forming.

Next: event delegation — one listener for a list that is still being built.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship