---
title: 'Form Events'
source: 'https://academia.sh/en/courses/browser-platform/form-events'
course: 'The Browser and the Web Platform'
language: en
updated: '2026-08-17T18:09:12+00:00'
license: 'CC BY-SA 4.0'
---

# Form Events

The flow from value change to submission; the distinction between input and change events, building the entry list, the submitter's contribution, the states of constraint validation, and accessible reporting of an error.

The previous lesson said the reliable way to learn a field's content is not key events.
The form on the page does not only collect text: a measurement value is a number, a date
comes from a calendar control, a station code is chosen from a list. Every control reports
its value at different moments.

On top of this there is submission. The browser collects the fields, tests the
constraints, and builds the request. This lesson covers the flow in two parts: what is
reported when a value changes, and what happens when submission is requested.

## Events That Report Change

Three event types report a value's change, and the three mark different moments.

The **before-input event** is produced before the change is applied to the field. It
reports what is about to happen and is cancelable; rejecting a specific input outright is
possible with this event. Which operation the user performed — typing, pasting, undoing —
is also read from this event.

The **input event** is produced after the change has been applied. It runs on every
character typed, every paste, every undo. Code that wants to continuously track the value
listens to this. The composition mechanism mentioned in the previous lesson, voice input,
and autofill also produce this event; the cases key events miss are seen here.

The **change event** reports the moment the value **settles**. On a text field, this is
when the field is left or submission happens; on a checkbox, a radio button, and a
dropdown list, it is the moment a selection is made. So on text fields the change event is
much rarer than the input event, while on selection controls it is produced together with
it.

The selection rule depends on the nature of the work. If the page's filter field narrows
the result as it is typed, the input event is needed. If the field's value is only going
to be written to a record, the change event is enough and does less.

Code listening to the input event carries an added responsibility: a handler that runs on
every keystroke should not produce a request on every keystroke. The established solution
is **debouncing**: when the event arrives, the work is not done immediately, it is
deferred to shortly after, and if a new event arrives within that time, the deferral
restarts. A single run remains once the user stops typing. Measuring and cancelling this
duration is the subject of the Timing and the Rendering Loop lesson.

## Submission and the Entry List

Submission is the browser gathering the form's controls and building a request body. The
**entry list** defined in the Form Structure lesson of the Web Fundamentals and HTML
course is the result of this gathering. Which control enters the list is governed by
rules.

```js
// entrylist.mjs — which controls enter the entry list on submission
const controls = [
  { name: "station",  type: "text",     value: "North Slope" },
  { name: "measurement", type: "select", value: "temperature" },
  { name: "value",    type: "number",   value: "-4.2" },
  { name: "unit",     type: "radio",    value: "C",     checked: true },
  { name: "unit",     type: "radio",    value: "F",     checked: false },
  { name: "verified", type: "checkbox", value: "yes",   checked: false },
  { name: "note",     type: "textarea", value: "" },
  { name: "",         type: "text",     value: "unnamed field" },
  { name: "hiddenCode", type: "text",   value: "K-9", disabled: true },
  { name: "action",   type: "submit",   value: "save" },
  { name: "action",   type: "submit",   value: "delete" },
];

// The button that starts submission: only it enters the list, the others do not.
const submitter = controls.find((d) => d.type === "submit" && d.value === "save");

function entryList(controls, submitter) {
  const entries = [];
  for (const d of controls) {
    if (d.name === "" || d.disabled) continue;
    if ((d.type === "radio" || d.type === "checkbox") && d.checked !== true) continue;
    if (d.type === "submit" && d !== submitter) continue;
    entries.push([d.name, d.value]);
  }
  return entries;
}

const entries = entryList(controls, submitter);
for (const [name, value] of entries) console.log(`  ${name.padEnd(11)} = ${JSON.stringify(value)}`);
console.log("body:", new URLSearchParams(entries).toString());
```

```
  station     = "North Slope"
  measurement = "temperature"
  value       = "-4.2"
  unit        = "C"
  note        = ""
  action      = "save"
body: station=North+Slope&measurement=temperature&value=-4.2&unit=C&note=&action=save
```

Four rules read from the output. **A control with no name does not enter the list**; the
name is the field's identity as seen by the server. **A disabled control does not enter
the list**; this is the data-side counterpart of the previous lesson's "disabling makes
invisible" rule. **An unchecked checkbox and an unselected radio button do not enter the
list**; the receiving side has to interpret this field's absence as "no." **An empty text
field does enter the list**, because an empty value is a value too.

The submitter button makes a separate contribution. If a form has more than one submit
button, only the clicked button's name and value enter the list; the receiving side learns
which action was requested from this. The submit event reports the submitter through the
event object.

The submit event is cancelable. The rule from the Default Behavior and Cancellation lesson
applies here: code that cancels submission has to send the data itself. There is a
program-side counterpart of the entry list too, and it can be handed to the request as it
is; this way the server side sees the same body on both paths.

## Constraint Validation

Before the submit event is produced, the browser runs **constraint validation**.
Constraints are declared in the markup; their results are read as a set of states.

