Working with Forms
Forms are how visitors talk to your site: booking a seat, sending an enquiry, signing up. HTML already gives you a lot for free, including fields, keyboard support and basic validation. JavaScript adds the rest: reading the values without a page reload, showing live totals, checking rules HTML cannot express, and sending the data to a server.
In this lesson you will handle the submit event, read every field with FormData, react to typing with the input event, and add your own validation messages. You should know how to build forms from HTML Forms and how listeners work from Events.
Handle submit, not click
Listen for the submit event on the form, not the click event on the button. A form can be submitted by clicking the button or by pressing Enter in a text field, and only submit catches both. Then call event.preventDefault() so the browser does not reload the page.
Reading values with FormData
new FormData(form) collects every named field in one go. Each field needs a name attribute: fields without one are skipped. Object.fromEntries() turns the result into a plain object.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Booking form</title>
<style>
label { display: block; margin-block: 0.5rem; }
fieldset { margin-block: 0.5rem; }
</style>
</head>
<body>
<h1>Book a train seat</h1>
<form id="booking">
<label>Passenger name <input name="passenger" required></label>
<label>Age <input name="age" type="number" min="1" max="120" required></label>
<label>To
<select name="destination">
<option>Jaipur</option>
<option>Lucknow</option>
<option>Kochi</option>
</select>
</label>
<fieldset>
<legend>Meal</legend>
<label><input type="radio" name="meal" value="veg" checked> Veg</label>
<label><input type="radio" name="meal" value="non-veg"> Non-veg</label>
</fieldset>
<label><input type="checkbox" name="sms" value="yes"> Send me SMS updates</label>
<button>Book</button>
</form>
<p id="summary"></p>
<script>
const form = document.querySelector("#booking");
const summary = document.querySelector("#summary");
form.addEventListener("submit", (event) => {
event.preventDefault();
const data = new FormData(form);
const booking = Object.fromEntries(data);
const age = Number(booking.age);
const seniorNote = age >= 60 ? " (senior citizen)" : "";
const sms = data.has("sms") ? "SMS updates on" : "no SMS";
summary.textContent =
`${booking.passenger}, age ${age}${seniorNote}, to ${booking.destination}, ` +
`${booking.meal} meal, ${sms}.`;
});
</script>
</body>
</html>Fill in the form and click Book. For a 64-year-old passenger going to Kochi with SMS ticked, the page shows "Isha, age 64 (senior citizen), to Kochi, veg meal, SMS updates on." Try leaving the age empty: the browser shows its own message and your submit listener never runs, because the field is required.
Four facts about form values save a lot of debugging:
- Every value is a string, even from
type="number". Convert withNumber()before doing maths. - A radio group gives the
valueof the checked option. - An unticked checkbox is not sent at all, so
data.get("sms")isnull. Usedata.has()to test it. - Several checkboxes with the same name need
data.getAll("name"), which returns an array.
Reading single fields
You can also reach any field through form.elements and its name. This is handy when you need one value, or a value as a number:
const form = document.querySelector("#booking");
form.elements.passenger.value; // "Isha" (a string)
form.elements.age.valueAsNumber; // 64 (a number, or NaN when empty)
form.elements.sms.checked; // true or false
form.elements.meal.value; // the checked radio's value
form.elements.destination.value; // the selected option's valueUse form.elements.age rather than form.age. Form elements have their own properties, such as action, method and submit, and a field with one of those names would hide them. For the same reason, never name a field submit, and avoid names like item and length, which clash with built-in properties of form.elements.
Live feedback with the input event
The input event fires on every change: each key press, each paste, each new option chosen. The change event fires less often, when the user finishes and leaves the field. Because input bubbles, one listener on the form hears every field.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Live total</title>
</head>
<body>
<h1>Order sweets</h1>
<form id="order">
<label>Box
<select name="box">
<option value="450">Kaju katli, 500 g (₹450)</option>
<option value="320">Motichoor ladoo, 500 g (₹320)</option>
<option value="280">Soan papdi, 500 g (₹280)</option>
</select>
</label>
<label>Quantity <input name="qty" type="number" value="1" min="1" max="20"></label>
<p>Total: <output name="total">₹450</output></p>
</form>
<script>
const form = document.querySelector("#order");
// "input" bubbles, so one listener on the form hears every field.
form.addEventListener("input", () => {
const price = Number(form.elements.box.value);
const qty = form.elements.qty.valueAsNumber; // a number, or NaN if empty
if (Number.isNaN(qty) || qty < 1) {
form.elements.total.value = "Enter a quantity";
return;
}
form.elements.total.value = `₹${(price * qty).toLocaleString("en-IN")}`;
});
</script>
</body>
</html>Set the quantity to 3 with kaju katli selected, and the total shows ₹1,350. Clear the box and it asks for a quantity. The <output> element is made for calculated results, and screen readers can announce its changes.
Validation: HTML first, JavaScript for the rest
Start with HTML attributes such as required, type="email", min, max, minlength and pattern (see Form Validation). The browser checks them before it fires submit, shows helpful messages in the user's language, and works even if your script fails to load.
JavaScript steps in for rules that HTML cannot describe, such as "both passwords must match". The key method is field.setCustomValidity(message): a non-empty message marks the field as invalid and becomes the error text, and an empty string marks it valid again.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Custom validation</title>
<style>
label { display: block; margin-block: 0.5rem; }
input:user-invalid { outline: 2px solid crimson; }
</style>
</head>
<body>
<h1>Create an account</h1>
<form id="signup">
<label>Mobile number
<input name="mobile" type="tel" inputmode="numeric" required
pattern="[6-9][0-9]{9}" title="10 digits, starting with 6, 7, 8 or 9">
</label>
<label>Password <input name="password" type="password" required minlength="8"></label>
<label>Confirm password <input name="confirm" type="password" required></label>
<button>Sign up</button>
</form>
<p id="message"></p>
<script>
const form = document.querySelector("#signup");
const { password, confirm } = form.elements;
function checkMatch() {
// An empty string means "valid"; any other text marks the field invalid.
confirm.setCustomValidity(
confirm.value === password.value ? "" : "Passwords do not match"
);
}
password.addEventListener("input", checkMatch);
confirm.addEventListener("input", checkMatch);
form.addEventListener("submit", (event) => {
event.preventDefault(); // the browser only fires submit if every field is valid
const button = form.querySelector("button");
button.disabled = true; // stop double submits
document.querySelector("#message").textContent =
`Account created for ${form.elements.mobile.value}.`;
});
</script>
</body>
</html>Other useful tools:
form.checkValidity()returnstrueorfalsewithout showing messages;form.reportValidity()also shows them.field.validitytells you why a field is invalid, with flags such asvalueMissing,typeMismatch,tooShortandpatternMismatch.- The CSS
:user-invalidselector styles a field only after the user has interacted with it, so an empty form does not start out covered in red.
Sending the data
To send a form to a server without reloading, pass the FormData object straight to fetch(). You will learn the details in Fetch API.
const response = await fetch("/api/enquiry", {
method: "POST",
body: new FormData(form), // sent in the same format as a normal form submit
});Common mistakes
- Listening for
clickon the button. Pressing Enter submits the form without a click. Listen forsubmiton the form. - Forgetting
preventDefault(). The page reloads and your result vanishes. - Doing maths on strings.
"2" + 1is"21". UseNumber()orvalueAsNumber. - Reading values when the page loads. Read them inside the listener, when the user has actually typed something.
- Calling
form.submit(). It skips validation and yoursubmitlisteners. Useform.requestSubmit()if you need to submit from code.
Key points
- Handle the form's
submitevent and callpreventDefault()to stay on the page. new FormData(form)reads every named field;Object.fromEntries()turns it into an object.- Form values are strings; unticked checkboxes are missing; use
getAll()for groups. - The
inputevent gives live feedback, and it bubbles to the form. - Use HTML validation first, then
setCustomValidity()for custom rules. - Always validate again on the server.
Exercise
Build a feedback form with a <textarea name="message" maxlength="160"> and a paragraph that shows "120 characters left" as the user types (update it on the input event). On submit, prevent the reload, show "Thanks! We received 40 characters." with the real number, and reset the form.
Show solution
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Feedback</title>
</head>
<body>
<form id="feedback">
<textarea name="message" maxlength="160" required></textarea>
<p id="left">160 characters left</p>
<button>Send</button>
</form>
<p id="thanks"></p>
<script>
const form = document.querySelector("#feedback");
const message = form.elements.message;
const left = document.querySelector("#left");
message.addEventListener("input", () => {
left.textContent = `${160 - message.value.length} characters left`;
});
form.addEventListener("submit", (event) => {
event.preventDefault();
document.querySelector("#thanks").textContent =
`Thanks! We received ${message.value.length} characters.`;
form.reset();
left.textContent = "160 characters left";
});
</script>
</body>
</html>form.reset() does not fire an input event, so the counter is reset by hand.
Quick Quiz
1. Why should you listen for submit on the form instead of click on the button?
The submit event catches every way a form can be sent, including the Enter key.
2. What does data.get("sms") return when the sms checkbox is not ticked?
Unticked checkboxes are not included in the form data, so get() returns null.
3. How do you mark a field invalid with your own message?
A non-empty message makes the field invalid and becomes its error text; an empty string makes it valid again.
4. form.elements.qty.value is "3". What is form.elements.qty.value + 2?
Form values are strings, so + joins them. Convert with Number() or use valueAsNumber.
Learn JavaScript with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
