Topic 9.6
Form Events and Validation in JavaScript
In one line
input fires on every change to a field's value, change fires when a change is committed, and submit fires on the form; FormData collects every named field in one step, and the Constraint Validation API (checkValidity, setCustomValidity, validity) lets JavaScript add its own rules and messages to the browser's built-in ones.
Think of it like this
Filling in a paper form at a bank counter. While you write, the clerk can see each letter appear (that's input). When you put the pen down and move to the next box, that box is done (that's change). When you hand the whole form over, that's submit. The clerk checks it against the rules ("the two signatures must match"), and if something is wrong, hands it back with a note on the exact box: that's validation with a custom message.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- input event
- Fires every time a field's value changes, letter by letter.
- change event
- Fires when a change is finished: on leaving a text box, or straight after picking a checkbox or menu option.
- submit event
- Fires on a form when it is sent, after built-in validation passes.
- submitter
- The button that sent the form, read with
event.submitter. - FormData
- An object that collects all named fields of a form as name and value pairs.
- Validation
- Checking that what the user entered follows the rules before using it.
- Constraint Validation API
- The browser's built-in validation tools for JavaScript:
validity,checkValidity,setCustomValidityand friends. - validity
- An object on each field with true or false flags saying which rule, if any, is broken.
- novalidate
- A form attribute that turns off the browser's automatic validation bubbles, so your code shows errors instead.
Step by step
01Live feedback with input
A character counter updates on every keystroke, paste or deletion, so it listens for input.
bio.addEventListener("input", () => {
counter.textContent = bio.value.length + " / 160";
});02Save the final value with change
A theme menu only matters once the user picks an option, so change is enough.
theme.addEventListener("change", () => {
document.body.dataset.theme = theme.value;
});03Handle the submit
Cancel the default, then read every field with one FormData.
form.addEventListener("submit", (event) => {
event.preventDefault();
const data = new FormData(form, event.submitter);
console.log(Object.fromEntries(data));
});04Add a rule HTML can't express
The confirm box must match the password. Recalculate on every input: a message makes the field invalid, an empty string makes it valid.
confirm.addEventListener("input", () => {
confirm.setCustomValidity(confirm.value === password.value ? "" : "Passwords don't match");
});05How a submit is checked
The browser checks every field before firing submit. Any invalid field (from attributes or from your custom message) stops the submission and gets an invalid event.
Try it yourself
- 1
Count characters
In the first example, add
<p id="count">0</p>and make theinputlistener writenameBox.value.lengthinto it. Type in the box. Wouldchangework as well? Predict, then try. - 2
Leave out the submitter
In the second example, change
new FormData(form, event.submitter)tonew FormData(form). Predict which part of the output changes. - 3
Forget to clear the message
In the third example, change
checkMatchso it only ever callssetCustomValidity("Passwords don't match")when they differ, and never"". Predict whatvalid now?prints.
Code & diagrams
Type in the box yourself: `input` fires on every key, `change` only when you click or Tab away after changing it.
Preview
Console
Nothing logged yet.Expected console output
input: A | inputType: insertText
input: An | inputType: insertText
input: Ann | inputType: insertText
change: Ann
size change: M
gift input: true
gift change: trueThe disabled coupon field is left out, the unchecked onion is left out, and `Object.fromEntries` kept only the last `extras`. `action: 'order'` is there because the submitter was passed to `FormData`.
Preview
Console
Nothing logged yet.Expected console output
name: Asha
extras (get): cheese
extras (getAll): [ 'cheese', 'olives' ]
coupon: null
as an object: { name: 'Asha', size: 'M', extras: 'olives', action: 'order' }
as a query string: name=Asha&size=M&extras=cheese&extras=olives&action=order`checkValidity()` fires `invalid` on each bad field before returning `false`. Try typing different passwords yourself and pressing the button: the browser shows your message in its bubble.
Preview
Console
Nothing logged yet.Expected console output
invalid: confirm | customError: true | Passwords don't match
valid? false
valid now? true
submitted!Press Send with the box empty, then type `asha`, then `asha@example.com`. The message updates live once a field has been marked invalid, which avoids nagging people before they've finished typing.
Preview
Console
Nothing logged yet.Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Expect value = ... to fire input
A counter listens for input. Elsewhere, a "Clear" button runs bio.value = "";.
Break #2
Fields without a name
Write <input id="email"> (no name) and read the form with new FormData(form).
Break #3
Custom message never cleared
In an input listener, call field.setCustomValidity("Too short") when the value is short, but never setCustomValidity("").
Myth vs fact
Myth
change fires on every keystroke in a text box.
Fact
For text fields, change fires only when the edited field loses focus or the user presses Enter. input is the per-keystroke event. (React's onChange actually listens to input, which is where the confusion comes from.)
Myth
Client-side validation keeps bad data out of your database.
Fact
It improves the user's experience, but anyone can turn off JavaScript, edit the page in DevTools or send requests directly. The server must validate everything again.
Myth
novalidate turns off validation completely.
Fact
It only stops the browser from blocking submission and showing its bubbles. validity, checkValidity() and :invalid still work, so your code can use the same rules and show its own messages.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
CSS
:invalidmatches a field from the start (an empty required field is red before the user types).:user-invalid(Baseline 2023) only matches after the user has interacted with the field or tried to submit, which is almost always what you want for error styles. - ▸
FormDataincludes<input type="file">values asFileobjects, sofetch(url, { method: "POST", body: data })sends amultipart/form-dataupload with no extra code. Don't set theContent-Typeheader yourself; the browser adds the boundary string it needs. - ▸
The
formdataevent fires on the form whenever aFormDatais built from it, letting custom controls add their values withevent.formData.append(...). Form-associated custom elements (ElementInternals) are the fuller solution for building your own form controls. - ▸
Validation messages are worded by the browser and translated into the user's language, so never compare
validationMessagetext in code; check thevalidityflags instead. Announce errors to screen readers witharia-describedbyplus anaria-liveregion, and move focus to the first invalid field on submit.
Remember this
- 1
**
inputfires every time the value changes**: each key typed, each paste, each deletion, autofill, or a slider being dragged. Use it for live feedback: character counters, filtering a list as you type, password strength. Its event is anInputEventwithinputType(such as"insertText"or"deleteContentBackward") anddata(the text inserted). It is not cancelable; to block a change before it happens, usebeforeinput. - 2
**
changefires when a change is committed**. For text boxes and textareas, that's when the field loses focus (or Enter is pressed) after its value changed. For checkboxes, radio buttons,<select>menus, file pickers and date pickers, it fires straight after the user picks something. Use it when you only care about the final value, like saving a setting. Settingfield.value = "x"from JavaScript fires neither event; dispatch one yourself if other code needs to know. - 3
**
submit** fires on the<form>when the user sends it (Topic 9.4), after the browser's built-in validation passes.event.submitteris the button that sent it, useful when a form has two buttons such as Save draft and Publish. Callevent.preventDefault()to handle the data in JavaScript. Related:resetfires when a reset button restores the default values, andinvalidfires on each field that fails validation. - 4
**
FormData** reads a whole form at once.new FormData(form)collects every field that has aname, is notdisabled, and is "successful" (checked, for checkboxes and radios).data.get("name")returns one value (a string, or aFilefor file inputs);data.getAll("extras")returns every value for a name, which you need for several checkboxes sharing one name.Object.fromEntries(data)turns it into a plain object (keeping only the last value of repeated names), andnew URLSearchParams(data).toString()givesname=Asha&size=M. You can passFormDatastraight tofetchas a request body (Topic 11.2). Passing the submitter,new FormData(form, event.submitter), also includes the clicked button's name and value (supported in all major browsers since 2023). - 5
The browser's built-in validation comes from HTML attributes (see
/html, forms):required,type="email",minlength,maxlength,min,max,stepandpattern. JavaScript reads the result through the Constraint Validation API:field.validityis an object of true/false flags (valueMissing,typeMismatch,tooShort,patternMismatch,customError, and finallyvalid);field.validationMessageis the message the browser would show;form.checkValidity()returnstrueorfalseand firesinvalidon each failing field;form.reportValidity()does the same and also shows the browser's message bubbles. - 6
For rules HTML can't express, like "the two passwords must match" or "this username is taken", call **
field.setCustomValidity("message")**. A non-empty message makes the field invalid (validity.customErroristrue) and blocks submission with your message; an empty string""makes it valid again. You must clear it yourself, usually in aninputlistener, or the field stays invalid forever. For fully custom error display, addnovalidateto the form, callcheckValidity()in yoursubmitlistener, and show messages in the page next to each field, linked witharia-describedbyand marked witharia-invalid="true"so screen readers announce them. Always validate again on the server: anyone can skip your JavaScript.
Explain it without notes
What is the difference between the input and change events?
How does FormData decide which fields to include?
How do you add a custom validation rule, and what's the common mistake?
Why validate on the client if you must validate on the server anyway?
Practice
Make a <textarea id="bio" maxlength="160"> with a #left paragraph that shows how many characters are left, updated as the user types. Test by setting a value and dispatching input.
Write a submit handler for a form with fields title and tags (several checkboxes) that logs { title, tags }, with tags as an array.
Trade-offs
- ↔
The browser's own validation bubbles are free, translated and accessible, but you can't style them;
novalidateplus your own messages gives full design control, at the cost of writing and testing the accessibility yourself. - ↔
Validating on every
inputgives instant feedback but can nag users mid-typing; validating onchange/focusout, then live once a field is marked invalid, is the gentler common pattern.
Done when you can
I can choose between
inputandchangefor a task.I can handle
submit, cancel it and read all fields withFormData, includinggetAll.I know fields need a
nameand disabled fields are skipped.I can add and clear a custom rule with
setCustomValidity.I can read
validityflags and show accessible error messages.I always repeat validation on the server.