📝 Lesson 17: Forms & Validation
Forms are how users send data to your application — login credentials, search queries, registration info, feedback. In this lesson you'll learn to read form values with JavaScript, intercept submissions, and validate input before it goes anywhere.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Access form elements and read their values with JavaScript
- Handle the
submitevent and usepreventDefault() - Build validation rules for required fields, length, format, and matching
- Provide real-time validation feedback on
inputandblurevents - Display custom error messages and style valid/invalid fields
- Use the Constraint Validation API for built-in browser validation
- Combine everything into a complete, accessible validation system
Estimated Time: 55 minutes
Project: Build a registration form with real-time validation
📑 In This Lesson
Forms in HTML — A Quick Refresher
Before we dive into JavaScript, let's make sure the HTML side is solid. A <form> groups related inputs together and defines how and where data is submitted.
<form id="signup-form" action="/api/signup" method="POST">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
<button type="submit">Sign Up</button>
</form>
Common Input Types
HTML provides many built-in input types, each with its own keyboard behavior and basic validation.
| Type | Purpose | Example |
|---|---|---|
text | General single-line text | Name, username |
email | Email address (validates format) | user@example.com |
password | Hidden text input | Login password |
number | Numeric value with spinners | Age, quantity |
tel | Phone number (mobile keyboard) | 555-1234 |
url | URL (validates format) | https://example.com |
checkbox | On/off toggle | Agree to terms |
radio | One choice from a group | Plan selection |
textarea | Multi-line text (not an input!) | Comments, bio |
select | Dropdown menu (not an input!) | Country, category |
💡 Labels Matter
Always pair each input with a <label> using the for attribute. This isn't just a best practice — it's essential for accessibility. Screen readers use labels to describe inputs, and clicking a label focuses its associated input, making forms easier to use for everyone.
Reading Form Values with JavaScript
Every form element has a .value property that gives you whatever the user has typed or selected. This is how JavaScript reads form data.
Accessing Individual Inputs
// By ID (most common)
const username = document.querySelector("#username");
console.log(username.value); // Whatever the user typed
// The value is always a string — even for number inputs
const age = document.querySelector("#age");
console.log(age.value); // "25" (string!)
console.log(Number(age.value)); // 25 (number)
Different Input Types, Different Properties
// Text, email, password, number, tel, url → use .value
const email = document.querySelector("#email");
console.log(email.value); // "user@example.com"
// Checkbox → use .checked (boolean)
const terms = document.querySelector("#agree-terms");
console.log(terms.checked); // true or false
// Radio buttons → find the checked one
const plan = document.querySelector('input[name="plan"]:checked');
console.log(plan?.value); // "premium" (or undefined if none selected)
// Select dropdown → use .value
const country = document.querySelector("#country");
console.log(country.value); // "US"
// Textarea → use .value (same as text inputs)
const bio = document.querySelector("#bio");
console.log(bio.value); // Multi-line text string
Accessing Inputs Through the Form
You can also access inputs through the form element itself using the name attribute. This avoids needing individual IDs for every field.
const form = document.querySelector("#signup-form");
// Access by name attribute
console.log(form.elements.username.value);
console.log(form.elements.email.value);
// Or use bracket notation (useful for names with hyphens)
console.log(form.elements["first-name"].value);
// form.elements is an HTMLFormControlsCollection — it's live
console.log(form.elements.length); // Number of form controls
.username"] B --> D["By index
[0]"] B --> E["By ID
.email"] C --> F[".value"] D --> F E --> F style A fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b style B fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style C fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style D fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style E fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style F fill:#fce7f3,stroke:#ec4899,stroke-width:2px,color:#1e293b
Handling Form Submission
By default, submitting a form reloads the page (or navigates to the action URL). In modern JavaScript, we almost always want to prevent that default behavior and handle the data ourselves.
The submit Event & preventDefault()
const form = document.querySelector("#signup-form");
form.addEventListener("submit", (event) => {
// Stop the browser from reloading the page
event.preventDefault();
// Now we can handle the data ourselves
const username = form.elements.username.value;
const email = form.elements.email.value;
console.log("Submitted:", { username, email });
// Later: validate, then send to server via fetch()
});
⚠️ Why preventDefault() Is Essential
Without event.preventDefault(), the browser will navigate away from your page when the form is submitted. Your JavaScript code after the submit event won't have time to run — the page is already reloading. Always call it first in your submit handler.
Gathering All Form Data at Once
The FormData API gives you a convenient way to collect all form values without reading each input individually.
form.addEventListener("submit", (event) => {
event.preventDefault();
const formData = new FormData(form);
// Get individual values
console.log(formData.get("username"));
console.log(formData.get("email"));
// Convert to a plain object
const data = Object.fromEntries(formData);
console.log(data);
// { username: "ray", email: "ray@example.com", password: "..." }
// Loop through all entries
for (const [name, value] of formData) {
console.log(`${name}: ${value}`);
}
});
Resetting the Form
// After successful submission, clear the form
form.reset(); // Resets all fields to their initial values
// Or reset individual fields
form.elements.username.value = "";
(fetch)"] E -->|Invalid| G["Show error
messages"] F --> H["Show success &
reset form"] G --> I["User fixes
errors"] I --> A style A fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b style B fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style C fill:#fce7f3,stroke:#ec4899,stroke-width:2px,color:#1e293b style D fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style E fill:#f3e8ff,stroke:#a855f7,stroke-width:2px,color:#1e293b style F fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style G fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#1e293b style H fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style I fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b
Validation Patterns
Client-side validation gives users immediate feedback — no waiting for a server round-trip. Here are the most common validation checks you'll build.
Required Fields
function isRequired(value) {
return value.trim() !== "";
}
// Usage
if (!isRequired(username.value)) {
showError(username, "Username is required");
}
Length Validation
function isLengthValid(value, min, max) {
const length = value.trim().length;
return length >= min && length <= max;
}
// Username: 3–20 characters
if (!isLengthValid(username.value, 3, 20)) {
showError(username, "Username must be 3–20 characters");
}
// Password: at least 8 characters
if (password.value.length < 8) {
showError(password, "Password must be at least 8 characters");
}
Pattern Matching with Regular Expressions
// Email: basic format check
function isValidEmail(value) {
// Checks for something@something.something
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
// Username: letters, numbers, underscores only
function isValidUsername(value) {
return /^[a-zA-Z0-9_]+$/.test(value);
}
// Password: at least one uppercase, one lowercase, one digit
function isStrongPassword(value) {
const hasUpper = /[A-Z]/.test(value);
const hasLower = /[a-z]/.test(value);
const hasDigit = /\d/.test(value);
return hasUpper && hasLower && hasDigit && value.length >= 8;
}
Matching Fields
// Confirm password matches
function doFieldsMatch(value1, value2) {
return value1 === value2;
}
const password = form.elements.password.value;
const confirm = form.elements["confirm-password"].value;
if (!doFieldsMatch(password, confirm)) {
showError(confirmInput, "Passwords do not match");
}
Checkbox Validation
const terms = document.querySelector("#agree-terms");
if (!terms.checked) {
showError(terms, "You must agree to the terms");
}
Putting It Together: A Validate Function
function validateForm(form) {
let isValid = true;
// Clear previous errors
clearErrors(form);
const username = form.elements.username.value;
const email = form.elements.email.value;
const password = form.elements.password.value;
if (!isRequired(username)) {
showError(form.elements.username, "Username is required");
isValid = false;
} else if (!isLengthValid(username, 3, 20)) {
showError(form.elements.username, "Username must be 3–20 characters");
isValid = false;
}
if (!isRequired(email)) {
showError(form.elements.email, "Email is required");
isValid = false;
} else if (!isValidEmail(email)) {
showError(form.elements.email, "Please enter a valid email");
isValid = false;
}
if (!isRequired(password)) {
showError(form.elements.password, "Password is required");
isValid = false;
} else if (!isStrongPassword(password)) {
showError(form.elements.password, "Use 8+ chars with uppercase, lowercase, and a number");
isValid = false;
}
return isValid;
}
form.addEventListener("submit", (event) => {
event.preventDefault();
if (validateForm(form)) {
console.log("Form is valid — submit to server!");
}
});
⚠️ Client-Side Validation Is Not Enough
Client-side validation is for user experience — it gives instant feedback and prevents obvious mistakes. But it can always be bypassed (DevTools, disabled JavaScript, direct API calls). Always validate on the server too. Think of client-side validation as a helpful first line of defense, not a security boundary.
Real-Time Validation
Validating only on submit is the bare minimum. A better experience gives feedback as the user types or tabs away from a field.
Choosing the Right Event
| Event | When It Fires | Best For |
|---|---|---|
input | Every keystroke / change | Live character counts, password strength |
blur | User leaves the field | Full field validation (email, username) |
focus | User enters the field | Showing hints or clearing errors |
change | Value changes & field loses focus | Selects, checkboxes, radio buttons |
Validate on blur, Feedback on input
A common pattern: validate when the user leaves a field (blur) and provide live feedback as they type (input), but only after the first blur. This avoids showing errors before the user has even finished typing.
// Track which fields have been "touched" (blurred at least once)
const touched = new Set();
function setupField(input, validateFn) {
// Validate when the user leaves the field
input.addEventListener("blur", () => {
touched.add(input.name);
validateFn(input);
});
// Re-validate on input, but only if already touched
input.addEventListener("input", () => {
if (touched.has(input.name)) {
validateFn(input);
}
});
}
// Validate the email field
function validateEmail(input) {
const value = input.value.trim();
if (!value) {
showError(input, "Email is required");
} else if (!isValidEmail(value)) {
showError(input, "Please enter a valid email");
} else {
showSuccess(input);
}
}
setupField(form.elements.email, validateEmail);
Showing and Clearing Errors
function showError(input, message) {
// Remove any existing success state
input.classList.remove("input-success");
input.classList.add("input-error");
// Find or create the error message element
let error = input.parentElement.querySelector(".error-message");
if (!error) {
error = document.createElement("span");
error.classList.add("error-message");
error.setAttribute("role", "alert"); // Accessibility!
input.parentElement.append(error);
}
error.textContent = message;
}
function showSuccess(input) {
input.classList.remove("input-error");
input.classList.add("input-success");
const error = input.parentElement.querySelector(".error-message");
if (error) error.remove();
}
function clearErrors(form) {
form.querySelectorAll(".error-message").forEach(el => el.remove());
form.querySelectorAll(".input-error, .input-success").forEach(el => {
el.classList.remove("input-error", "input-success");
});
}
Styling Valid & Invalid States
<style>
.input-error {
border-color: #ef4444;
background-color: #fef2f2;
}
.input-success {
border-color: #22c55e;
background-color: #f0fdf4;
}
.error-message {
color: #ef4444;
font-size: 0.85rem;
margin-top: 0.25rem;
display: block;
}
</style>
✅ Accessibility Tips for Validation
- Use
role="alert"on error messages so screen readers announce them immediately - Associate errors with inputs using
aria-describedbypointing to the error element's ID - Don't rely only on color — add text messages and/or icons
- Focus the first invalid field after a failed submission attempt
touched?"} B -->|No| C["No feedback yet"] B -->|Yes| D["Validate on input"] D -->|Valid| E["✅ Green border"] D -->|Invalid| F["❌ Red border
+ error message"] G["User leaves field
(blur)"] --> H["Mark as touched"] H --> D style A fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b style B fill:#f3e8ff,stroke:#a855f7,stroke-width:2px,color:#1e293b style C fill:#f8fafc,stroke:#94a3b8,stroke-width:2px,color:#1e293b style D fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style E fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style F fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#1e293b style G fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b style H fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b
The Constraint Validation API
The browser has a built-in validation system called the Constraint Validation API. When you use HTML attributes like required, minlength, pattern, and type="email", the browser validates automatically and provides its own error messages. JavaScript can hook into this system.
HTML Validation Attributes
<input type="text" required minlength="3" maxlength="20"
pattern="[a-zA-Z0-9_]+"
title="Letters, numbers, and underscores only">
<input type="email" required>
<input type="number" min="1" max="120" step="1">
<input type="url" required>
Checking Validity in JavaScript
const input = document.querySelector("#email");
// Check if the input is valid
console.log(input.validity.valid); // true or false
// Check specific validity states
console.log(input.validity.valueMissing); // true if required & empty
console.log(input.validity.typeMismatch); // true if wrong type (e.g. not email)
console.log(input.validity.tooShort); // true if below minlength
console.log(input.validity.tooLong); // true if above maxlength
console.log(input.validity.patternMismatch); // true if doesn't match pattern
// Check the entire form at once
const form = document.querySelector("#signup-form");
console.log(form.checkValidity()); // true if ALL fields valid
Custom Validation Messages
const email = document.querySelector("#email");
email.addEventListener("invalid", (event) => {
// Prevent the browser's default tooltip
event.preventDefault();
// Show your own message
if (email.validity.valueMissing) {
showError(email, "We need your email to create your account");
} else if (email.validity.typeMismatch) {
showError(email, "That doesn't look like an email address");
}
});
// Or set a custom message for the browser's built-in tooltip
email.setCustomValidity("Please enter your company email");
// IMPORTANT: Clear it when valid, or the field stays "invalid"
email.addEventListener("input", () => {
email.setCustomValidity("");
});
Disabling Built-In Validation
If you want full control with your own custom validation, disable the browser's built-in tooltips with novalidate.
<!-- The browser won't show its own error messages -->
<form id="signup-form" novalidate>
<!-- HTML attributes still work with the Constraint API -->
<input type="email" required>
<button type="submit">Submit</button>
</form>
// You can still use checkValidity() and validity states
// but now YOU control what error messages look like
form.addEventListener("submit", (event) => {
event.preventDefault();
if (!form.checkValidity()) {
// Show your own custom errors
highlightInvalidFields(form);
} else {
// Form is valid — proceed
submitData(form);
}
});
💡 Which Approach Should You Use?
For simple forms (contact, newsletter signup), the built-in browser validation with a few HTML attributes might be all you need. For complex forms with custom rules (password strength, field matching, conditional fields), use novalidate and build your own validation — but you can still use the Constraint Validation API's validity object to check individual states.
Hands-on Exercise
🏋️ Exercise: Registration Form with Real-Time Validation
Objective: Build a registration form that validates username, email, password, and password confirmation in real time. Fields validate on blur, then re-validate on input. The form only submits when all fields are valid.
<!-- Save this as registration.html and open in a browser -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Registration Form</title>
<style>
* { box-sizing: border-box; margin: 0; }
body { font-family: system-ui, sans-serif; background: #f1f5f9; padding: 2rem; }
.form-container {
max-width: 450px; margin: 0 auto; background: white;
padding: 2rem; border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
}
h1 { margin-bottom: 1.5rem; color: #1e293b; }
.form-group { margin-bottom: 1.25rem; }
label {
display: block; font-weight: 600; margin-bottom: 0.35rem;
color: #334155; font-size: 0.9rem;
}
input {
width: 100%; padding: 0.6rem 0.75rem; border: 2px solid #cbd5e1;
border-radius: 8px; font-size: 1rem; transition: border-color 0.2s;
}
input:focus { outline: none; border-color: #6366f1; }
input.input-error { border-color: #ef4444; background: #fef2f2; }
input.input-success { border-color: #22c55e; background: #f0fdf4; }
.error-message {
color: #ef4444; font-size: 0.8rem; margin-top: 0.3rem; display: block;
}
.strength-meter {
height: 4px; border-radius: 2px; margin-top: 0.4rem;
background: #e2e8f0; overflow: hidden;
}
.strength-bar {
height: 100%; width: 0; border-radius: 2px;
transition: width 0.3s, background-color 0.3s;
}
.strength-text { font-size: 0.8rem; margin-top: 0.2rem; color: #64748b; }
button[type="submit"] {
width: 100%; padding: 0.75rem; background: #6366f1; color: white;
border: none; border-radius: 8px; font-size: 1rem;
cursor: pointer; margin-top: 0.5rem; font-weight: 600;
}
button[type="submit"]:hover { background: #4f46e5; }
button[type="submit"]:disabled { background: #94a3b8; cursor: not-allowed; }
.success-message {
text-align: center; color: #22c55e; font-weight: 600;
display: none; padding: 1rem;
}
</style>
</head>
<body>
<div class="form-container">
<h1>📋 Create an Account</h1>
<form id="register-form" novalidate>
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username"
placeholder="3–20 characters, letters/numbers/underscores">
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email"
placeholder="you@example.com">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password"
placeholder="At least 8 characters">
<div class="strength-meter">
<div class="strength-bar" id="strength-bar"></div>
</div>
<span class="strength-text" id="strength-text"></span>
</div>
<div class="form-group">
<label for="confirm-password">Confirm Password</label>
<input type="password" id="confirm-password" name="confirm-password"
placeholder="Re-enter your password">
</div>
<button type="submit">Create Account</button>
</form>
<div class="success-message" id="success-message">
✅ Account created successfully!
</div>
</div>
<script>
// TODO: Complete each task
// 1. Write helper functions:
// - showError(input, message) — adds "input-error" class
// and shows a .error-message span
// - showSuccess(input) — adds "input-success" class
// and removes any .error-message
// - clearErrors(form) — removes all error states
// 2. Write validation functions for each field:
// - validateUsername: required, 3–20 chars, letters/numbers/underscores
// - validateEmail: required, valid email format
// - validatePassword: required, 8+ chars, uppercase + lowercase + digit
// - validateConfirm: required, matches password
// 3. Write a password strength meter:
// - On "input" event on the password field
// - Score: +1 for length >= 8, +1 for uppercase, +1 lowercase,
// +1 digit, +1 special char
// - Update the width and color of #strength-bar
// - Update the text in #strength-text
// 4. Set up real-time validation:
// - Track "touched" fields (blur marks a field as touched)
// - On blur: validate the field
// - On input: re-validate only if already touched
// 5. Handle form submission:
// - preventDefault
// - Run all validations
// - If all valid: hide the form, show #success-message
// - If invalid: focus the first invalid field
</script>
</body>
</html>
💡 Hint
1: For showError, find an existing .error-message in the input's parent or create one with createElement("span"). Add role="alert" for accessibility.
2: Each validator takes an input element, checks input.value.trim(), calls showError or showSuccess, and returns true or false.
3: Use separate regex tests: /[A-Z]/, /[a-z]/, /\d/, /[^a-zA-Z0-9]/. Map score 0–1 to red, 2–3 to orange/yellow, 4–5 to green.
4: Use a Set to track touched field names. On blur, add to the set and validate. On input, check if the name is in the set before validating.
5: Call all four validators. If any returns false, find the first .input-error and call .focus() on it.
✅ Solution
const form = document.querySelector("#register-form");
const strengthBar = document.querySelector("#strength-bar");
const strengthText = document.querySelector("#strength-text");
const successMessage = document.querySelector("#success-message");
const touched = new Set();
// 1. Helper functions
function showError(input, message) {
input.classList.remove("input-success");
input.classList.add("input-error");
let error = input.parentElement.querySelector(".error-message");
if (!error) {
error = document.createElement("span");
error.classList.add("error-message");
error.setAttribute("role", "alert");
input.after(error);
}
error.textContent = message;
}
function showSuccess(input) {
input.classList.remove("input-error");
input.classList.add("input-success");
const error = input.parentElement.querySelector(".error-message");
if (error) error.remove();
}
function clearErrors(form) {
form.querySelectorAll(".error-message").forEach(el => el.remove());
form.querySelectorAll(".input-error, .input-success").forEach(el => {
el.classList.remove("input-error", "input-success");
});
}
// 2. Validation functions
function validateUsername(input) {
const value = input.value.trim();
if (!value) {
showError(input, "Username is required");
return false;
}
if (value.length < 3 || value.length > 20) {
showError(input, "Username must be 3–20 characters");
return false;
}
if (!/^[a-zA-Z0-9_]+$/.test(value)) {
showError(input, "Only letters, numbers, and underscores");
return false;
}
showSuccess(input);
return true;
}
function validateEmail(input) {
const value = input.value.trim();
if (!value) {
showError(input, "Email is required");
return false;
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
showError(input, "Please enter a valid email address");
return false;
}
showSuccess(input);
return true;
}
function validatePassword(input) {
const value = input.value;
if (!value) {
showError(input, "Password is required");
return false;
}
if (value.length < 8) {
showError(input, "Password must be at least 8 characters");
return false;
}
if (!/[A-Z]/.test(value) || !/[a-z]/.test(value) || !/\d/.test(value)) {
showError(input, "Include uppercase, lowercase, and a number");
return false;
}
showSuccess(input);
return true;
}
function validateConfirm(input) {
const value = input.value;
const password = form.elements.password.value;
if (!value) {
showError(input, "Please confirm your password");
return false;
}
if (value !== password) {
showError(input, "Passwords do not match");
return false;
}
showSuccess(input);
return true;
}
// 3. Password strength meter
form.elements.password.addEventListener("input", () => {
const value = form.elements.password.value;
let score = 0;
if (value.length >= 8) score++;
if (/[A-Z]/.test(value)) score++;
if (/[a-z]/.test(value)) score++;
if (/\d/.test(value)) score++;
if (/[^a-zA-Z0-9]/.test(value)) score++;
const percent = (score / 5) * 100;
strengthBar.style.width = percent + "%";
const levels = [
{ label: "", color: "#e2e8f0" },
{ label: "Very weak", color: "#ef4444" },
{ label: "Weak", color: "#f97316" },
{ label: "Fair", color: "#eab308" },
{ label: "Strong", color: "#22c55e" },
{ label: "Very strong", color: "#16a34a" }
];
strengthBar.style.backgroundColor = levels[score].color;
strengthText.textContent = value ? levels[score].label : "";
strengthText.style.color = levels[score].color;
});
// 4. Real-time validation
const validators = {
username: validateUsername,
email: validateEmail,
password: validatePassword,
"confirm-password": validateConfirm
};
Object.keys(validators).forEach(name => {
const input = form.elements[name];
input.addEventListener("blur", () => {
touched.add(name);
validators[name](input);
});
input.addEventListener("input", () => {
if (touched.has(name)) {
validators[name](input);
}
});
});
// 5. Handle submission
form.addEventListener("submit", (event) => {
event.preventDefault();
// Mark all fields as touched
Object.keys(validators).forEach(name => touched.add(name));
const results = Object.entries(validators).map(
([name, fn]) => fn(form.elements[name])
);
if (results.every(Boolean)) {
form.style.display = "none";
successMessage.style.display = "block";
} else {
const firstInvalid = form.querySelector(".input-error");
if (firstInvalid) firstInvalid.focus();
}
});
🎯 Quick Quiz
Question 1: How do you read what the user typed into a text input?
Question 2: What does event.preventDefault() do in a submit handler?
Question 3: How do you check if a checkbox is checked?
Question 4: When is the blur event useful for form validation?
Question 5: Why is client-side validation not sufficient on its own?
Summary
🎉 Key Takeaways
- Read input values with
.value(text, email, password) or.checked(checkboxes, radios) - Access inputs via
form.elements.namefor clean, name-based access - Always call
event.preventDefault()in yoursubmithandler to stop page reloads - Use
FormDatato collect all form values at once - Validate fields for required, length, format (regex), and matching
- Validate on
blur(when user leaves), then re-validate oninput(as they type) — but only after the field has been touched - Show errors with
role="alert"for accessibility; style with color + text, not color alone - The Constraint Validation API lets you hook into the browser's built-in validation system
- Client-side validation is for UX; always validate on the server too
📚 Additional Resources
- MDN — Web Forms Guide
- MDN — FormData API
- MDN — Constraint Validation
- MDN — <input> Types
- javascript.info — Forms and Controls
- W3C — Accessible Form Validation
🚀 What's Next?
Now that you can capture and validate user input, it's time to add motion and timing to your pages. In the next lesson, you'll learn to use setTimeout, setInterval, and requestAnimationFrame to create timers, countdowns, and smooth animations driven by JavaScript.