Skip to content

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

SlotRequiredDescription
fieldYesRoot container.
field-labelNoLabel element(s). Auto-wired to the control via for / id.
field-controlNoThe form control. Auto-detected if omitted: input, textarea, select, button, [contenteditable], or [tabindex].
field-descriptionNoHelp text associated with the control via aria-describedby.
field-errorNoError message shown when the field is invalid.
field-validityNoOutput element that receives dataset.valid, dataset.error, and dataset.errors.
field-itemNoAdditional 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:

AttributeDescription
data-validPresent when the field is valid.
data-invalidPresent when the field is invalid.
data-dirtyPresent when the user has modified the control value.
data-touchedPresent when the user has blurred the control.
data-filledPresent when the control has a non-empty value.
data-focusedPresent when the control currently has focus.
data-disabledPresent when the field is disabled.

ARIA wiring applied by the controller:

  • for / id association between labels and the control.
  • aria-labelledby on the control referencing labels.
  • aria-describedby on the control referencing descriptions and errors.
  • aria-invalid on the control.

Events

Outbound

EventDetailDescription
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

EventDetailDescription
field:validateDispatch 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-invalidDispatch on the root to clear forced invalid state and re-run validation.

API Reference

Options

OptionTypeDefaultDescription
namestringcontrol nameField name. Used to name the control element.
disabledbooleanfalseDisables the field and control.
invalidbooleanfalseForces the field into an invalid state.
dirtybooleanfalseInitial dirty state.
touchedbooleanfalseInitial touched state.
validationMode"onBlur" | "onChange" | "onSubmit""onBlur"When native and custom validation is committed.
validate(value: string, control: HTMLElement) => string | string[] | null | undefined | falseCustom validation function. Return a non-empty value to mark invalid. Supports async.
validationDebounceTimenumber0Debounce custom validation on change events, in milliseconds.
onValidityChange(valid: boolean) => voidCalled after validation state changes.

Controller

MemberTypeDescription
namestring | undefined (getter)The field name.
validboolean (getter)Whether the field is valid (no native or custom errors).
invalidboolean (getter)Whether the field is invalid (forced or validation errors).
dirtyboolean (getter)Whether the user has modified the control value.
touchedboolean (getter)Whether the user has blurred the control.
filledboolean (getter)Whether the control has a non-empty value.
focusedboolean (getter)Whether the control currently has focus.
validityFieldValidityData (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[]) => voidSets or clears a forced invalid state with an optional error message.
clearInvalid()() => voidClears forced invalid state and re-runs validation.
destroy()() => voidRemoves all event listeners and cleans up the controller.

FieldValidityData

Snapshot returned by controller.validity and controller.validate().

PropertyTypeDescription
validbooleanAggregate validity — true when all constraints pass.
valueMissingbooleanRequired constraint failed.
typeMismatchbooleanValue does not match the input type.
patternMismatchbooleanValue does not match the pattern attribute.
tooLongbooleanValue exceeds maxlength.
tooShortbooleanValue is shorter than minlength.
rangeUnderflowbooleanValue is below min.
rangeOverflowbooleanValue is above max.
stepMismatchbooleanValue does not match the step constraint.
badInputbooleanInput could not be parsed.
customErrorbooleanCustom validation failed.
errorstringThe first error message.
errorsstring[]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.