JavaScript course menu
Handling Forms with JavaScript: submit, preventDefault, FormData and Validation
Learn to handle HTML forms with JavaScript: listening for submit, preventing page reloads, reading values with input.value and FormData, checkboxes and selects, input and change events, custom validation messages with the Constraint Validation API, and resetting forms.
On this page
Forms collect input; JavaScript can read that input, check it, show feedback, and send it to a server without reloading the page. If you have not met HTML forms yet, read HTML Forms first.
Listening for submit#
Always listen for the form's submit event, not a button's click. submit fires whether the user clicks the button or presses Enter in a field:
<form id="signup">
<p><label>Name <input name="name" required></label></p>
<p><label>Email <input name="email" type="email" required></label></p>
<button type="submit">Sign up</button>
</form>
<p id="out"></p>const form = document.querySelector("#signup");
form.addEventListener("submit", (event) => {
event.preventDefault(); // stop the page from reloading
const name = form.elements.name.value.trim();
document.querySelector("#out").textContent = `Thanks, ${name}! Check your email.`;
form.reset();
});event.preventDefault()stops the browser's normal submission, which would load a new page.- The browser's built-in validation (
required,type="email") still runs before thesubmitevent, so the handler only runs with valid data. form.elements.namefinds the field withname="name";.valueis its current text (always a string).form.reset()restores all fields to their starting values.
Reading all fields with FormData#
FormData collects every named field at once:
<form id="order">
<p><label>Item <select name="item"><option>Tea</option><option>Coffee</option></select></label></p>
<p><label>Quantity <input name="qty" type="number" value="1" min="1"></label></p>
<p><label><input type="checkbox" name="sugar" checked> Sugar</label></p>
<p>
<label><input type="radio" name="size" value="small" checked> Small</label>
<label><input type="radio" name="size" value="large"> Large</label>
</p>
<button>Order</button>
</form>
<pre id="out"></pre>const form = document.querySelector("#order");
form.addEventListener("submit", (event) => {
event.preventDefault();
const data = Object.fromEntries(new FormData(form));
data.qty = Number(data.qty);
data.sugar = data.sugar === "on";
document.querySelector("#out").textContent = JSON.stringify(data, null, 2);
});new FormData(form)reads all fields that have aname.Object.fromEntriesturns it into a plain object.- Values are strings: convert numbers with
Number(). An unticked checkbox is missing entirely, and a ticked one gives"on"unless it has avalue. - For fields with several values (several checkboxes with the same name), use
formData.getAll("name").
A FormData object can also be sent directly with fetch: fetch("/api/order", { method: "POST", body: new FormData(form) }).
Reacting as the user types#
<label>Password <input id="pwd" type="password"></label>
<ul id="rules">
<li data-rule="length">At least 8 characters</li>
<li data-rule="digit">Contains a number</li>
</ul>const pwd = document.querySelector("#pwd");
const checks = {
length: (v) => v.length >= 8,
digit: (v) => /\d/.test(v),
};
pwd.addEventListener("input", () => {
for (const li of document.querySelectorAll("#rules li")) {
const ok = checks[li.dataset.rule](pwd.value);
li.style.color = ok ? "seagreen" : "";
li.textContent = li.textContent.replace(/^✓ /, "");
if (ok) li.textContent = "✓ " + li.textContent;
}
});Custom validation messages#
The Constraint Validation API lets you add your own rules while keeping the browser's validation UI:
<form id="reg">
<p><label>Password <input id="p1" type="password" required minlength="6"></label></p>
<p><label>Confirm <input id="p2" type="password" required></label></p>
<button>Register</button>
</form>
<p id="out"></p>const form = document.querySelector("#reg");
const p1 = document.querySelector("#p1");
const p2 = document.querySelector("#p2");
function checkMatch() {
p2.setCustomValidity(p1.value === p2.value ? "" : "Passwords do not match");
}
p1.addEventListener("input", checkMatch);
p2.addEventListener("input", checkMatch);
form.addEventListener("submit", (event) => {
event.preventDefault();
document.querySelector("#out").textContent = "Registered!";
});setCustomValidity("message")marks the field invalid with your message;setCustomValidity("")marks it valid again.form.checkValidity()returns whether every field is valid;form.reportValidity()also shows the messages.
Client-side checks are for the user's convenience. The server must always validate the data again.
Accessible error messages#
When you show your own error text instead of the browser's, connect it to the field so screen readers announce it:
<input id="phone" aria-describedby="phone-error" aria-invalid="true">
<p id="phone-error">Enter a 10-digit mobile number.</p>Practice#
Practice exercise
Build a "split the bill" form with an amount field and a number-of-people field (minimum 1). On submit, show each person's share with two decimals, without reloading the page.
Show solution
<form id="split">
<p><label>Bill amount (₹) <input name="amount" type="number" min="0" step="0.01" required></label></p>
<p><label>People <input name="people" type="number" min="1" value="2" required></label></p>
<button>Split</button>
</form>
<p id="out"></p>const form = document.querySelector("#split");
form.addEventListener("submit", (event) => {
event.preventDefault();
const { amount, people } = Object.fromEntries(new FormData(form));
const share = Number(amount) / Number(people);
document.querySelector("#out").textContent = `Each person pays ₹${share.toFixed(2)}`;
});Key takeaway#
Finished this lesson?