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
- 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.
- Log the whole
eventobject and explore it in devtools. Findtype,targetandcurrentTarget. - Put a listener on a list and click a nested element inside it. Print
targetandcurrentTargetand make sure you can predict both. - Attach
inputandchangeto the same text box, logging each. Type, then click away. Watch which fires when. - 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.
- Use
{ once: true }on a button and confirm the second click does nothing. - Compare
thisin an ordinary handler and an arrow handler on the same element. - 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. - Add a
keydownlistener to an input that logsevent.key. Press Enter, Escape and a letter. - 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
Intlformatter. 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