## Overview

HTML forms expect native elements (`<input>`, `<select>`) to participate in submission, validation, and reset. Custom elements are invisible to forms by default. AdiaUI solves this with `UIFormElement`, a base class that bridges custom elements into the native form lifecycle via the `ElementInternals` API.

Any component that extends `UIFormElement` can be placed inside a `<form>` and will:

- Submit its value with the form (via `name` attribute)
- Participate in constraint validation (required, pattern, minlength, maxlength)
- Surface validation errors automatically on submit, blur, and input
- Reset when the form resets
- Be disabled when the form or fieldset is disabled

## Components Using UIFormElement

The following components extend `UIFormElement` and participate in forms:

Component | Tag | Value Type
--- | --- | ---
Input | `input-ui` | String
Textarea | `textarea-ui` | String
Select | `select-ui` | String
Check | `check-ui` | String (on/off)
Switch | `switch-ui` | String (on/off)
Radio | `radio-ui` | String
Slider | `slider-ui` | Number (as string)
Range | `range-ui` | Number (as string)
Search | `search-ui` | String
Segmented | `segmented-ui` | String
OTP Input | `otp-input-ui` | String
Calendar Picker | `calendar-picker-ui` | Date string
Color Picker | `color-picker-ui` | Color string
Upload | `upload-ui` | File(s)

## ElementInternals

`ElementInternals` is the browser API that lets custom elements act as form controls. AdiaUI's `UIFormElement` sets `static formAssociated = true` to opt in, then uses the internals object to:

- `setFormValue(val)` — set the value submitted with the form
- `setValidity(flags, message, anchor)` — set validation state
- `checkValidity()` / `reportValidity()` — trigger validation
- Access `.form`, `.labels`, `.validity`

class UIFormElement extends UIElement { static formAssociated = true; // Provided by the browser when formAssociated is true: // this.internals = this.attachInternals(); get form() { return this.internals.form; } get validity() { return this.internals.validity; } get validationMessage() { return this.internals.validationMessage; } }

## Constraint Validation

`UIFormElement` implements four constraint types that mirror native HTML validation:

Attribute | Validity Flag | Default Message
--- | --- | ---
`required` | `valueMissing` | "This field is required."
`pattern="regex"` | `patternMismatch` | "Please match the requested format."
`minlength="n"` | `tooShort` | "Please use at least n characters."
`maxlength="n"` | `tooLong` | "Please use no more than n characters."

<form> <input-ui name="email" required pattern="[^@]+@[^@]+" data-msg-required="Email is required" data-msg-pattern="Enter a valid email"> </input-ui> <input-ui name="username" required minlength="3" maxlength="20" data-msg-minlength="Username must be at least 3 characters" data-msg-maxlength="Username cannot exceed 20 characters"> </input-ui> <button-ui type="submit" text="Submit" variant="primary"></button-ui> </form>

## Auto-Validation Lifecycle

AdiaUI implements a progressive validation UX that avoids showing errors prematurely. The lifecycle is:

1. On form submit: The browser fires the `invalid` event on any field that fails validation. `UIFormElement` catches this, sets `aria-invalid="true"`, and populates the `error` attribute with the validation message.
2. On input (while invalid): If the field currently has an error, every keystroke re-validates. As soon as the value passes, the error is cleared immediately. This gives instant positive feedback.
3. On blur (if dirty): When the user tabs away from a field they have typed in, validation runs. This catches errors on fields the user has interacted with but not submitted.
4. On form reset: All validation state is cleared — `error` attribute removed, `aria-invalid` removed, dirty flag reset.

The "dirty" flag ensures blur validation only fires after the user has actually interacted with the field. A pristine field that receives focus and then loses it will not show an error.

## Custom Error Messages

Override the default validation messages using `data-msg-*` attributes on any form element:

Attribute | Overrides
--- | ---
`data-msg-required` | Message when field is empty and `required`
`data-msg-pattern` | Message when value doesn't match `pattern`
`data-msg-minlength` | Message when value is shorter than `minlength`
`data-msg-maxlength` | Message when value exceeds `maxlength`

<input-ui name="phone" required pattern="\d{3}-\d{3}-\d{4}" data-msg-required="Phone number is required" data-msg-pattern="Use format: 123-456-7890"> </input-ui>

## Manual Control

For cases where you need to validate programmatically or set custom errors (e.g. server-side validation), use these methods:

Method | Returns | Description
--- | --- | ---
`validate()` | `boolean` | Runs all constraints, surfaces error if invalid, clears if valid
`setInvalid(message)` | `void` | Sets a custom error — useful for async/server validation
`setValid()` | `void` | Clears all validation errors and `aria-invalid`
`syncValue(val)` | `void` | Updates the form value and re-runs constraint validation

const emailField = document.querySelector('input-ui[name="email"]'); // Programmatic validation if (!emailField.validate()) { console.log(emailField.validationMessage); } // Server-side error async function onSubmit() { const res = await fetch('/api/check-email', { ... }); if (!res.ok) { emailField.setInvalid('This email is already registered'); } } // Clear error after user action emailField.setValid(); // Update form value programmatically emailField.syncValue('new@example.com');

## Form Lifecycle Callbacks

`UIFormElement` implements the standard form-associated custom element callbacks. Subclasses can override the hook methods:

Browser Callback | Override Hook | When It Fires
--- | --- | ---
`formResetCallback()` | `onFormReset()` | When the parent form is reset
`formDisabledCallback(disabled)` | `onFormDisabled(disabled)` | When a parent fieldset is disabled/enabled
`formAssociatedCallback(form)` | `onFormAssociated(form)` | When the element is associated with a form
`formStateRestoreCallback(state, reason)` | `onFormStateRestore(state, reason)` | When browser restores form state (back/forward navigation)

## Live Demo

Try submitting the form below without filling in the fields to see auto-validation in action:

## Extending UIFormElement

To create a new form-participating component, extend `UIFormElement` and call `syncValue()` whenever the internal value changes:

import { UIFormElement } from '@core/form.js'; class MyRating extends UIFormElement { static properties = { ...UIFormElement.properties, max: { type: Number, default: 5, reflect: true }, }; render() { // Render stars, handle clicks... } #onStarClick(rating) { this.value = String(rating); this.syncValue(this.value); // updates form + runs validation this.dispatch('change', { value: this.value }); } onFormReset() { this.value = ''; // Reset visual state... } } customElements.define('my-rating', MyRating);

Always call `syncValue()` after changing the component's value. This updates the form's submission data and runs constraint validation so errors clear as soon as the value becomes valid.

## Authoring rules

- Extend `UIFormElement`, not `UIElement`, when the component must participate in native form submission. `UIFormElement` handles `ElementInternals`, constraint validation, and the dirty-state lifecycle automatically.
- Always call `syncValue()` after changing the component's internal value. This updates the form's submission data and runs constraint validation so errors clear as soon as the value becomes valid.
- Do not manipulate the `field-ui` error attribute directly. Use `setCustomError()` and `clearErrors()` from the base class — these coordinate with the field wrapper's ARIA live region.
- Every form control needs a `name` attribute for its value to be included in `FormData` on submit. Components without `name` are invisible to the form.
- Required validation belongs on `field-ui required`, not only on the inner control. The field wrapper surfaces the asterisk and the ARIA required state; the inner control handles native constraint validation.
- Never bypass the auto-validation lifecycle by setting `validity.valid = false` from the outside. If you need a custom error from application code, use the MCP `submit_feedback` pattern or call `setCustomValidity()` on the internal form element.