Learning Goals
3 minBy the end of this lesson you will be able to:
- Read what a visitor typed with
.value. - Stop a form reloading the page with
event.preventDefault(). - Validate input and show a helpful message when it is wrong.
- Add a typed item to a list on the page, then clear the box.
Warm-Up · The Vanishing Page
5 minLast lesson your buttons responded to clicks. Now try the same thing on a form.
<form id="order-form">
<input id="dish" />
<button>Add</button>
</form>document.querySelector('#order-form')
.addEventListener('submit', () => {
console.log('Adding the dish…');
});Predict together
- Type something and press Enter. What happens to the page?
- The console message flashes and disappears. Why might that be?
Reveal
The page reloads. That is a form's built-in behaviour from long before JavaScript: gather the fields, send them to a server, load a fresh page.
Your listener does run — but the reload wipes the console a split second later. Today's key line stops that reload and keeps you in control.
New Concept · Taking the Order
12 minA waiter takes your order on a pad. They check it makes sense — you cannot order a dish that is not on the menu — before walking to the kitchen. Reading, checking, then acting: that is form handling.
Reading a field
const input = document.querySelector('#dish');
console.log(input.value);.value is what is in the box right now. Two things to remember, both of which have caught you before:
- It is always a string, even from a number field —
Number()it before any maths (Lesson 4). - An empty box gives
'', which is falsy (Lesson 6), soif (input.value)reads nicely.
preventDefault — take the wheel
form.addEventListener('submit', (event) => {
event.preventDefault(); // stop the reload
console.log('Now I am in charge.');
});event.preventDefault() tells the browser “do not do your usual thing”. For a form, the usual thing is reloading. Put it as the first line of every submit handler you write.
Without preventDefault
Page reloads on submit.
Your changes are wiped.
The console clears before you can read it.
With preventDefault
Page stays put.
You decide what happens.
This is how every modern web app behaves.
Listen on the form, not the button
Use submit on the <form>, not click on the button. Submitting also happens when somebody presses Enter in a text box — and a click listener misses that entirely.
Validating before you act
Never trust what arrives. Check it, and say something useful when it is wrong.
form.addEventListener('submit', (event) => {
event.preventDefault();
const name = input.value.trim(); // strip stray spaces
if (!name) {
message.textContent = 'Please type a dish name.';
return; // stop here
}
message.textContent = `Added ${name}.`;
input.value = ''; // clear the box
});Three habits in that snippet, all worth keeping:
.trim()first, so “ ” does not count as typing (Lesson 3).returnearly when it is invalid — noelseneeded, and the rest cannot run.- Clear the box after success, so the next entry is quick.
Say what is wrong, kindly
“Invalid input” helps nobody. Name the field and the rule: “Quantity must be between 1 and 20.” Put the message near the field, and clear it once they fix it.
Why it matters
Every login, search, checkout and sign-up is this pattern. In Lesson 32 React does it with controlled forms, where state — not the input — is the source of truth. Recognising preventDefault there will make that lesson far easier.
Worked Example · An Order Form
12 minSave as form.html, with app.js beside it.
<body>
<h1>Warung Aisyah</h1>
<form id="order-form">
<label for="dish">Dish</label>
<input id="dish" type="text" />
<label for="qty">Quantity</label>
<input id="qty" type="number" value="1" />
<button type="submit">Add to order</button>
</form>
<p id="message"></p>
<ul id="order"></ul>
<script src="app.js"></script>
</body>Step 1 — Stop the reload
// app.js
const form = document.querySelector('#order-form');
const dishInput = document.querySelector('#dish');
const qtyInput = document.querySelector('#qty');
const message = document.querySelector('#message');
const orderList = document.querySelector('#order');
form.addEventListener('submit', (event) => {
event.preventDefault();
console.log('Dish:', dishInput.value, 'Qty:', qtyInput.value);
});Type “nasi lemak”, quantity 2, and submit:
Dish: nasi lemak Qty: 2The page stays put and the message survives. Notice Qty is the string '2', even though the field is type="number".
Step 2 — Validate the dish
form.addEventListener('submit', (event) => {
event.preventDefault();
const dish = dishInput.value.trim();
if (!dish) {
message.textContent = 'Please type a dish name.';
dishInput.focus(); // put the cursor back
return;
}
message.textContent = '';
});Submit with an empty box and the message appears, with the cursor waiting in the field. .focus() is a small kindness that makes a form feel well made.
Step 3 — Validate the quantity
const MAX_QTY = 20;
const qty = Number(qtyInput.value);
if (!Number.isInteger(qty) || qty < 1 || qty > MAX_QTY) {
message.textContent = `Quantity must be a whole number from 1 to ${MAX_QTY}.`;
qtyInput.focus();
return;
}Three separate ways to be wrong, one clear message naming the rule. Try 0, 25 and 2.5 to see each guard fire.
Step 4 — Add it to the page
Lesson 15's create-fill-attach, driven by typed input:
const li = document.createElement('li');
li.textContent = `${qty} × ${dish}`;
orderList.append(li);
message.textContent = `Added ${qty} × ${dish}.`;
dishInput.value = '';
qtyInput.value = '1';
dishInput.focus();2 × nasi lemak
1 × cendolStep 5 — Live feedback while typing
Waiting for submit to complain is annoying. Clear the message the moment they start fixing it:
dishInput.addEventListener('input', () => {
if (dishInput.value.trim()) {
message.textContent = '';
}
});What changed? The form now corrects itself as the visitor types, instead of scolding them once at the end. Two events on two elements, cooperating — submit to act, and input to reassure.
Try It Yourself
13 minWork on form.html and app.js.
Task 1 — A running total
Add a price field to the form. Keep a total outside the listener, add to it on each successful submit, and show it under the list as
Total: RM 12.50.Remember to
Number()the price before adding it.Task 2 — A dropdown instead
Replace the dish text box with a
<select>holding four dishes. Read its.valuethe same way, and remove the now-unnecessary empty check.Add a first option of
""reading “Choose a dish…” — and validate that they moved off it.Task 3 — A contact form
Build a second form with name, phone and a message box. Validate that the name is at least 2 characters and the phone is at least 9 digits.
Show one message per problem, near the field it belongs to. On success, print a summary and clear all three boxes.
🔥 Mini-Challenge · The Form That Runs Away
8 minAhmad's booking form reloads the page and accepts nonsense. Find three mistakes.
<!-- ahmad-booking.html -->
<form id="booking">
<input id="guests" type="number" />
<button id="go" type="submit">Book</button>
</form>
<p id="msg"></p>// ahmad-booking.js — buggy
const button = document.querySelector('#go');
const guests = document.querySelector('#guests');
const msg = document.querySelector('#msg');
button.addEventListener('click', (event) => {
const count = guests.value;
if (count > 0) {
msg.textContent = 'Booked for ' + count + ' guests';
} else {
msg.textContent = 'Enter a number';
}
});It works if: the page never reloads, pressing Enter works as well as clicking, and only whole numbers from 1 to 10 are accepted.
Reveal the answer
Mistake 1 — no preventDefault, so the page reloads.
The handler runs, sets the message, and then the form does its default submit. The page reloads and the message vanishes instantly. Every submit handler needs event.preventDefault() as its first line.
Mistake 2 — listening on the button, not the form.
Pressing Enter in the number field submits the form without any click, so the handler never runs — but the page still reloads. Listen for submit on the <form> and both routes are covered.
Mistake 3 — comparing a string to a number.
guests.value is text. '5' > 0 happens to work because JavaScript converts it — but 'abc' > 0 is false and '2.5' passes despite being half a guest. Convert with Number() and check the range properly.
// ahmad-booking.js — fixed
const form = document.querySelector('#booking');
const guests = document.querySelector('#guests');
const msg = document.querySelector('#msg');
const MAX_GUESTS = 10;
form.addEventListener('submit', (event) => {
event.preventDefault(); // stay on the page
const count = Number(guests.value); // text becomes a number
if (!Number.isInteger(count) || count < 1 || count > MAX_GUESTS) {
msg.textContent = `Please enter a whole number from 1 to ${MAX_GUESTS}.`;
guests.focus();
return;
}
msg.textContent = `Booked for ${count} guests.`;
guests.value = '';
});Booked for 4 guests.The reload bug is the sneaky one: the code looks like it works for a split second before the page wipes it. If a form seems to “do nothing”, check preventDefault first.
Recap
3 min.valuereads a field, and is always a string.- Listen for
submiton the form, notclickon the button — Enter counts too. event.preventDefault()stops the reload. First line, every time..trim()before checking, so spaces do not pass as input.returnearly on invalid input; noelseneeded.- Convert with
Number()before comparing or calculating. - Messages should name the field and the rule, and clear once fixed.
- Clear the fields and
.focus()after a successful submit.
New words
- Submit event — fired when a form is sent, by click or Enter.
- preventDefault — stops the browser's built-in response.
- Validation — checking input makes sense before acting on it.
📦 Homework
4 min to brief · ~20 min to doRequired
- Build
signup.html: a form taking a name, an age and a favourite dish from a dropdown. Validate all three, show a clear message per problem, and on success add a line to a list on the page. - Make sure pressing Enter works as well as clicking. Bring the file next lesson.
Optional stretch
- Add the HTML
requiredandminattributes to a field. The browser validates before your code even runs — why keep your own checks as well? - Look up
<input type="checkbox">and.checked. How does reading it differ from.value?