Field
Groups a label, form control, description, and validation message. Field wires accessible relationships between labels and controls, tracks focused, filled, dirty, touched, valid, and invalid states, and supports native and custom validation.
import { Field } from "@areia/slots";
document.querySelector("#root")!.innerHTML = `
<div data-slot="field" class="grid w-72 gap-1">
<label data-slot="field-label" class="font-bold">Email</label>
<input
data-slot="field-control"
type="email"
name="email"
required
class="win95-input"
/>
<p data-slot="field-description" class="text-xs text-neutral-700">Use a work email address.</p>
<div data-slot="field-error" class="text-xs text-red-800"></div>
</div>
`;
const root = document.querySelector('[data-slot="field"]')!;
const controller = Field.createField(root, {
name: "email",
validationMode: "onBlur",
});
Import
import { Field } from "@areia/slots";
Usage
Add data-slot="field" to a container element and place the sub-slot elements inside. Call Field.createField(root) to initialize the controller.
import { Field } from "@areia/slots";
// Auto-bind a single root
const root = document.querySelector('[data-slot="field"]');
const controller = Field.createField(root, {
name: "username",
validationMode: "onBlur",
});
// Auto-bind all unbound roots in scope
const controllers = Field.create();
Expected Markup
The controller expects a root element with data-slot="field" and the following sub-slots.
<div data-slot="field">
<label data-slot="field-label">Email</label>
<input
data-slot="field-control"
type="email"
name="email"
required
/>
<p data-slot="field-description">Use a work email address.</p>
<div data-slot="field-error"></div>
<output data-slot="field-validity"></output>
</div>
Data Slots
| Slot | Required | Description |
|---|---|---|
field | Yes | Root container. |
field-label | No | Label element(s). Auto-wired to the control via for / id. |
field-control | No | The form control. Auto-detected if omitted: input, textarea, select, button, [contenteditable], or [tabindex]. |
field-description | No | Help text associated with the control via aria-describedby. |
field-error | No | Error message shown when the field is invalid. |
field-validity | No | Output element that receives dataset.valid, dataset.error, and dataset.errors. |
field-item | No | Additional elements that receive state attribute updates. |
Generated Attributes
The controller writes the following attributes on initialization and updates them as state changes on the root and all sub-slot elements:
| Attribute | Description |
|---|---|
data-valid | Present when the field is valid. |
data-invalid | Present when the field is invalid. |
data-dirty | Present when the user has modified the control value. |
data-touched | Present when the user has blurred the control. |
data-filled | Present when the control has a non-empty value. |
data-focused | Present when the control currently has focus. |
data-disabled | Present when the field is disabled. |
ARIA wiring applied by the controller:
for/idassociation between labels and the control.aria-labelledbyon the control referencing labels.aria-describedbyon the control referencing descriptions and errors.aria-invalidon the control.
Events
Outbound
| Event | Detail | Description |
|---|---|---|
field:validity-change | { valid: boolean, validity: FieldValidityData } | Fired on the root after validation state changes. |
field:change | { value: string, dirty: boolean, filled: boolean } | Fired on the root on control input and change. |
field:dirty-change | { dirty: boolean } | Fired when the dirty state changes. |
field:touched-change | { touched: boolean } | Fired when the touched state changes. |
field:filled-change | { filled: boolean } | Fired when the filled state changes. |
field:focus-change | { focused: boolean } | Fired when focus enters or leaves the control. |
Inbound
| Event | Detail | Description |
|---|---|---|
field:validate | — | Dispatch on the root to trigger a validation run. |
field:set-invalid | { error?: string | string[] } | Dispatch on the root to force an invalid state with an optional custom error. |
field:clear-invalid | — | Dispatch on the root to clear forced invalid state and re-run validation. |
API Reference
Options
| Option | Type | Default | Description |
|---|---|---|---|
name | string | control name | Field name. Used to name the control element. |
disabled | boolean | false | Disables the field and control. |
invalid | boolean | false | Forces the field into an invalid state. |
dirty | boolean | false | Initial dirty state. |
touched | boolean | false | Initial touched state. |
validationMode | "onBlur" | "onChange" | "onSubmit" | "onBlur" | When native and custom validation is committed. |
validate | (value: string, control: HTMLElement) => string | string[] | null | undefined | false | — | Custom validation function. Return a non-empty value to mark invalid. Supports async. |
validationDebounceTime | number | 0 | Debounce custom validation on change events, in milliseconds. |
onValidityChange | (valid: boolean) => void | — | Called after validation state changes. |
Controller
| Member | Type | Description |
|---|---|---|
name | string | undefined (getter) | The field name. |
valid | boolean (getter) | Whether the field is valid (no native or custom errors). |
invalid | boolean (getter) | Whether the field is invalid (forced or validation errors). |
dirty | boolean (getter) | Whether the user has modified the control value. |
touched | boolean (getter) | Whether the user has blurred the control. |
filled | boolean (getter) | Whether the control has a non-empty value. |
focused | boolean (getter) | Whether the control currently has focus. |
validity | FieldValidityData (getter) | The current validity state snapshot. |
validate() | () => Promise<FieldValidityData> | Runs native and custom validation, returning the result. |
setInvalid(invalid, error?) | (invalid: boolean, error?: string | string[]) => void | Sets or clears a forced invalid state with an optional error message. |
clearInvalid() | () => void | Clears forced invalid state and re-runs validation. |
destroy() | () => void | Removes all event listeners and cleans up the controller. |
FieldValidityData
Snapshot returned by controller.validity and controller.validate().
| Property | Type | Description |
|---|---|---|
valid | boolean | Aggregate validity — true when all constraints pass. |
valueMissing | boolean | Required constraint failed. |
typeMismatch | boolean | Value does not match the input type. |
patternMismatch | boolean | Value does not match the pattern attribute. |
tooLong | boolean | Value exceeds maxlength. |
tooShort | boolean | Value is shorter than minlength. |
rangeUnderflow | boolean | Value is below min. |
rangeOverflow | boolean | Value is above max. |
stepMismatch | boolean | Value does not match the step constraint. |
badInput | boolean | Input could not be parsed. |
customError | boolean | Custom validation failed. |
error | string | The first error message. |
errors | string[] | All collected error messages. |
Controller
The controller is returned by Field.createField() and provides imperative control over the field's state.
import { Field } from "@areia/slots";
const root = document.querySelector('[data-slot="field"]');
const controller = Field.createField(root, {
name: "email",
validate: (value) =>
value.includes("@") ? null : "Enter a valid email address.",
});
// Read state
console.log(
controller.valid,
controller.dirty,
controller.filled,
);
// Validate on demand
const result = await controller.validate();
if (!result.valid) {
console.error(result.errors);
}
// Force an error from server state
controller.setInvalid(true, "Email already taken.");
// Clear forced errors
controller.clearInvalid();
// Tear down
controller.destroy();
Form Reset
When the control is inside a <form>, the controller listens for reset events and resets dirty, touched, and validity state automatically.