/** * @typedef {Object} FormStateMember - The form state member. * @property {string | number | boolean | string[] | Record | null} value - The value of the form element. Mirrors the child component's own `.value`, so the shape varies by element type (e.g. an array for `auro-checkbox-group`, an object keyed by counter name for `auro-counter-group`). A `range` `auro-datepicker` is the one form-specific case: its `.values` array is stored rather than its single `.value` string. * @property {ValidityState} validity - The validity state of the form element, stored when fired from the form element. * @property {boolean} required - Whether the form element is required or not. * @property {boolean} disabled - Whether the form element is currently disabled. Cached from the live attribute via the MutationObserver in `connectedCallback` and refreshed from `_handleAttributeMutations`. */ /** * @typedef {Object.} FormState - The form state. */ /** * The `auro-form` element provides users a way to create and manage forms in a consistent manner. * @customElement auro-form * * @slot default - The default slot for form elements. * * @event input - Fires when a child form element receives user input. * @event change - Fires when a child form element's value changes or the form is initialized. * @event reset - Fires when the form is reset. The event detail contains the previous value of the form before reset. * @event submit - Fires when the form is submitted. The event detail contains the current value of the form. */ export class AuroForm extends LitElement { static get properties(): { /** @private */ formState: { type: ObjectConstructor; attribute: boolean; }; /** @private */ _validity: { type: ObjectConstructor; attribute: boolean; }; /** @private */ _isInitialState: { type: BooleanConstructor; attribute: boolean; }; /** @private */ _elements: { type: ArrayConstructor; attribute: boolean; }; /** @private */ _submitElements: { type: ArrayConstructor; attribute: boolean; }; /** @private */ _resetElements: { type: ArrayConstructor; attribute: boolean; }; }; static get formElementTags(): string[]; static get buttonElementTags(): string[]; static get styles(): import("lit").CSSResult[]; /** * Registers the `auro-form` custom element with the browser under a given tag name. * @param {string} [name="auro-form"] - The custom element tag name to register. * * @example * AuroForm.register("custom-form") // registers as */ static register(name?: string): void; /** * @type {FormState} * @private */ private formState; /** * @type {"valid" | "invalid" | null} * @private */ private _validity; /** @private */ private _isInitialState; /** * @type {(HTMLElement & {reset: () => void})[]} * @private */ private _elements; /** * @type {HTMLButtonElement[]} * @private */ private _submitElements; /** * @type {HTMLButtonElement[]} * @private */ private _resetElements; /** * @private * @type {MutationObserver[]} */ private mutationObservers; /** * Captured initial (default) value per field `name`. Populated on first * sight of each name in `_addElementToState` and preserved across * subsequent `initializeState` cycles (slot change, rename, reset) so * `_setInitialState` can detect user edits as `current !== initial`, * matching HTML's `dirtyValueFlag` semantics. * @private * @type {Record | null | undefined>} */ private _initialValues; /** * @private * @type {MutationObserver | null} */ private _attributeObserver; /** * Handle batched MutationObserver records for `disabled` and `name` * attribute changes on tracked form elements. A `name` change invalidates * the formState keying — we resolve it by re-initializing state. A `disabled` * change simply needs a re-render (so `value` / `validity` getters re-evaluate) * and a refresh of the submit/reset button enablement. * @param {MutationRecord[]} mutations - The batched mutation records. * @returns {void} * @private */ private _handleAttributeMutations; /** * Resets all form elements to their initial state and fires a `reset` event. The event's `detail.previousValue` contains the form values captured immediately before the reset. * @returns {void} */ reset(): void; /** * Validates all form elements. If all are valid, fires a `submit` event with `detail.value` containing the current form values. If any element is invalid, its error state is surfaced and the `submit` event is not fired. * @returns {Promise} */ submit(): Promise; /** * Shared input listener for all form elements. * @param {Event} event - The event that is fired from the form element. * @private */ private sharedInputListener; /** * Shared validation listener for all form elements. * @param {Event} event - The event that is fired from the form element. * @private */ private sharedValidationListener; /** * Mutation observer for form elements. Slot change does not trigger unless * root-level elements are added/removed. This is a workaround to ensure * nested form elements are also observed. * * @returns {void} * @private */ private mutationEventListener; /** * Handle Enter key press on form elements. * @param {KeyboardEvent} event - The keyboard event. * @private */ private handleKeyDown; /** * Compare tag name with element to identify it (for API purposes). * @param {string} elementTag - The HTML tag name like `auro-datepicker`. * @param {HTMLElement} element - The actual HTML element to compare. * @returns {boolean} * @private */ private _isElementTag; /** * Shared code for determining if an element is something we care about (submit, form element, etc.). * @param {string[]} collection - The array to use for tag name search. * @param {HTMLElement} element - The element to compare against the master list. * @returns {boolean} * @private */ private _isInElementCollection; /** * Check if the tag name is a form element. * @param {HTMLElement} element - The element to check (attr or tag name). * @returns {boolean} * @private */ private isFormElement; /** * Whether a given element is currently disabled. Disabled controls are excluded * from submission, validity, and initial-state checks per the HTML spec * (section 4.10.19.2 "Enabling and disabling form controls": * https://www.w3.org/TR/2011/WD-html5-20110113/association-of-controls-and-forms.html). * * Implementation note: we deliberately read only the attribute. Every Auro * form element in `formElementTags` declares `disabled` with `reflect: true`, * so the attribute and property stay in sync. Reading the attribute also * lets the MutationObserver in `connectedCallback` (which is filtered to * `['disabled', 'name']`) be the single source of truth for re-renders. * If a future form-element type ships without attribute reflection, expand * this helper to also read `element.disabled`. * @param {HTMLElement | undefined | null} element - The element to check. * @returns {boolean} * @private */ private _isDisabled; /** * Whether the tracked form element registered under `name` is currently disabled. * See `_isDisabled` for the HTML-spec rationale behind excluding disabled * controls from form state. * * Reads a cached flag on `formState[name]` populated by `_addElementToState` * at registration and refreshed by `_handleAttributeMutations` whenever the * element's `disabled` attribute toggles. The cache is fed by the same * `hasAttribute('disabled')` read as `_isDisabled`, so the "future form-element * type without attribute reflection" caveat documented there applies here too. * @param {string} name - The `name` attribute used to register the element. * @returns {boolean} * @private */ private _isNameDisabled; /** * Validates if an event is from a valid form element with a name. * @param {Event} event - The event to validate. * @returns {boolean} - True if event is valid for processing. * @private */ private _eventIsValidFormEvent; /** * Check if the tag name is a button element. * @param {HTMLElement} element - The element to check. * @returns {boolean} * @private */ private isButtonElement; /** * Returns the current values of all named, enabled form elements as a key-value object, keyed by each element's `name` attribute. Each value is the child component's own `.value`, so the shape depends on the element type — see that component's documentation for its exact shape (for example, `auro-checkbox-group` yields an array, `auro-counter-group` yields an object keyed by counter name, and `auro-select` with `multiSelect` yields a JSON-encoded string). The one form-specific exception is a `range` `auro-datepicker`, whose `.values` array (`[start, end]`) is stored instead of its single `.value` string. * @returns {Record | null>} The current form values. */ get value(): Record | null>; /** * Getter for internal _submitElements. * @returns {HTMLButtonElement[]} * @private */ private get submitElements(); /** * Returns a collection of elements that will reset the form. * @returns {HTMLButtonElement[]} * @private */ private get resetElements(); /** * Raw constraint-validation check. Returns `true` when no enabled field * has a validity error. Unlike the public `validity` getter, this does * NOT gate on `isInitialState` — callers that need to make a decision * based on the actual constraint state (submit-button enablement, the * internal `submit()` gate) read this so a pre-filled valid form is * correctly recognized as submittable at first render. * @returns {boolean} * @private */ private _isFormValid; /** * Whether the reset button should be enabled. True when the form has * diverged from its initial state (so the user can always return to * defaults — even if the dirty value lives behind a now-disabled field), * OR when any non-disabled field has a current value or captured initial * value (covers pre-filled forms and user-cleared-back-to-empty cases). * @returns {boolean} * @private */ private _hasResetableState; /** * Collapse empty representations to a single canonical `null`. * * `_addElementToState` captures `null` for a field that mounts without a * `value` attribute (`element.value || element.getAttribute('value')` is * falsy → resolves to `null`), but `sharedInputListener` later stores the * raw `event.target.value` — which is `''` for a user-cleared text input. * Without this normalization, backspacing back to empty would taint the * form forever (`'' !== null`) and Reset would stay enabled with nothing * to actually reset. * * `''`, `undefined`, and `[]` all collapse to `null`. The empty-array case * covers checkbox-group, radio-group, and multiselect, where `[]` means * "no selection" — semantically the same as `null`/`''`. Genuine values * — including `0`, `false`, non-empty strings, and non-empty arrays — * pass through unchanged so number, boolean, and populated multi-value * fields still compare correctly. * @param {*} value - Value to normalize. * @returns {*} * @private */ private _normalizeEmpty; /** * Infer validity status based on current formState. * * Validity stays `null` while the form is in its initial state — this is * the "stay quiet until the user interacts" UX contract that consumers * depend on to delay error indicators. Code that needs the raw * constraint-validation result regardless of interaction (e.g., * submit-button enablement) should call `_isFormValid()` directly. * @private */ private _calculateValidity; /** * Returns `'valid'` if all required and interacted-with form elements are valid, `'invalid'` if any are not, or `null` if the form has not been interacted with yet. * @returns {"valid" | "invalid" | null} */ get validity(): "valid" | "invalid" | null; /** * Determines whether the form is in its initial (untouched) state. * * A field is tainted if either: * - its value differs from the value captured on first render, OR * - its validity is failing (anything other than `null` or `'valid'`). * * Validity acts as a backup signal: it catches users who interact with a * field without changing its value (e.g., focusing and blurring a required * field). We skip `null` (not yet validated) and `'valid'` (the default * after Auro's auto-validation on mount) because neither proves the user * touched anything. * @returns {void} * @private */ private _setInitialState; /** * Returns `true` if no form element has been interacted with or had its value changed since the form was initialized or last reset. * @returns {boolean} */ get isInitialState(): boolean; /** * Enables or disables submit and reset buttons based on the current form state and validity. * @returns {void} * @private */ private setDisabledStateOnButtons; /** * Construct the query strings from elements, append them together, execute, and return the NodeList. * @returns {NodeList} * @private */ private queryAuroElements; /** * Store an element in state and on the _elements array. * @param {HTMLElement} element - The element to add to our state. * @private */ private _addElementToState; /** * Initialize (or reinitialize) the form state. * @returns {void} * @private */ private initializeState; /** * Attaches input, validation, and keydown listeners to all tracked form and button elements. * Removes existing listeners first to avoid duplicates on re-initialization. * @returns {void} * @private */ private _attachEventListeners; /** * Slot change event listener. This is the main entry point for the form element. * @param {Event} event - The slot change event. * @returns {void} * @private */ private onSlotChange; /** * @returns {import('lit').TemplateResult} */ render(): import("lit").TemplateResult; } /** * - The form state member. */ export type FormStateMember = { /** * - The value of the form element. Mirrors the child component's own `.value`, so the shape varies by element type (e.g. an array for `auro-checkbox-group`, an object keyed by counter name for `auro-counter-group`). A `range` `auro-datepicker` is the one form-specific case: its `.values` array is stored rather than its single `.value` string. */ value: string | number | boolean | string[] | Record | null; /** * - The validity state of the form element, stored when fired from the form element. */ validity: ValidityState; /** * - Whether the form element is required or not. */ required: boolean; /** * - Whether the form element is currently disabled. Cached from the live attribute via the MutationObserver in `connectedCallback` and refreshed from `_handleAttributeMutations`. */ disabled: boolean; }; /** * - The form state. */ export type FormState = { [x: string]: FormStateMember; }; import { LitElement } from "lit";