Lesson 06 / 24
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.
Contents
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.
// 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¬e=&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.
// 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.
To keep your progress and take notes, Log in
My notes
Log in to take notes.