Day 13: Forms, FormData & Validation
✅ Today you will learn
- Handle submit with preventDefault
- Read all fields with FormData and Object.fromEntries
- Write clear validation rules (mobile, PIN, quantity, date)
- Show accessible error messages next to fields
- Constraint Validation API and live calculations
🏢 In the real world
The HTML-only order form from the HTML course stopped obviously wrong data, but customers still ordered for yesterday’s date, typed PIN codes outside the delivery area and got a vague browser bubble in English only. Businesses lose orders on bad forms. Today you build a friendly, bilingual-ready form that explains every mistake and calculates the total live.
Handling submit and reading values
<style>
form { display: grid; gap: 10px; max-width: 380px; }
label { font-weight: 600; display: block; margin-bottom: 2px; }
input, select { width: 100%; box-sizing: border-box; }
.error { color: #b91c1c; font-size: .85rem; min-height: 1.1em; }
input[aria-invalid="true"], select[aria-invalid="true"] { border-color: #dc2626; background: #fef2f2; }
.ok { background: #dcfce7; padding: 10px; border-radius: 8px; }
</style>
<form id="f">
<label for="name">Name</label><input id="name" name="name" value="Aarti Gupta">
<label for="sweet">Sweet</label>
<select id="sweet" name="sweet"><option value="1">Kaju Katli</option><option value="2" selected>Ghewar</option></select>
<label><input type="checkbox" name="gift" checked> Gift wrap</label>
<fieldset><legend>Delivery</legend>
<label><input type="radio" name="mode" value="home" checked> Home</label>
<label><input type="radio" name="mode" value="pickup"> Pick-up</label>
</fieldset>
<button>Submit</button>
</form>
<pre id="out"></pre>
<script>
document.querySelector('#f').addEventListener('submit', function (e) {
e.preventDefault(); // no page reload
const data = new FormData(e.currentTarget); // reads every named field
const order = Object.fromEntries(data); // → plain object
order.gift = data.has('gift'); // unchecked boxes are missing
order.sweet = Number(order.sweet);
document.querySelector('#out').textContent = JSON.stringify(order, null, 2);
});
</script>FormData only reads fields that have a name. Radio buttons share one name and give the selected value; an unchecked checkbox is not included at all – check it with data.has("gift").
Validation rules as small functions
const DELIVERY_PINS = ['302001', '302003', '302004', '302012', '302017'];
const rules = {
name: v => v.trim().length >= 2 || 'Please enter your full name.',
phone: v => /^[6-9]\d{9}$/.test(v.replace(/\D/g, '').slice(-10)) || 'Enter a 10-digit mobile number starting with 6–9.',
pin: v => DELIVERY_PINS.includes(v.trim()) || 'Sorry, we do not deliver to this PIN yet.',
qty: v => (Number(v) >= 0.25 && Number(v) <= 50) || 'Quantity must be between 0.25 and 50 kg.',
date: (v, today) => v >= today || 'Choose today or a future date.',
};
function validate(order, today) {
const errors = {};
for (const [field, rule] of Object.entries(rules)) {
const result = rule(order[field] ?? '', today);
if (result !== true) errors[field] = result;
}
return errors;
}
const today = '2026-10-25';
console.log(validate({ name: 'Aarti', phone: '+91 98290 12345', pin: '302003', qty: '2', date: '2026-10-26' }, today));
console.log(validate({ name: 'A', phone: '12345', pin: '110001', qty: '0', date: '2026-10-20' }, today));Each rule answers “is it OK?” with true, or explains what is wrong. The same object can be reused on the server (Node.js) – and the messages can be swapped for Hindi versions without touching the logic.
A complete validated order form
<style>
form { display: grid; gap: 10px; max-width: 380px; }
label { font-weight: 600; display: block; margin-bottom: 2px; }
input, select { width: 100%; box-sizing: border-box; }
.error { color: #b91c1c; font-size: .85rem; min-height: 1.1em; }
input[aria-invalid="true"], select[aria-invalid="true"] { border-color: #dc2626; background: #fef2f2; }
.ok { background: #dcfce7; padding: 10px; border-radius: 8px; }
</style>
<form id="order" novalidate>
<div><label for="name">Name</label><input id="name" name="name" autocomplete="name" aria-describedby="name-err"><div class="error" id="name-err"></div></div>
<div><label for="phone">Mobile</label><input id="phone" name="phone" type="tel" inputmode="numeric" autocomplete="tel" aria-describedby="phone-err"><div class="error" id="phone-err"></div></div>
<div><label for="pin">PIN code</label><input id="pin" name="pin" inputmode="numeric" maxlength="6" aria-describedby="pin-err"><div class="error" id="pin-err"></div></div>
<div><label for="qty">Kaju Katli (kg) – ₹900/kg</label><input id="qty" name="qty" type="number" value="1" step="0.25" aria-describedby="qty-err"><div class="error" id="qty-err"></div></div>
<div><label for="date">Delivery date</label><input id="date" name="date" type="date" aria-describedby="date-err"><div class="error" id="date-err"></div></div>
<p id="total"></p>
<button>Place order</button>
<div id="result" role="status"></div>
</form>
<script>
const PINS = ['302001', '302003', '302004', '302012', '302017'];
const today = new Date().toISOString().slice(0, 10);
const form = document.querySelector('#order');
form.date.min = today;
const rules = {
name: v => v.trim().length >= 2 || 'Please enter your full name.',
phone: v => /^[6-9]\d{9}$/.test(v.replace(/\D/g, '').slice(-10)) || 'Enter a 10-digit mobile number starting with 6–9.',
pin: v => PINS.includes(v.trim()) || 'We deliver to 302001, 302003, 302004, 302012, 302017.',
qty: v => (Number(v) >= 0.25 && Number(v) <= 50) || 'Between 0.25 and 50 kg.',
date: v => (v !== '' && v >= today) || 'Choose today or a future date.',
};
function showError(field, message) {
const input = form.elements[field];
input.setAttribute('aria-invalid', message ? 'true' : 'false');
document.querySelector('#' + field + '-err').textContent = message || '';
}
function check(field) {
const result = rules[field](form.elements[field].value);
showError(field, result === true ? '' : result);
return result === true;
}
function updateTotal() {
const kg = Number(form.qty.value) || 0;
const amount = kg * 900;
const delivery = amount >= 1500 || amount === 0 ? 0 : 60;
document.querySelector('#total').textContent = `Total ₹${amount + delivery} (delivery ₹${delivery})`;
}
Object.keys(rules).forEach(f => form.elements[f].addEventListener('blur', () => check(f))); // validate on leave
form.addEventListener('input', e => {
if (e.target.getAttribute('aria-invalid') === 'true') check(e.target.name); // re-check while fixing
if (e.target.name === 'qty') updateTotal();
});
form.addEventListener('submit', e => {
e.preventDefault();
const results = Object.keys(rules).map(check);
const firstBad = Object.keys(rules).find((f, i) => !results[i]);
if (firstBad) { form.elements[firstBad].focus(); return; } // move focus to the first error
const data = Object.fromEntries(new FormData(form));
document.querySelector('#result').innerHTML = `<p class="ok">✅ Order for ${data.qty} kg booked for ${data.date}. We will call ${data.phone}.</p>`;
});
updateTotal();
</script>| UX rule | How we did it |
|---|---|
| Do not shout before the user types | Validate on blur and on submit, not on page load |
| Clear the error as soon as it is fixed | Re-check on input only for invalid fields |
| Say exactly what to do | “Enter a 10-digit mobile number starting with 6–9” |
| Accessible errors | aria-invalid + aria-describedby link the message to the field |
| Help users get to the problem | Focus the first invalid field on submit |
| Accept human formats | +91 98290-12345 → digits are extracted before checking |
Anyone can bypass JavaScript (DevTools, curl). The server must validate everything again – our practice API on Day 20 does exactly that and returns field errors with HTTP 422.
The built-in Constraint Validation API
<style>
form { display: grid; gap: 10px; max-width: 380px; }
label { font-weight: 600; display: block; margin-bottom: 2px; }
input, select { width: 100%; box-sizing: border-box; }
.error { color: #b91c1c; font-size: .85rem; min-height: 1.1em; }
input[aria-invalid="true"], select[aria-invalid="true"] { border-color: #dc2626; background: #fef2f2; }
.ok { background: #dcfce7; padding: 10px; border-radius: 8px; }
</style>
<form id="f2">
<label for="email">E-mail</label>
<input id="email" type="email" required>
<button>Subscribe</button>
</form>
<script>
const email = document.querySelector('#email');
email.addEventListener('input', () => {
if (email.validity.typeMismatch) email.setCustomValidity('Please enter an e-mail like name@example.com');
else if (email.validity.valueMissing) email.setCustomValidity('E-mail is required to get offers');
else email.setCustomValidity(''); // '' = valid again
});
document.querySelector('#f2').addEventListener('submit', e => {
e.preventDefault();
if (!e.currentTarget.checkValidity()) { e.currentTarget.reportValidity(); return; }
e.currentTarget.insertAdjacentHTML('beforeend', '<p class="ok">Subscribed ✅</p>');
});
</script>🏅 Job skills unlocked today
Practice tasks
🏋️ Practice – do it yourself
Show solution
Quick quiz
🧠 Quick Quiz – test yourself
Interview corner
💼 Interview Corner
How do you validate forms well in vanilla JS?
Use HTML attributes for basics, centralise rules as functions, validate on blur/submit, show inline accessible messages (aria-invalid, aria-describedby), focus the first error, normalise input (trim, digits) and always re-validate on the server.
What does e.preventDefault() do on submit?
It stops the browser’s default navigation/reload so JavaScript can validate and send the data (e.g. with fetch) while staying on the page.
Summary
- submit + preventDefault + FormData + Object.fromEntries.
- Rules as small functions returning true or a message.
- Validate on blur/submit, show accessible inline errors, focus the first one.
- Client checks are UX; the server must validate again.
Day 14 – Mini project: a complete to-do app with add, edit, complete, filter, delete and saving.