```js
// validity.mjs — deriving validity state from constraint attributes
const CONSTRAINT = { required: true, min: -50, max: 60, step: 0.1, base: 0 };

// The step check is scaled to an integer to avoid binary-fraction error.
const scale = 1000;
const stepMismatch = (n, k) =>
  Math.round((n - k.base) * scale) % Math.round(k.step * scale) !== 0;

function validity(raw, k = CONSTRAINT, disabled = false) {
  const state = {
    valueMissing: false, typeMismatch: false, rangeUnderflow: false,
    rangeOverflow: false, stepMismatch: false,
  };
  if (disabled) return { willValidate: false, state, valid: true };

  if (k.required && raw === "") state.valueMissing = true;
  else if (raw !== "") {
    const n = Number(raw);
    if (Number.isNaN(n)) state.typeMismatch = true;
    else {
      if (n < k.min) state.rangeUnderflow = true;
      if (n > k.max) state.rangeOverflow = true;
      if (stepMismatch(n, k)) state.stepMismatch = true;
    }
  }
  const valid = Object.values(state).every((v) => v === false);
  return { willValidate: true, state, valid };
}

const samples = [
  ["", false], ["-4.2", false], ["-80", false], ["61.5", false],
  ["12.34", false], ["too cold", false], ["999", true],
];

for (const [raw, disabled] of samples) {
  const { willValidate, state, valid } = validity(raw, CONSTRAINT, disabled);
  const broken = Object.entries(state).filter(([, v]) => v).map(([name]) => name);
  console.log(
    JSON.stringify(raw).padEnd(12),
    "willValidate:", String(willValidate).padEnd(5),
    "valid:", String(valid).padEnd(5),
    broken.join(", "),
  );
}
```

```
""           willValidate: true  valid: false valueMissing
"-4.2"       willValidate: true  valid: true  
"-80"        willValidate: true  valid: false rangeUnderflow
"61.5"       willValidate: true  valid: false rangeOverflow
"12.34"      willValidate: true  valid: false stepMismatch
"too cold"   willValidate: true  valid: false typeMismatch
"999"        willValidate: false valid: true  
```

The model shows three things. First, validity is made up not of a single true-or-false but
of **separately readable states**; the error message can be written according to which
constraint was broken. Second, the step constraint cannot be tested by direct division on
decimal values; the binary-fraction problem from the How Computers Work course shows up
here, and the check is done by scaling to an integer. Third, a disabled control is
**never validated at all**; the out-of-range value on the last line counts as valid,
because that control does not take part in validation.

The browser's own testing can also correct the field's raw value. When a non-numeric
string is typed into a field expecting a number, the value can be cleared; in that case
the reported state is not "type mismatch" but "value missing." For this reason, the error
message is chosen by looking at the reported state, not at the value the user sees.

Three operations are defined on the program side. One **tests** validity and returns the
result; the second tests and, if invalid, **shows** the browser's own message; the third
**writes** a field-specific error message. The third exists to carry application rules: a
rule that cannot be declared in markup — such as a measurement date not being in the
future — is tied to the built-in mechanism this way. Writing an empty message makes the
field valid again.

There is also a declaration that turns off validation for the whole form. This declaration
removes the browser's own messages; constraints are still read. Pages that build their own
error presentation follow this route. Under no circumstance does client-side validation
stand in for server-side validation: a request can be built without the page ever being
used at all.

## Reporting an Error

An event is produced for every invalid field, and this event does not bubble; code that
wants to collect a form's errors in one place uses capture mode. The rule from the Event
Delegation lesson comes up a second time here.

An error's presentation is made of three parts. The **relevant message** stands next to
the field and is attached to it with an extra description bond; the field is marked with
an invalidity notice. An **error summary** sits at the top of the form, and every item
links to its field. After a submission attempt, **focus moves to the first invalid field**
or to the summary itself; the programmatic focus rule from the Focus and Keyboard
Interaction lesson applies here.

The moment error messages are written is also a design decision. Showing an error on
every keystroke while the user is filling in the field declares an input that is not yet
finished to be wrong. The common layout is this: the first message is written when the
field is left or submission is attempted; once a field has been found invalid once, it is
updated on the input event, so the message disappears the moment it is corrected.

Resetting the form produces a separate event and is cancelable. Reset returns fields to
their starting values declared in the markup — the direct consequence of the
attribute-property distinction from the DOM API lesson. Code that does not clear its own
held state on this event ends up with a form empty on screen but full in memory.

## Summary

- The before-input event is produced before the change is applied and is cancelable; the
  input event is produced on every change, the change event at the moment the value
  settles.
- Work bound to the input event is thinned out with debouncing; not every keystroke
  produces a request.
- The entry list includes controls that have a name, are not disabled, and are checked in
  the case of selection controls; among several submit buttons, only the clicked one
  contributes.
- Constraint validation produces separately readable states rather than a single
  true-or-false; disabled controls do not take part in validation at all.
- A custom error message writer ties application rules that cannot be declared in markup
  to the built-in validation; client-side validation does not stand in for server-side
  validation.
- The invalidity event does not bubble; error presentation is built from a message, a
  summary, and moving focus.

## Next Step

This lesson assumed a form ends with a submission. But most forms are left half-finished:
the user enters a measurement value, closes the tab, comes back the next day. Everything
held in memory is erased when the page reloads — the criterion typed into the filter
field, and the half-finished form alike. Storing these requires writing to a place that
outlives the page. The next lesson covers these places — the one bound to the tab's
lifetime, the one that persists for the resource's lifetime, and the one that holds
structured data — along with their distinctions.
