Command Palette

Search for a command to run...

Hectal
PHASE 9Intermediate ~23 min· topic 6 of 8

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, setCustomValidity and 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.

app.jswhole filejavascript
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.

app.jswhole filejavascript
theme.addEventListener("change", () => {
  document.body.dataset.theme = theme.value;
});

03Handle the submit

Cancel the default, then read every field with one FormData.

app.jswhole filejavascript
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.

app.jswhole filejavascript
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.

How a submit is checkeddiagram
Rendering diagram…

Try it yourself

  1. 1

    Count characters

    In the first example, add <p id="count">0</p> and make the input listener write nameBox.value.length into it. Type in the box. Would change work as well? Predict, then try.

  2. 2

    Leave out the submitter

    In the second example, change new FormData(form, event.submitter) to new FormData(form). Predict which part of the output changes.

  3. 3

    Forget to clear the message

    In the third example, change checkMatch so it only ever calls setCustomValidity("Passwords don't match") when they differ, and never "". Predict what valid now? prints.

Code & diagrams

input versus change New tab

Type in the box yourself: `input` fires on every key, `change` only when you click or Tab away after changing it.

Preview

Press Run to see the page.

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: true
Reading a form with FormData New tab

The 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

Press Run to see the page.

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
A custom rule with setCustomValidity New tab

`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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

invalid: confirm | customError: true | Passwords don't match
valid? false
valid now? true
submitted!
Your own error messages with novalidate New tab

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

Press Run to see the page.

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 = "";.

terminal
$ the page shows
── what you'll see ──
The box is empty but the counter still says 57 / 160.

Break #2

Fields without a name

Write <input id="email"> (no name) and read the form with new FormData(form).

terminal
$ Console
── what you'll see ──
{} (Object.fromEntries(data) is empty)

Break #3

Custom message never cleared

In an input listener, call field.setCustomValidity("Too short") when the value is short, but never setCustomValidity("").

terminal
$ the page shows
── what you'll see ──
Even after typing a long enough value, the form refuses to submit and keeps saying "Too short".

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 :invalid matches 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.

  • ▸

    FormData includes <input type="file"> values as File objects, so fetch(url, { method: "POST", body: data }) sends a multipart/form-data upload with no extra code. Don't set the Content-Type header yourself; the browser adds the boundary string it needs.

  • ▸

    The formdata event fires on the form whenever a FormData is built from it, letting custom controls add their values with event.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 validationMessage text in code; check the validity flags instead. Announce errors to screen readers with aria-describedby plus an aria-live region, and move focus to the first invalid field on submit.

Remember this

  1. 1

    **input fires 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 an InputEvent with inputType (such as "insertText" or "deleteContentBackward") and data (the text inserted). It is not cancelable; to block a change before it happens, use beforeinput.

  2. 2

    **change fires 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. Setting field.value = "x" from JavaScript fires neither event; dispatch one yourself if other code needs to know.

  3. 3

    **submit** fires on the <form> when the user sends it (Topic 9.4), after the browser's built-in validation passes. event.submitter is the button that sent it, useful when a form has two buttons such as Save draft and Publish. Call event.preventDefault() to handle the data in JavaScript. Related: reset fires when a reset button restores the default values, and invalid fires on each field that fails validation.

  4. 4

    **FormData** reads a whole form at once. new FormData(form) collects every field that has a name, is not disabled, and is "successful" (checked, for checkboxes and radios). data.get("name") returns one value (a string, or a File for 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), and new URLSearchParams(data).toString() gives name=Asha&size=M. You can pass FormData straight to fetch as 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. 5

    The browser's built-in validation comes from HTML attributes (see /html, forms): required, type="email", minlength, maxlength, min, max, step and pattern. JavaScript reads the result through the Constraint Validation API: field.validity is an object of true/false flags (valueMissing, typeMismatch, tooShort, patternMismatch, customError, and finally valid); field.validationMessage is the message the browser would show; form.checkValidity() returns true or false and fires invalid on each failing field; form.reportValidity() does the same and also shows the browser's message bubbles.

  6. 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.customError is true) and blocks submission with your message; an empty string "" makes it valid again. You must clear it yourself, usually in an input listener, or the field stays invalid forever. For fully custom error display, add novalidate to the form, call checkValidity() in your submit listener, and show messages in the page next to each field, linked with aria-describedby and marked with aria-invalid="true" so screen readers announce them. Always validate again on the server: anyone can skip your JavaScript.

Explain it without notes

01

What is the difference between the input and change events?

02

How does FormData decide which fields to include?

03

How do you add a custom validation rule, and what's the common mistake?

04

Why validate on the client if you must validate on the server anyway?

Practice

01

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.

02

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; novalidate plus your own messages gives full design control, at the cost of writing and testing the accessibility yourself.

  • ↔

    Validating on every input gives instant feedback but can nag users mid-typing; validating on change/focusout, then live once a field is marked invalid, is the gentler common pattern.

Done when you can

  • I can choose between input and change for a task.

  • I can handle submit, cancel it and read all fields with FormData, including getAll.

  • I know fields need a name and disabled fields are skipped.

  • I can add and clear a custom rule with setCustomValidity.

  • I can read validity flags and show accessible error messages.

  • I always repeat validation on the server.