Form
import { Form } from '@absoluteui/components/form';import '@absoluteui/components/form/style.css';Using Angular? See Form for Angular.
What it does
Section titled “What it does”State manager and validation orchestrator for a native <form>. Form discovers the AbsoluteUI controls inside the form (including native controls wrapped by Form Field), validates them on submit, runs cross-field rules and an optional async validator (cancellable, with a timeout), shows an Error Summary that links to each invalid field, serializes values, applies server errors, tracks dirty/touched/submitting state, and resets every control consistently. The same entry point also exports the validation engine (checkText, checkNumber, messageFor) and the FormControl contract.
When to use
Section titled “When to use”- Any form built from AbsoluteUI inputs that needs one consistent submit, validation, and error-navigation flow.
- Rules that span several fields, such as password confirmation or conditional requirements.
- Server-side or async checks (uniqueness, availability) whose errors must appear on the right fields.
- Forms that need dirty/touched state for unsaved-changes prompts.
When not to use
Section titled “When not to use”- A single standalone control with no submission: use the control on its own.
- Showing a list of errors you already have without a form: use Error Summary directly.
- Multi-step wizards with per-step persistence: Form manages one <form>; compose one Form per step.
Examples
Section titled “Examples”Sign-up form with a cross-field rule
Section titled “Sign-up form with a cross-field rule”import { Form, type FormSubmitDetail } from '@absoluteui/components/form';import { TextInput } from '@absoluteui/components/text-input';import { Button } from '@absoluteui/components/button';
const el = document.createElement('form');document.body.append(el);const field = (): HTMLDivElement => { const host = document.createElement('div'); el.append(host); return host;};TextInput.create(field(), { label: 'Email', name: 'email', type: 'email', required: true, autocomplete: 'email' });TextInput.create(field(), { label: 'Password', name: 'password', type: 'password', autocomplete: 'new-password', required: true, minLength: 12 });TextInput.create(field(), { label: 'Confirm password', name: 'confirm', type: 'password', autocomplete: 'new-password', required: true });const submit = document.createElement('button');submit.type = 'submit';submit.textContent = 'Create account';Button.enhance(submit, { variant: 'primary' });el.append(submit);
Form.create(el, { rules: [{ fields: ['password', 'confirm'], validate: (values) => (values.password === values.confirm ? null : 'Passwords do not match.') }],});el.addEventListener('aui-submit', (event) => { const { values } = (event as CustomEvent<FormSubmitDetail>).detail; el.dataset.account = typeof values.email === 'string' ? values.email : '';});Async validation with AbortSignal, then server errors with setErrors
Section titled “Async validation with AbortSignal, then server errors with setErrors”import { Form, type FormErrors, type FormSubmitDetail } from '@absoluteui/components/form';import { TextInput } from '@absoluteui/components/text-input';import { Button } from '@absoluteui/components/button';
const el = document.createElement('form');document.body.append(el);const host = document.createElement('div');el.append(host);TextInput.create(host, { label: 'Username', name: 'username', required: true, autocomplete: 'username' });const submit = document.createElement('button');submit.type = 'submit';submit.textContent = 'Register';Button.enhance(submit, { variant: 'primary' });el.append(submit);
function isFormErrors(value: unknown): value is FormErrors { return typeof value === 'object' && value !== null && Object.values(value).every((m) => typeof m === 'string');}
const post = (url: string, body: unknown, signal?: AbortSignal): Promise<Response> => fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), ...(signal ? { signal } : {}) });
const form = Form.create(el, { asyncTimeoutMs: 5000, // Runs after constraints and cross-field rules pass. Rejecting or timing out reports reason 'async-failed'. validate: async (values, signal) => { const response = await post('/api/username-check', values, signal); if (!response.ok) throw new Error(`Username check failed: ${response.status}`); const body: unknown = await response.json(); if (!isFormErrors(body)) throw new Error('Unexpected response'); return body; },});
// Form prevents double submission only while the async validator runs; guard the save yourself.el.addEventListener('aui-submit', (event) => { const { values } = (event as CustomEvent<FormSubmitDetail>).detail; submit.disabled = true; post('/api/register', values) .then(async (response) => { if (response.status !== 422) return; const body: unknown = await response.json(); if (isFormErrors(body)) form.setErrors(body); }) .catch(() => { // A name that matches no field is listed in the summary without a link. form.setErrors({ _form: 'Your account could not be created. Try again.' }); }) .finally(() => { submit.disabled = false; });});Native submission with Form Field-wrapped and plain native controls
Section titled “Native submission with Form Field-wrapped and plain native controls”import { Form } from '@absoluteui/components/form';import { FormField } from '@absoluteui/components/form-field';import { Button } from '@absoluteui/components/button';
const el = document.createElement('form');el.method = 'post';el.action = '/contact';document.body.append(el);
const zip = document.createElement('input');zip.name = 'zip';zip.inputMode = 'numeric';zip.pattern = '[0-9]{5}';const zipHost = document.createElement('div');el.append(zipHost);FormField.create(zipHost, { label: 'ZIP code', control: zip, required: true, description: 'Five digits' });
// Plain named native elements are serialized and reset too, but not validated.const source = document.createElement('input');source.type = 'hidden';source.name = 'source';source.value = 'footer';el.append(source);
const submit = document.createElement('button');submit.type = 'submit';submit.name = 'intent';submit.value = 'send';submit.textContent = 'Send';Button.enhance(submit, { variant: 'primary' });el.append(submit);
// After validation passes and aui-submit is not prevented, the form is submitted natively (submitter name/value included).Form.create(el, { submitMode: 'native', focusOnInvalid: 'first-field' });Unsaved-changes indicator and reset
Section titled “Unsaved-changes indicator and reset”import { Form, type FormState } from '@absoluteui/components/form';import { TextInput } from '@absoluteui/components/text-input';import { Button } from '@absoluteui/components/button';
const el = document.createElement('form');document.body.append(el);const host = document.createElement('div');el.append(host);TextInput.create(host, { label: 'Display name', name: 'displayName', value: 'Ada' });const status = document.createElement('p');const reset = document.createElement('button');reset.type = 'reset';reset.textContent = 'Discard changes';Button.enhance(reset);el.append(status, reset);
const form = Form.create(el);// Emitted only when dirty, touched, submitting, submitCount, or valid actually changes.el.addEventListener('aui-state-change', (event) => { const state = (event as CustomEvent<FormState>).detail; status.textContent = state.dirty ? 'You have unsaved changes.' : '';});// The native reset button is intercepted and calls form.reset().el.addEventListener('aui-reset', () => { status.textContent = '';});window.addEventListener('beforeunload', (event) => { if (form.state.dirty) event.preventDefault();});Reuse the validation engine in a cross-field rule
Section titled “Reuse the validation engine in a cross-field rule”import { checkText, Form, messageFor, type FormValues } from '@absoluteui/components/form';import { TextInput } from '@absoluteui/components/text-input';
const el = document.createElement('form');document.body.append(el);for (const [label, name] of [['Contact method', 'method'], ['Contact email', 'contactEmail']] as const) { const host = document.createElement('div'); el.append(host); TextInput.create(host, { label, name });}
// Email is required only when the contact method is "email"; messages stay localized.const emailRule = (values: FormValues): string | null => { const email = typeof values.contactEmail === 'string' ? values.contactEmail : ''; const outcome = checkText(email, { type: 'email', required: values.method === 'email' }); return outcome.error === null ? null : messageFor(outcome, { locale: 'en-US' });};Form.create(el, { rules: [{ fields: ['method', 'contactEmail'], validate: emailRule, target: 'contactEmail' }] });Focus the first invalid field and shorten the async timeout (Mode B)
Section titled “Focus the first invalid field and shorten the async timeout (Mode B)”import { configureAbsoluteUI } from '@absoluteui/core';import '@absoluteui/components/form';
configureAbsoluteUI({ components: { form: { focusOnInvalid: 'first-field', asyncTimeoutMs: 5000 } } });Custom control joining a Form through registerFormControl
Section titled “Custom control joining a Form through registerFormControl”import { emit } from '@absoluteui/core';import { Form, registerFormControl, type ControlValidity, type FormControl } from '@absoluteui/components/form';import { Button } from '@absoluteui/components/button';
type Priority = 'low' | 'normal' | 'high';
/** A required toggle-button group. It implements the FormControl contract so Form can validate, serialize, reset, and focus it. */class PriorityPicker implements FormControl { readonly name = 'priority'; readonly label = 'Priority'; readonly disabled = false; touched = false; #value: Priority | null = null; #customError: string | null = null; #showErrors = false; readonly #buttons: HTMLButtonElement[]; readonly #message: HTMLParagraphElement; readonly #unregister: () => void;
constructor(readonly host: HTMLElement) { const doc = host.ownerDocument; const legend = doc.createElement('p'); legend.id = 'priority-label'; legend.textContent = this.label; this.#message = doc.createElement('p'); this.#message.id = 'priority-message'; this.#buttons = (['low', 'normal', 'high'] as const).map((value) => { const button = doc.createElement('button'); button.type = 'button'; button.textContent = value; button.setAttribute('aria-pressed', 'false'); button.setAttribute('aria-describedby', this.#message.id); button.addEventListener('click', () => { this.#value = value; this.#customError = null; this.#render(); emit(host, 'aui-change', { value }); }); Button.enhance(button, { size: 'sm' }); return button; }); host.setAttribute('role', 'group'); host.setAttribute('aria-labelledby', legend.id); host.replaceChildren(legend, ...this.#buttons, this.#message); host.addEventListener('focusout', () => { this.touched = true; }); this.#render(); // Registration makes the host discoverable by any Form that contains it. this.#unregister = registerFormControl(this); }
get validity(): ControlValidity { if (this.#value === null) return { valid: false, message: 'Select a priority.' }; if (this.#customError !== null) return { valid: false, message: this.#customError }; return { valid: true, message: '' }; }
get dirty(): boolean { return this.#value !== null; }
getFormValue(): Priority | null { return this.#value; }
reportValidity(): boolean { this.#showErrors = true; this.#render(); return this.validity.valid; }
setCustomError(message: string | null): void { this.#customError = message === '' ? null : message; this.#render(); }
focus(): void { this.#buttons[0]?.focus(); }
reset(): void { this.#value = null; this.#customError = null; this.#showErrors = false; this.touched = false; this.#render(); }
destroy(): void { this.#unregister(); this.host.replaceChildren(); }
#render(): void { for (const button of this.#buttons) button.setAttribute('aria-pressed', String(button.textContent === this.#value)); this.#message.textContent = this.#showErrors ? this.validity.message : ''; }}
const el = document.createElement('form');const host = document.createElement('div');const submit = document.createElement('button');submit.type = 'submit';submit.textContent = 'Create ticket';Button.enhance(submit, { variant: 'primary' });el.append(host, submit);document.body.append(el);new PriorityPicker(host);Form.create(el);import { Form, type FormErrors, type FormSubmitDetail } from '@absoluteui/components/form';import '@absoluteui/components/form/style.css';
const el = document.querySelector<HTMLFormElement>('#signup');if (el) { const form = Form.create(el, { validate: async (values, signal) => { const response = await fetch('/api/signup/check', { method: 'POST', body: JSON.stringify(values), signal }); if (response.status === 409) return { email: 'This email is already registered.' } satisfies FormErrors; if (!response.ok) throw new Error(`Check failed: ${response.status}`); return {}; }, }); el.addEventListener('aui-submit', (event) => { const { values } = (event as CustomEvent<FormSubmitDetail>).detail; void fetch('/api/signup', { method: 'POST', body: JSON.stringify(values) }).then((response) => { if (!response.ok) form.setErrors({ _form: 'Sign-up failed. Try again.' }); }); });}More usage
Section titled “More usage”import { Form, type FormSubmitDetail } from '@absoluteui/components/form';import { TextInput } from '@absoluteui/components/text-input';
const el = document.createElement('form');document.body.append(el);for (const [label, name] of [['Password', 'password'], ['Confirm password', 'confirm']] as const) { const host = document.createElement('div'); el.append(host); TextInput.create(host, { label, name, type: 'password', autocomplete: 'new-password', required: true });}const submit = document.createElement('button');submit.type = 'submit';submit.textContent = 'Save';el.append(submit);Form.create(el, { rules: [{ fields: ['password', 'confirm'], validate: (v) => (v.password === v.confirm ? null : 'Passwords do not match.') }] });el.addEventListener('aui-submit', (event) => { const { values } = (event as CustomEvent<FormSubmitDetail>).detail; el.dataset.saved = String(Object.keys(values).length);});Anatomy
Section titled “Anatomy”<form class="aui-form" novalidate [aria-busy="true"]> (consumer element) <div class="aui-error-summary" role="region" tabindex="-1"> (inserted on first failed submit, or summaryHost) <div class="aui-field aui-text-input" data-aui-control> (AbsoluteUI controls, discovered) <div class="aui-field aui-form-field" data-aui-control> (Form Field-wrapped native controls) <input name="..."> (plain named native elements: serialized and reset) <div class="aui-form__actions"><button type="submit"> (optional consumer wrapper)</form>API reference
Section titled “API reference”Factory
Section titled “Factory”Form.create(element: HTMLFormElement, options?: FormOptions): FormGlobal configuration key: form (see Configuration).
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
rules |
readonly CrossFieldRule[] |
[] |
Cross-field rules { fields, validate(values), target? }. Run on validate/submit and live on every edit after the first submit. The first failing rule per target wins. |
validate |
AsyncFormValidator |
(values, signal) => Promise<FormErrors>. Runs on submit after constraints and rules pass. Rejection or timeout reports reason ‘async-failed’. | |
summaryTitle |
string |
Error summary heading. Default: localized “There is a problem”. Applied when the summary is created, on update(), and on global configuration changes. | |
summaryHost |
HTMLElement |
Element that hosts the error summary. Read only when the summary is first created (on the first failed submission); changing it afterwards has no effect. Default: a <div> inserted as the form’s first child on the first failed submit (removed on destroy). | |
focusOnInvalid |
'summary' | 'first-field' |
"summary" |
Where focus moves after a failed submission. Without a summary, focus always goes to the first invalid field. Globally configurable. |
errorSummary |
boolean |
true |
Render an error summary on failed submission. When false, messages that belong to no field are announced assertively. Globally configurable. |
submitMode |
'event' | 'native' |
"event" |
‘event’: emit aui-submit only. ‘native’: then call HTMLFormElement.submit() unless aui-submit was prevented (the submitter’s name/value is included). Globally configurable. |
asyncTimeoutMs |
number |
10000 |
Async validator timeout in ms (integer 100..120000). On timeout the signal aborts with a TimeoutError. Globally configurable. |
locale |
string |
"en-US" |
Locale of the summary heading and form-level messages. Globally configurable. Inherited from common configuration. |
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
element |
HTMLFormElement |
The managed native form. |
controls |
FormControl[] |
Registered controls inside the form, in document order (computed on access). |
state |
FormState |
Frozen { dirty, touched, submitting, submitCount, valid }; valid ignores disabled controls. |
errorSummary |
ErrorSummary | null |
The summary, created lazily on the first failed submission; null until then or when errorSummary is false. |
Methods
Section titled “Methods”| Method | Description |
|---|---|
getValues(): FormValues |
Frozen values by name: enabled named controls first, then plain named native elements outside controls. Unchecked checkboxes/radios, buttons, files, and :disabled elements (including disabled-fieldset descendants) are skipped; repeated names become arrays. |
validate(): boolean |
Apply rules, show every control’s messages and the summary, and return validity. Does not run the async validator or emit events. |
submit(): void |
requestSubmit(): runs the full submit pipeline. |
setErrors(errors: FormErrors): void |
Apply external (server) errors by field name, show them, and move focus. Unknown names and disabled controls are listed in the summary as plain text without a link. Replaces earlier external errors; each clears when the user edits the field or on the next submit. |
clearErrors(): void |
Clear every rule and external error this form set, and hide the summary. |
reset(): void |
Abort pending async validation, reset every control and plain native element to its initial value, clear state and the summary, emit aui-reset. |
update(options: Partial<FormOptions>): void |
Merge and validate options; summaryTitle and locale are applied to an existing summary, and rules re-run (skipping unrendered targets) if the form was already submitted. Invalid options leave the previous options in place. |
registerFormControl(control: FormControl): () => void |
Standalone export (not a Form method): register a custom control implementing FormControl so any Form containing control.host discovers it. Marks the host with data-aui-control; call the returned function on destroy to unregister. |
destroy(): void |
Abort async validation, remove listeners and subscriptions, destroy the summary (and remove its host if Form created it), and restore the form’s original noValidate value. |
Events
Section titled “Events”| Event | Detail | Bubbles | Cancelable | Description |
|---|---|---|---|---|
aui-submit |
{ values: FormValues } |
No | Yes | All validation passed. Prevent it to stop a native submission (submitMode ‘native’). |
aui-invalid |
{ errors: readonly FormErrorEntry[]; reason: 'constraints' | 'async' | 'async-failed' } |
No | No | Submission stopped: constraint or rule errors, async validator errors, or async validator rejection/timeout. |
aui-state-change |
FormState |
No | No | dirty, touched, submitting, submitCount, or valid changed (deduplicated). |
aui-reset |
null |
No | No | The form was reset (reset() or a native reset). |
Content areas
Section titled “Content areas”| Area | Description |
|---|---|
controls |
Consumer-owned children: AbsoluteUI controls, Form Field hosts, plain native elements, and buttons. |
CSS classes
Section titled “CSS classes”All rules live in the aui.components cascade layer, so application styles override them without specificity battles.
| Class | Purpose |
|---|---|
aui-form |
Added to the <form>: single-column grid with a space-4 gap. |
aui-form[aria-busy='true'] |
Async validation in progress (progress cursor). |
aui-form__actions |
Optional consumer wrapper for buttons: wrapping flex row. |
Design tokens
Section titled “Design tokens”Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.
--aui-color-text | --aui-font-family-sans | --aui-space-2 | --aui-space-4
TypeScript definitions
Section titled “TypeScript definitions”type FocusOnInvalid = 'summary' | 'first-field';type SubmitMode = 'event' | 'native';type FormValues = Readonly<Record<string, unknown>>;type FormErrors = Readonly<Record<string, string>>;interface CrossFieldRule { readonly fields: readonly string[]; readonly validate: (values: FormValues) => string | null; readonly target?: string }type AsyncFormValidator = (values: FormValues, signal: AbortSignal) => Promise<FormErrors>;interface FormState { readonly dirty: boolean; readonly touched: boolean; readonly submitting: boolean; readonly submitCount: number; readonly valid: boolean }interface FormErrorEntry { readonly name: string | null; readonly label: string; readonly message: string }interface FormSubmitDetail { values: FormValues }interface FormInvalidDetail { errors: readonly FormErrorEntry[]; reason: 'constraints' | 'async' | 'async-failed' }Also exported: registerFormControl(control: FormControl): () => void, FormControl, ControlValidity, ValidationTiming, FieldValidity, ValidationErrorCode, TextConstraints, NumberConstraints, Outcome, MessageContext, and the functions getFormControl, findFormControls, checkText, checkNumber, messageFor.
Accessibility
Section titled “Accessibility”Pattern: Native <form> with the GOV.UK error summary pattern: validate on submit, then move focus to a summary of links to each invalid field.
Keyboard
Section titled “Keyboard”| Key | Context | Action |
|---|---|---|
| Enter | Single-line input focused | Native implicit submission: runs the Form submit pipeline. |
| Tab / Shift+Tab | Error summary focused | Moves through the summary links, then on into the form. |
| Enter | Summary link focused | Moves focus to the invalid control and scrolls it into view. |
| Enter / Space | Reset button focused | Native reset, intercepted: every control returns to its initial value. |
| Element | Role |
|---|---|
| form | form (native; exposed as a landmark only when the consumer gives it an accessible name) |
| error summary | region labelled by its heading (see Error Summary) |
ARIA attributes
Section titled “ARIA attributes”| Attribute | Element | Value |
|---|---|---|
aria-busy |
form | “true” while the async validator runs; removed when it settles, times out, or is aborted |
aria-invalid / aria-describedby |
controls | Set by each control when its message is shown (submit marks every control as submitted) |
After a failed submission focus moves to the error summary (default) or, with focusOnInvalid ‘first-field’ or errorSummary false, to the first invalid control. setErrors moves focus the same way. A successful submission does not move focus.
WCAG 2.2 success criteria addressed
Section titled “WCAG 2.2 success criteria addressed”| Criterion | Level | Note |
|---|---|---|
| 1.3.1 Info and Relationships | A | |
| 2.1.1 Keyboard | A | |
| 2.4.3 Focus Order | A | |
| 3.3.1 Error Identification | A | |
| 3.3.3 Error Suggestion | AA | |
| 4.1.2 Name, Role, Value | A | |
| 4.1.3 Status Messages | AA |
Automated test results: Accessibility and compliance.
Notes and known caveats
Section titled “Notes and known caveats”- The form sets noValidate so native validation bubbles never compete with AbsoluteUI messages; destroy() restores the original value.
- Focusing the summary makes screen readers read its heading and list once; no live region is used.
- Messages that belong to no field (unknown setErrors names, async failure) appear in the summary; without a summary they are announced through an assertive live region.
- Blur/pointer deferral: when focus leaves a field because the user pressed the pointer elsewhere (for example on Submit), the field’s message renders after the pointer is released, so the layout shift does not make the click miss its target.
- aria-busy is set during async validation; consider a visible status for long checks.
- Summary focus and announcement have not yet been manually verified with NVDA, JAWS, and VoiceOver.
- Form does not announce a successful submission; announce the outcome from the aui-submit handler.
Behavior
Section titled “Behavior”States
Section titled “States”pristine, dirty, touched, validating (submitting, aria-busy), submitted (submitCount > 0), invalid (summary shown), valid.
State transitions
Section titled “State transitions”| From | Trigger | To |
|---|---|---|
| pristine | user edits a control | dirty; aui-state-change |
| any | submit with invalid controls or failing rules | invalid; summary shown; aui-invalid (constraints) |
| valid constraints | submit with validate |
submitting (aria-busy) |
| submitting | validator resolves {} |
aui-submit; native submit if submitMode: 'native' |
| submitting | validator returns errors | invalid; aui-invalid (async) |
| submitting | rejection or timeout | invalid; form-level message; aui-invalid (async-failed) |
| submitting | reset() or destroy() |
validator aborted; result discarded |
| submitted | user edits any field | rules re-run live |
| any | reset() or native reset |
pristine, untouched, submitCount 0; aui-reset |
Accessibility notes
Section titled “Accessibility notes”-
noValidateis set so native bubbles never compete with AbsoluteUI messages (the original value is restored ondestroy()). -
Focus moves to the summary after a failed submit, so screen readers read the heading and list once, without a live region.
focusOnInvalid: 'first-field'focuses the first invalid control instead. -
Messages that belong to no field appear in the summary; with
errorSummary: falsethey are announced through an assertive live region. -
Blur/pointer deferral (
markTouched): when focus leaves a field because the user pressed the pointer elsewhere, for example on Submit, the field’s message renders after the pointer is released. Rendering on pointerdown shifts the layout and makes the pending click miss the button (covered by a regression test). -
A successful submission is not announced; announce the outcome in your
aui-submithandler. -
Manual screen-reader verification is pending (see Known limitations).
-
Error summary links name their field: each item is the localized message
form.summaryItem('{label}: {message}'), for example “Email: This field is required.” Override it per locale withregisterMessages('form', locale, { summaryItem: ... }). Messages set withsetErrorsfor unknown or disabled fields are listed as given.
Responsive behavior
Section titled “Responsive behavior”Single-column grid; .aui-form__actions wraps. Consumer layout rules override both (layered styles).
Right-to-left
Section titled “Right-to-left”No direction-specific layout. The summary and controls use logical properties.
Theming
Section titled “Theming”Uses only text color, font, and spacing tokens. The summary and controls provide their own themed states.
Global configuration
Section titled “Global configuration”configureAbsoluteUI({ components: { form: { focusOnInvalid: 'first-field', asyncTimeoutMs: 5000 } } });Values are read at each submission. A global locale change updates an existing summary.
Component configuration
Section titled “Component configuration”Instance options override global and theme defaults. rules, validate, summaryTitle, locale, and the global options are dynamic through update(). summaryHost is read only when the summary is first created (on the first failed submission); set it before then.
Timezones
Section titled “Timezones”Not applicable. Date controls serialize their own ISO strings.
None on the form. The summary uses the Error Summary default icon (alert-circle).
Security
Section titled “Security”Summary items and announcements use textContent. setErrors messages are shown as text; do not echo raw user input into them. Form performs no network requests; the async validator is consumer code and should honour its AbortSignal. CSP-safe.
Data handling
Section titled “Data handling”Values stay in memory. Nothing is persisted or sent unless submitMode: 'native' submits the form or your handler sends values. Disabled controls and :disabled elements (including those inside a disabled <fieldset>) are never serialized.
Performance
Section titled “Performance”Five listeners on the form (submit, reset, aui-input, aui-change, focusout) and one configuration subscription. controls is a querySelectorAll on access; validation and serialization are linear in the number of controls. After the first submit, rules re-run on every edit. Budget: validate() over 200 invalid Text Inputs completes in under 150 ms in headless Chromium, including rendering 200 messages and moving focus. About 25 ms is script; the rest is the browser layout for the new messages.
Supported data scale
Section titled “Supported data scale”200 controls in the performance test; unit tests cover forms with up to six controls plus plain elements.
Edge cases
Section titled “Edge cases”- A second submit while the async validator runs is ignored;
submitCountdoes not change. - A validator that ignores its signal still settles on timeout: the form stops waiting and reports
async-failed. - A rule returning
''counts as valid. When several rules target one field, the first failing rule’s message wins. - A rule targeting a field that is not a named control throws
FORM_RULE_TARGETfromvalidate()and submit; live re-checks after the first submit skip it. - Errors returned for a disabled control are listed in the summary as plain text, without a link.
setErrorswith only unknown names shows a summary with plain-text items and no links.
Errors
Section titled “Errors”| Code | Category | Cause |
|---|---|---|
FORM_HOST |
configuration | element is not an HTMLFormElement. |
FORM_OPTIONS |
configuration | Rule without non-empty string fields or a validate function, non-function validate, or non-element summaryHost. |
FORM_RULE_TARGET |
configuration | On validate() or submit, a failing rule’s target (or last field) is not a named control in the form. |
CFG_INVALID_VALUE |
configuration | Invalid value for a global or instance config option (for example asyncTimeoutMs outside 100..120000). |
aui-invalid reason |
validation | constraints, async, or async-failed. |
A failed construction leaves the form untouched (noValidate unchanged, no listeners).
Known limitations
Section titled “Known limitations”- Double submission is blocked only while the async validator runs.
- Plain native elements are serialized and reset but not validated.
- File inputs are not serialized by
getValues(). - Summary focus and announcements have not yet been manually verified with NVDA, JAWS, and VoiceOver.
Unsupported scenarios
Section titled “Unsupported scenarios”- Nested forms or several
Forminstances on one element. - Native browser validation bubbles.
- HTML in error messages.