Learning Goals
3 minBy the end of this lesson you will be able to:
- Run code on a click with
addEventListener. - Explain why you pass a function's name, not a call.
- Use the event object to find which element was clicked.
- Build a counter button that updates the page every press.
Warm-Up · It Only Happens Once
5 minLast lesson you changed the page from JavaScript. But it happened the instant the page loaded, and never again.
const status = document.querySelector('#status');
status.textContent = 'Order placed!';Predict together
- When does that message appear?
- The page has an “Order” button. Does clicking it do anything?
Reveal
The message appears immediately, before the visitor has done anything at all. The order is already “placed” the moment the page loads, and the button does nothing whatsoever.
What is missing is waiting. You need to hand the browser some code and say: run this later, when they click.
New Concept · Code That Waits
12 minThink of a doorbell. You do not stand at the door all day watching. You fit a bell, and get on with your life. When somebody presses it, the bell brings you. An event listener is that bell.
The shape of a listener
const button = document.querySelector('#order');
button.addEventListener('click', () => {
console.log('Somebody clicked!');
});Three parts, and all three matter:
button'click'() => { … }That third part is a callback — the same idea as the arrow functions you handed to .map() in Lesson 10. You are not calling it; you are handing it over to be called later.
The mistake everyone makes once
function sayHello() {
console.log('Selamat datang!');
}
button.addEventListener('click', sayHello); // correct
button.addEventListener('click', sayHello()); // wrongsayHello
The function itself, handed over.
The browser calls it on each click.
sayHello()
Runs it right now, and hands over its return value.
Usually undefined — so clicking does nothing.
The symptom is unmistakable: it fires once on load, then the button is dead. Those brackets are the difference.
The event object
The browser passes your callback one argument — an object describing what happened. By convention it is called event or just e.
button.addEventListener('click', (event) => {
console.log(event.type);
console.log(event.target.textContent);
});click
Order nowevent.target is the element that was clicked. That is what lets one listener serve many elements — you will use it in the Worked Example.
Events beyond clicking
input.addEventListener('input', (e) => {
console.log('Now typed:', e.target.value);
});
select.addEventListener('change', (e) => {
console.log('Chose:', e.target.value);
});
document.addEventListener('keydown', (e) => {
console.log('Key:', e.key);
});input fires on every keystroke; change when a choice is finished; keydown on any key, anywhere. Note .value — that is how you read what someone typed, and it is always a string.
State lives outside the listener
A listener runs fresh each time. Anything it must remember has to live outside it — the same rule as a running total in Lesson 7.
let clicks = 0; // outside: survives between clicks
button.addEventListener('click', () => {
clicks = clicks + 1; // inside: runs per click
console.log(`Clicked ${clicks} times`);
});Move let clicks = 0; inside the callback and it resets to zero every press, forever printing 1. Where a variable lives decides what it remembers.
Why it matters
Select, change, and now listen — that is a complete interactive page. In React you will write onClick={handleClick}, and the no-brackets rule you just learned is exactly the same there.
Worked Example · An Order Counter
12 minBuild a small ordering page. Save as order.html.
<body>
<h1>Warung Aisyah</h1>
<p id="status">Nothing ordered yet.</p>
<ul id="menu">
<li><button class="add" data-name="nasi lemak" data-price="5.50">nasi lemak</button></li>
<li><button class="add" data-name="roti canai" data-price="1.50">roti canai</button></li>
<li><button class="add" data-name="cendol" data-price="4.00">cendol</button></li>
</ul>
<button id="clear">Clear order</button>
<script src="app.js"></script>
</body>Step 1 — One button, one listener
// app.js
const status = document.querySelector('#status');
const clearButton = document.querySelector('#clear');
clearButton.addEventListener('click', () => {
status.textContent = 'Order cleared.';
});Click it. The paragraph changes — and changes again every time. Your code is finally responding to a person.
Step 2 — Remember something
let itemCount = 0;
let total = 0;
function updateStatus() {
status.textContent = `${itemCount} items — RM ${total.toFixed(2)}`;
}Both totals sit outside any listener, so they survive between clicks. A small named function does the redrawing, so no listener has to repeat it.
Step 3 — Listen on every dish button
const addButtons = document.querySelectorAll('.add');
for (const button of addButtons) {
button.addEventListener('click', (event) => {
itemCount = itemCount + 1;
total = total + Number(event.target.dataset.price);
updateStatus();
});
}Click nasi lemak, then cendol:
2 items — RM 9.50event.target is whichever button was pressed, so one identical listener serves all three. And Number() is doing its Lesson 4 job again — dataset values are strings.
Step 4 — Make Clear actually clear
clearButton.addEventListener('click', () => {
itemCount = 0;
total = 0;
updateStatus();
});0 items — RM 0.00Step 5 — Show what was added
Combine with Lesson 15 and build a real order list:
const orderList = document.createElement('ul');
document.body.append(orderList);
for (const button of addButtons) {
button.addEventListener('click', (event) => {
const li = document.createElement('li');
li.textContent = event.target.dataset.name;
orderList.append(li);
});
}What changed? Each click now adds a row to the page, not just a number. Notice that an element can have more than one listener for the same event — both your click handlers run, in the order you added them.
Try It Yourself
13 minWork on order.html and app.js.
Task 1 — A toggle button
Add a “Night mode” button that toggles a
.darkclass ondocument.body. Write the class in your CSS with a dark background and light text.Use
.classList.toggle()from Lesson 15 — one line inside the listener.Task 2 — Remove the last item
Add an “Undo” button that takes one off the count and subtracts the price. Guard it so the total can never go below zero.
You will need an
iffrom Lesson 6, and somewhere to remember the last price added.Task 3 — Listen for a key
Add a
keydownlistener ondocumentso pressing c clears the order, exactly like the button.Print
e.keyfirst to see what arrives. Then avoid duplicating code by having both the button and the key call the same named function.
🔥 Mini-Challenge · The Button That Fires Once
8 minLakshmi's counter shows “1” on load and never moves. Find three mistakes.
<!-- lakshmi-counter.html -->
<p id="count">0</p>
<button id="add">Add one</button>// lakshmi-counter.js — buggy
const output = document.querySelector('#count');
const button = document.querySelector('#add');
function bump() {
let clicks = 0;
clicks = clicks + 1;
output.textContent = clicks;
}
button.addEventListener('click', bump());It works if: the page shows 0 at first, then 1, 2, 3… on each click.
Reveal the answer
Mistake 1 — the function is called, not handed over.
bump() runs immediately on load — which is why the page shows 1 before anyone clicks. Its return value (undefined) is then registered as the listener, so clicking does nothing at all. Drop the brackets.
Mistake 2 — the counter is declared inside.
let clicks = 0; lives inside bump, so it is created fresh on every call. It can only ever reach 1. Move it outside, where it survives between clicks.
Mistake 3 — nothing shows the starting value.
Minor, but the HTML says 0 while JavaScript knows nothing about it. If the starting count ever changed, the two would disagree. Render from the data at the start.
// lakshmi-counter.js — fixed
const output = document.querySelector('#count');
const button = document.querySelector('#add');
let clicks = 0; // outside: it remembers
function bump() {
clicks = clicks + 1;
render();
}
function render() {
output.textContent = clicks;
}
render(); // page matches the data from the start
button.addEventListener('click', bump); // no brackets!0 (on load)
1 (first click)
2 (second click)Two habits to keep: no brackets when handing over a function, and state lives outside the listener. Both come back word for word in React.
Recap
3 minel.addEventListener(type, callback)— who listens, for what, do this.- Hand over the function without brackets.
fn()runs it now and registers its return value. - The event object describes what happened;
event.targetis the element involved. - One listener can serve many elements, using
event.targetto tell them apart. - Common types:
click,input,change,keydown. .valuereads what a visitor typed — always a string.- Anything remembered between events must live outside the callback.
- An element can carry several listeners for the same event.
New words
- Event — something that happened: a click, a key, a change.
- Listener / handler — the function that runs when it happens.
- event.target — the element the event came from.
📦 Homework
4 min to brief · ~20 min to doRequired
- Extend your
shop.htmlso every product has an “Add to basket” button. Track the count and total outside the listeners, and update a status line on every click. - Add a “Clear basket” button. Bring the file next lesson.
Optional stretch
- Attach one listener to the
<ul>instead of each button, and useevent.targetto work out which was clicked. This is called event delegation — why is it better when items are added later? - Try
mouseoveranddblclick. What is the third argumentaddEventListenercan take?