Chapter 5 · The web page
Events
Step 1 of 22 · Concept
A page that answers
By the end of this lesson your pages answer what the user does. You will make:
- a button that counts its clicks,
- a form that reads what is typed,
- a list that redraws itself from an array.
Step 2 of 22 · Concept
Listen for a click
const button = document.querySelector("#hello");
button.addEventListener("click", () => {
console.log("clicked");
});
console.log("ready");
A click is an event. addEventListener("click", ...) hands the button a function to call on every click: its
handler. It does not call the function now.
The script runs on to its end and logs ready. Later, each click on the button runs the handler. A handler is a
callback (lesson 12): you write it, the browser calls it.
Step 3 of 22 · Quiz
When does the handler run?
The page has loaded, and nobody has clicked yet. What does Output show?
const button = document.querySelector("#hello");
button.addEventListener("click", () => {
console.log("clicked");
});
console.log("ready");
Step 4 of 22 · Demo
See it answer
Run it and compare with your answer. Then click Hello twice in the Page tab: each click adds clicked to
Output.
Step 5 of 22 · Concept
A click is a callback in the queue
The script has ended, so the stack is empty. The browser keeps the button's listener: the event and its handler.
- A click puts the handler in the callback queue.
- The event loop takes it to the stack, and it runs.
A setTimeout callback runs once. A listener stays, so every click queues the handler again.
Step 6 of 22 · Concept
The handler keeps its variable
let count = 0;
const more = document.querySelector("#more");
const output = document.querySelector("#count");
more.addEventListener("click", () => {
count++;
output.textContent = `Count: ${count}`;
});
The count lives in a variable, count, beside the handler. Each click adds 1 to it and shows the new count in the
p whose id is count.
Step 7 of 22 · Trace
Watch the count grow
The page has a p with Count: 0 and a button More. Press Next: the script has run, then come two clicks.
let count = 0;
const more = document.querySelector("#more");
const output = document.querySelector("#count");
more.addEventListener("click", () => {
count++;
output.textContent = `Count: ${count}`;
});- The script ran to its end: the stack is empty. The handler keeps
count, 0. Line 5 runs at a click. - Click 1: the handler's frame is on the stack.
count++madecount1. Line 6 is next. - Line 6 put
Count: 1in the page. The handler returned, and the stack is empty. - Click 2: the handler runs again, with the same
count. It is 2 now. - The page shows
Count: 2. The stack is empty, and the page waits for the next click.
Step 8 of 22 · Exercise
Your turn: plus and minus
Make the two buttons change one count:
- A click on + adds 1 to
count. - A click on − takes 1 away.
- Each click shows the new count,
Count: 2, in#count.
count lives outside both handlers, so both change the same count. A let count inside a handler would start again at 0 on every click.
One addEventListener("click", ...) per button. In each handler, change count (count++ or count -= 1), then set output.textContent.
minus.addEventListener("click", () => {
Step 9 of 22 · Concept
The handler gets the event
for (const button of document.querySelectorAll("button")) {
button.addEventListener("click", (event) => {
console.log(event.target.textContent);
});
}
The browser calls a handler with one argument: the event object, which describes what happened.
event.target is the element the event happened to: here, the button that was clicked. So the same handler can
serve every button, and still tell them apart.
Step 10 of 22 · Demo
Which button?
Each of the three buttons gets the same handler. Run it, then click Two and Three in the Page tab: Output names the button you clicked.
Step 11 of 22 · Concept
input.value is always text
<label>Amount <input id="amount" value="20"></label>
An <input> is a field to type in. Its value attribute is the text it starts with, and a <label> names it.
- In the script,
input.valueis what the field holds now. It is always text, even20. - So
Number(...)it before you add (lesson 4). - The field's
inputevent fires each time its text changes.
Step 12 of 22 · Quiz
20 plus 1
The page has <input id="amount" value="20">. What does the script log?
const input = document.querySelector("#amount");
console.log(input.value + 1);
Step 13 of 22 · Demo
See it run
Run it and compare Output with your answer. Then type another number in the field: nothing new is logged. The script ran once, when the page loaded.
Step 14 of 22 · Try it
Try it: add ten
Run it and type 20 in the field: the page shows 2010. Fix the handler so that it shows 30.
Wrap the field's text in Number(...): Number(input.value) + 10.
Step 15 of 22 · Concept
A form sends a submit event
<form id="greet">
<label>Name <input id="name"></label>
<button>Greet</button>
</form>
A <form> groups fields with their button. Pressing the button, or Enter in a field, sends the form a submit
event.
A browser would then load the page again, and the page would start over. So the handler calls
event.preventDefault() first. Without it, the Page tab keeps the page, and Output says the form was submitted.
Step 16 of 22 · Demo
Greet whoever types
Run it, type your name in the Page tab, and press Greet or Enter. The handler reads the field and greets you.
Step 17 of 22 · Concept
Keep state, then render
const items = [];
const list = document.querySelector("#items");
function render() {
list.replaceChildren();
for (const item of items) {
const li = document.createElement("li");
li.textContent = item;
list.append(li);
}
}
The page's data lives in variables: its state. Every handler changes the state, then calls render(), which
rebuilds the page from it. list.replaceChildren() empties the list, and the loop adds one li per item.
Step 18 of 22 · Demo
A list that redraws itself
Run it, then add milk and eggs in the Page tab. Each submit pushes the item into items, empties the field and
calls render().
Step 19 of 22 · Exercise
Your turn: a guest list
Make the form keep a list of guests:
- On
submit, add the typed name toguests, then callrender(). render()lists the guests, onelieach.render()also puts the number of guests in#count:2 guests.
Without event.preventDefault() the page would start over in a browser, and the list would be gone. Call it first in the submit handler.
The shopping list's render() and handler have the same shape: replaceChildren(), then a for...of that appends an li. After the loop, a template literal with guests.length.
form.addEventListener("submit", (event) => {
Step 20 of 22 · Check yourself
Check yourself: split the bill
Each bill added with the form joins the list, and #total shows the sum with two decimals. After 12.50 and 7.50
the page must show:
• 12.50
• 7.50
Total: 20.00
The guest list's shape: the state is an array of numbers, the handler pushes and renders, and render() adds up the bills as it lists them.
Step 21 of 22 · Challenge · optional
Challenge: no empty names
Optional.
Add adds a name even when the field is empty. Make the button do nothing until something is typed. While the field is empty or holds only spaces, grey it out.
A button has a property disabled. While it is true, the button is grey, and a click on it does nothing: no submit either.
Set add.disabled in two places. One is render(), which runs at load and after each add, when the field is empty. The other is an input handler on the field. trim() makes " " empty.
add.disabled = name.value.trim() === "";
Step 22 of 22 · Recap
Recap
addEventListener("click", () => { ... })hands an element a handler, for later.- A click queues the handler; the event loop runs it once the stack is empty.
- A handler keeps the variables around it;
event.targetis the element clicked. input.valueis always text. Asubmithandler callsevent.preventDefault()first.- Keep the data in variables, change it in handlers, then
render()the page from it.
Next: lesson 23 turns lesson 20's to-do list into a page.
Review
2 questions from this lesson, in a new order each time. Answer from memory: 2 of 2 right on the first try passes. On a day after you finish the lesson, a pass earns 15 XP.
The review needs JavaScript.
const button = document.querySelector("#hello");
button.addEventListener("click", () => {
console.log("clicked");
});
console.log("ready");
let count = 0;
const plus = document.querySelector("#plus");
const minus = document.querySelector("#minus");
const output = document.querySelector("#count");
// 1. a click on plus adds 1 to count
// 2. a click on minus takes 1 away
// 3. both show Count: and the count in output
for (const button of document.querySelectorAll("button")) {
button.addEventListener("click", (event) => {
console.log(event.target.textContent);
});
}
const input = document.querySelector("#amount");
console.log(input.value + 1);
const input = document.querySelector("#amount");
const result = document.querySelector("#result");
input.addEventListener("input", () => {
result.textContent = input.value + 10;
});
const form = document.querySelector("#greet");
const name = document.querySelector("#name");
const hello = document.querySelector("#hello");
form.addEventListener("submit", (event) => {
event.preventDefault();
hello.textContent = `Hello, ${name.value}`;
});
const items = [];
const form = document.querySelector("#add");
const input = document.querySelector("#item");
const list = document.querySelector("#items");
function render() {
list.replaceChildren();
for (const item of items) {
const li = document.createElement("li");
li.textContent = item;
list.append(li);
}
}
form.addEventListener("submit", (event) => {
event.preventDefault();
items.push(input.value);
input.value = "";
render();
});
const guests = [];
const form = document.querySelector("#guest-form");
const name = document.querySelector("#name");
const count = document.querySelector("#count");
const list = document.querySelector("#guests");
function render() {
// 2. one li per guest
// 3. the number of guests in count
}
// 1. on submit: add the typed name, then render
const bills = [];
const names = [];
const form = document.querySelector("#name-form");
const name = document.querySelector("#name");
const add = document.querySelector("#add");
const list = document.querySelector("#names");
function render() {
list.replaceChildren();
for (const n of names) {
const li = document.createElement("li");
li.textContent = n;
list.append(li);
}
}
form.addEventListener("submit", (event) => {
event.preventDefault();
names.push(name.value);
name.value = "";
render();
});
render();