# Field

Groups a label, a control, a description and an error message, and owns the validity state they all read.

Wrap a control in `Field` and it picks up a label, a description, an error
message and the validity state behind them. Reach for it whenever a control
needs a name, a hint, or something to say when the value is wrong.

::component-preview{name="FieldPreview"}
```vue
<template>
  <Field.Root
    class="group flex w-[17rem] flex-col gap-1.5"
    :options="{ name: 'username', validate: validateUsername }"
  >
    <div :class="box">
      <div :class="stack">
        <span :class="line">
          <Input
            v-model="username"
            :class="control"
            placeholder=" "
            autocomplete="off"
          />
        </span>
        <Label :class="label">Username</Label>
      </div>
    </div>

    <Field.Description :class="hint">
      Three characters or more. Validated when you leave the field.
    </Field.Description>

    <Field.Error v-slot="{ messages }" :class="error">
      {{ messages.join(' ') }}
    </Field.Error>
  </Field.Root>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Field, Input, Label } from '@maas/mirror/vue'

const username = ref('')

const box = [
  'relative isolate flex h-12 w-full cursor-text items-center justify-between gap-1.5',
  'rounded-component-lg border-surface border-2 px-[0.875rem]',
  'transition-all duration-100 ease-linear [&_*]:transition-all [&_*]:duration-100 [&_*]:ease-linear',
  'outline-4 outline-transparent focus-within:focus-ring',
  'group-[[data-invalid=true]:not([data-focused=true])]:border-danger-subtle',
  'group-[[data-invalid=true]:not([data-focused=true])]:bg-danger-subtle',
  'group-data-[disabled=true]:border-disabled-subtle group-data-[disabled=true]:cursor-not-allowed',
].join(' ')

const stack = [
  'group/stack flex h-full max-h-full w-full flex-col-reverse',
  'items-center justify-center gap-0 px-1',
  'focus-within:gap-1 has-[input:not(:placeholder-shown)]:gap-1',
].join(' ')

const line = [
  'relative flex h-0 w-full items-end',
  'group-focus-within/stack:h-[0.9375rem]',
  'has-[input:not(:placeholder-shown)]:h-[0.9375rem]',
].join(' ')

const control = [
  'type-component-lg leading-[normal]! text-surface',
  'block box-content h-[1lh] w-full py-1.25 -my-1.25 appearance-none bg-transparent outline-none',
  'placeholder:text-transparent',
  'group-[[data-invalid=true]:not([data-focused=true])]:text-danger-on-muted',
  'data-[disabled=true]:text-disabled-solid',
].join(' ')

const label = [
  'type-component-lg leading-[normal]! text-surface-muted',
  'flex h-full w-full items-center [--mirror-label-cursor:text]',
  "before:absolute before:-inset-0.5 before:-z-10 before:content-['']",
  'group-focus-within/stack:h-auto group-focus-within/stack:text-[0.6875rem] group-focus-within/stack:[--mirror-label-cursor:auto]',
  'group-has-[input:not(:placeholder-shown)]/stack:h-auto group-has-[input:not(:placeholder-shown)]/stack:text-[0.6875rem] group-has-[input:not(:placeholder-shown)]/stack:[--mirror-label-cursor:auto]',
  'group-[[data-invalid=true]:not([data-focused=true])]:text-danger-muted',
  'group-data-[disabled=true]:text-disabled-muted',
].join(' ')

const hint = 'type-component-2xs text-surface-muted'
const error = 'type-component-2xs text-danger-muted'

function validateUsername(value: unknown) {
  const entry = String(value ?? '')

  if (entry.length === 0) {
    return 'Enter a username.'
  }

  return entry.length < 3 ? 'Use at least three characters.' : null
}
</script>
```
::

## Usage guidelines

- Only one control may register per field, since it owns `data-focused` and the
  `aria-describedby` chain, and a second one throws `duplicate_field_control`.
- A control reports its initial value as it registers, which counts as a change,
  so `onChange` validation leaves an empty required field invalid before the
  user has touched it. We default to `onBlur` for that reason.
- Give a second `Field.Description` or `Field.Error` its own `id`, or both take
  the field’s generated ID and `aria-describedby` lists it once.
- In `onSubmit` mode the browser blocks its own `submit` event while a native
  control fails a constraint, so the field never hears it. Wrap the fields in
  [`Form`](/components/form), which sets `novalidate` for you, or set
  `novalidate` on your own `<form>`.

## Anatomy

Assemble the parts around one control.

::component-anatomy
---
parts:
  - name: Field.Root
    required: true
    description: Renders a <div>. Owns the validity state every other part reads.
    children:
      - name: Field.Label
        description: Label, re-exported. Renders a <label> pointed at the control.
      - name: Field.Control
        description: Renders an <input>. Only for native or third-party controls.
      - name: Field.Description
        description: Renders a <p>, registered into the control’s aria-describedby.
      - name: Field.Error
        description: Renders a <div role="alert">. Only while the field is invalid.
      - name: Field.Validity
        description: Renders nothing. Hands the validity flags and the values to its slot.
---
::

```vue
<script setup lang="ts">
import { Field, Input } from '@maas/mirror/vue'
</script>

<template>
  <Field.Root :options="{ name: 'email', validate: validateEmail }">
    <Field.Label>Email</Field.Label>
    <Input v-model="email" type="email" />
    <Field.Description>We only use this to send receipts.</Field.Description>
    <Field.Error v-slot="{ messages }">{{ messages.join(' ') }}</Field.Error>
  </Field.Root>
</template>
```

## Examples

### Wiring a control

A Mirror control joins the surrounding field on its own: it takes the field’s
`controlId`, inherits `name`, `disabled`, `readOnly` and `required`, and writes
the field state set onto its own element.

```vue
<template>
  <Field.Root :options="{ name: 'terms', disabled: true, required: true }">
    <Field.Label>Accept the terms</Field.Label>
    <Checkbox.Root v-model="accepted">
      <Checkbox.Indicator>✓</Checkbox.Indicator>
    </Checkbox.Root>
  </Field.Root>
</template>
```

A prop on the control wins over the field, so one control can opt out.

```vue
<template>
  <Field.Root :options="{ name: 'notes', required: true }">
    <Field.Label>Notes</Field.Label>
    <Textarea v-model="notes" :required="false" />
  </Field.Root>
</template>
```

For a native or third-party control, `Field.Control` hands the association to
its child under `asChild`. It follows what was typed whether or not you bind
`v-model`, so `data-dirty`, `data-filled` and `validate` see the value either
way. Wiring up a control of your own is covered in
[Form integration](/components/form-integration).

```vue
<template>
  <Field.Root :options="{ name: 'colour' }">
    <Field.Label>Colour</Field.Label>
    <Field.Control as-child>
      <input v-model="colour" type="color" />
    </Field.Control>
  </Field.Root>
</template>
```

### Validation

`validate` receives the value the control last reported and the values of every
field registered with the surrounding [`Form`](/components/form), keyed by name.
It returns a message, an array of messages, or `null` for a pass, and it may be
async.

```vue
<template>
  <Field.Root :options="{ name: 'handle', validate: checkHandle }">
    <Field.Label>Handle</Field.Label>
    <Input v-model="handle" />
    <Field.Error v-slot="{ messages }">{{ messages.join(' ') }}</Field.Error>
  </Field.Root>
</template>

<script setup lang="ts">
const handle = ref('')

async function checkHandle(value: unknown) {
  const taken = await isTaken(String(value ?? ''))

  return taken ? 'That handle is taken.' : null
}
</script>
```

`validationMode` decides when it runs, and `validationDebounce` puts a delay in
front of the `onChange` mode.

```vue
<template>
  <Field.Root
    :options="{
      name: 'search',
      validate: validateSearch,
      validationMode: 'onChange',
      validationDebounce: 300,
    }"
  >
    <Field.Label>Search</Field.Label>
    <Input v-model="query" />
  </Field.Root>
</template>
```

The second argument is how one field reads another, and it is filled in by the
surrounding `Form`. Outside one it is an empty object.

```vue
<template>
  <Form>
    <Field.Root :options="{ name: 'password' }">
      <Field.Label>Password</Field.Label>
      <Input v-model="password" type="password" />
    </Field.Root>

    <Field.Root :options="{ name: 'confirm', validate: matchPassword }">
      <Field.Label>Repeat password</Field.Label>
      <Input v-model="confirm" type="password" />
    </Field.Root>
  </Form>
</template>

<script setup lang="ts">
function matchPassword(value: unknown, formValues: Record<string, unknown>) {
  return value === formValues.password ? null : 'The two do not match.'
}
</script>
```

An async `validate` that resolves after a newer run has started is dropped, so
the slower answer never overwrites the newer one. While a run is in flight
the root publishes `pending`.

Every mode also validates when the closest `<form>` submits, though the field
never calls `preventDefault`, so blocking the submission stays with the form.
In `onSubmit` mode a field that has run once keeps running on every later
change, so a message the reader is looking at follows the edit that fixes it.
Each run reads native constraint validation too, prepending the control’s
`validationMessage` to whatever `validate` returned. To validate from the form
itself, keep a template ref on the root and call `validate()`.

```vue
<template>
  <form @submit.prevent="submit">
    <Field.Root
      ref="field"
      :options="{
        name: 'email',
        validate: validateEmail,
        validationMode: 'onSubmit',
      }"
    >
      <Field.Label>Email</Field.Label>
      <Input v-model="email" />
    </Field.Root>
    <Button type="submit">Save</Button>
  </form>
</template>

<script setup lang="ts">
const field = useTemplateRef('field')

async function submit() {
  const { valid } = await field.value.validate()

  if (valid) {
    await save()
  }
}
</script>
```

A browser blocks its own `submit` event while a native control fails a
constraint, which is the one moment `onSubmit` mode needs it. `Form` renders
`novalidate` by default and validates every field it holds, so the field hears
the submission and the native `ValidityState` still reaches `Field.Error`. On a
plain `<form>`, set `novalidate` yourself.

```vue
<template>
  <Form>
    <Field.Root :options="{ name: 'email', validationMode: 'onSubmit' }">
      <Field.Label>Email</Field.Label>
      <Input v-model="email" type="email" required />
      <Field.Error match="valueMissing">Enter an email address.</Field.Error>
    </Field.Root>
  </Form>
</template>
```

### Error messages

`Field.Error` renders a `<div role="alert">` once the field is invalid, so a
message appearing after blur is announced without moving focus.

```vue
<template>
  <Field.Error v-slot="{ messages }">
    <ul>
      <li v-for="message in messages" :key="message">{{ message }}</li>
    </ul>
  </Field.Error>
</template>
```

`match` narrows an error to one native `ValidityState` flag, or to a predicate
over the whole snapshot the last run took. A message from `validate` sets
`customError`, so an error can separate your own rules from the browser’s.

```vue
<template>
  <Field.Root :options="{ name: 'age', required: true }">
    <Field.Label>Age</Field.Label>
    <Input v-model="age" type="number" min="18" />
    <Field.Error id="age-missing" match="valueMissing">
      Enter your age.
    </Field.Error>
    <Field.Error id="age-range" match="rangeUnderflow">
      You must be 18 or older.
    </Field.Error>
  </Field.Root>
</template>
```

If you want to transition the message yourself, `forceMount` keeps the node
mounted whatever the validity.

```vue
<template>
  <Field.Error v-slot="{ invalid, messages }" force-mount>
    <Transition name="message">
      <p v-if="invalid">{{ messages.join(' ') }}</p>
    </Transition>
  </Field.Error>
</template>
```

### Reading the validity

`Field.Validity` renders nothing at all. It hands the flags of the last run,
the messages, the current value and the value the field started with to its
slot, which is where a summary or a debug panel gets them from.

```vue
<template>
  <Field.Root :options="{ name: 'age', validate: checkAge }">
    <Field.Label>Age</Field.Label>
    <Input v-model="age" type="number" min="18" required />

    <Field.Validity v-slot="{ validity, errors, pending }">
      <p v-if="pending">Checking…</p>
      <p v-else-if="validity?.rangeUnderflow">Too young.</p>
      <p v-else-if="errors.length">{{ errors.join(' ') }}</p>
    </Field.Validity>
  </Field.Root>
</template>
```

The flags are a snapshot rather than the live `ValidityState` of the element,
so every run publishes a new one and a template that reads them keeps up.

### Forcing and observing state

`invalid` overrides the outcome of validation entirely, which is how a response
from the server gets in.

```vue
<template>
  <Field.Root :invalid="!!serverError" :options="{ name: 'email' }">
    <Field.Label>Email</Field.Label>
    <Input v-model="email" />
    <Field.Error>{{ serverError }}</Field.Error>
  </Field.Root>
</template>
```

`dirty` and `touched` are overrides rather than two-way bindings, so passing one
stops the field deriving it.

```vue
<template>
  <Field.Root
    :options="{ name: 'email' }"
    @validity-change="onValidityChange"
    @update:touched="onTouched"
  >
    <Field.Label>Email</Field.Label>
    <Input v-model="email" />
  </Field.Root>
</template>
```

### Styling from state

Every part writes the same field state set, so the label, the description and
the control all react to the field’s validity without a wrapper class.

```vue
<template>
  <Field.Root :options="{ name: 'email', validate: validateEmail }">
    <Field.Label>Email</Field.Label>
    <Input v-model="email" />
    <Field.Description>We only use this to send receipts.</Field.Description>
    <Field.Error v-slot="{ messages }">{{ messages.join(' ') }}</Field.Error>
  </Field.Root>
</template>

<style>
.mirror-label[data-invalid='true'] {
  color: var(--app-color-danger-fg-muted);
}

.mirror-input[data-invalid='true'] {
  border-color: var(--app-color-danger-border-subtle);
}

.mirror-field-description[data-disabled='true'] {
  color: var(--app-color-disabled-fg-muted);
}
</style>
```

The same values arrive as slot props where the template has to branch.

```vue
<template>
  <Field.Root v-slot="{ invalid, touched }" :options="{ name: 'email' }">
    <Field.Label>Email</Field.Label>
    <Input v-model="email" />
    <span v-if="touched && !invalid" aria-hidden="true">✓</span>
  </Field.Root>
</template>
```

### Reaching a field from elsewhere

Give a field an `id` and `useMirrorField(id)` reads and drives it from anywhere
in the app, with no template ref and no nesting.

```vue
<script setup lang="ts">
import { useMirrorField } from '@maas/mirror/vue'

const field = useMirrorField('signup-email')

async function submit() {
  const { valid, messages } = await field.validate()

  if (!valid) {
    report(messages)
  }
}
</script>

<template>
  <Field.Root id="signup-email" :options="{ name: 'email', validate }">
    <Field.Label>Email</Field.Label>
    <Input v-model="email" />
  </Field.Root>
  <Button @click="submit">Save</Button>
</template>
```

## API reference

**Module.** A bundled `options` object, and `useMirrorField(id)` as the
programmatic API. `Field.Label` is [`Label`](/components/label), re-exported
under this namespace.

### `Field.Root`

Renders a `<div>`.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: The instance ID. Base for `${id}-control`, `${id}-label`, `${id}-description` and `${id}-error`.
      - label: string
      - label: generated
        plaintext: true
  - items:
      - label: options
        description: Everything else. Deep-merged over the defaults.
      - label: FieldOptions
      - label: see below
        plaintext: true
  - items:
      - label: invalid
        description: 'Forces the [invalid state](#forcing-and-observing-state).'
      - label: boolean
      - label: undefined
  - items:
      - label: dirty
        description: Sets the dirty state instead of deriving it.
      - label: boolean
      - label: undefined
  - items:
      - label: touched
        description: Sets the touched state instead of deriving it.
      - label: boolean
      - label: undefined
---
::

#### Options

::docs-table
---
columns:
  - label: Option
  - label: Type
  - label: Default
rows:
  - items:
      - label: name
        description: 'Passed to the control [for form submission](/components/form-integration#form-participation).'
      - label: string
      - label: undefined
  - items:
      - label: validate
        description: 'Custom [validation](#validation), run against the value and the form’s values.'
      - label: '(value: unknown, formValues: Record<string, unknown>) => string | string[] | null | undefined | Promise<…>'
        escape: true
      - label: undefined
  - items:
      - label: validationMode
        description: 'When [`validate`](#validation) runs.'
      - label: '''onSubmit'' | ''onBlur'' | ''onChange'''
      - label: '''onBlur'''
  - items:
      - label: validationDebounce
        description: Milliseconds to debounce `onChange` validation. Ignored in the other modes.
      - label: number
      - label: '0'
  - items:
      - label: disabled
        description: 'Disables [every control](#wiring-a-control) in the field.'
      - label: boolean
      - label: 'false'
  - items:
      - label: readOnly
        description: Marks every control read-only.
      - label: boolean
      - label: 'false'
  - items:
      - label: required
        description: Marks the control required.
      - label: boolean
      - label: 'false'
---
::

`invalid`, `dirty` and `touched` stay props because they are state a consumer
takes over rather than configuration: the field derives all three on its own
until one is passed.

#### Emits

::docs-table
---
columns:
  - label: Emit
  - label: Payload
rows:
  - items:
      - label: validityChange
        description: A validation run resolves.
      - label: '{ valid: boolean; messages: string[] }'
  - items:
      - label: update:dirty
        description: The dirty state changes.
      - label: boolean
  - items:
      - label: update:touched
        description: The touched state changes.
      - label: boolean
---
::

#### Slot props

The [field state set](/components/styling#the-field-state-set), plus
`messages: string[]` and `pending: boolean`.

#### Exposed

::docs-table
---
columns:
  - label: Method
  - label: Returns
rows:
  - items:
      - label: validate()
        description: Runs validation immediately, regardless of `validationMode`.
      - label: 'Promise<{ valid: boolean; messages: string[] }>'
        escape: true
---
::

### `Field.Control`

Renders an `<input>`, for native or third-party controls. `name`, `disabled`,
`readOnly` and `required` come from `Field.Root` as native attributes rather
than as props here.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: The element’s DOM ID.
      - label: string
      - label: the field’s `controlId`
        plaintext: true
  - items:
      - label: modelValue
        description: The value. `v-model`. Only emitted from a rendered `<input>`.
      - label: 'string | number'
      - label: undefined
  - items:
      - label: defaultValue
        description: Initial value while uncontrolled.
      - label: 'string | number'
      - label: undefined
---
::

#### Emits

::docs-table
---
columns:
  - label: Emit
  - label: Payload
rows:
  - items:
      - label: update:modelValue
        description: The rendered `<input>` fires `input`.
      - label: string
  - items:
      - label: focus
        description: The control gains focus.
      - label: FocusEvent
  - items:
      - label: blur
        description: The control loses focus.
      - label: FocusEvent
---
::

#### Slot props

The field state set.

### `Field.Description`

Renders a `<p>` and registers its ID into the control’s `aria-describedby`.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: The element’s DOM ID.
      - label: string
      - label: the field’s `descriptionId`
        plaintext: true
---
::

#### Slot props

The field state set.

### `Field.Error`

Renders a `<div role="alert">` while the field is invalid, registering its ID
into `aria-describedby` for as long as it is rendered.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: The element’s DOM ID.
      - label: string
      - label: the field’s `errorId`
        plaintext: true
  - items:
      - label: match
        description: 'Narrows the error to one [validity flag](#error-messages), or to a predicate over them.'
      - label: 'keyof ValidityState | ((state: FieldValidityFlags) => boolean)'
        escape: true
      - label: undefined
  - items:
      - label: forceMount
        description: Keeps the node mounted while valid, and registered in `aria-describedby`.
      - label: boolean
      - label: 'false'
---
::

#### Slot props

The field state set, plus `messages: string[]`.

### `Field.Validity`

Renders nothing of its own, only its slot.

#### Props

None.

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: validity
        description: The flags the last run took off the native control, or `null` before the first run.
      - label: FieldValidityFlags | null
        escape: true
  - items:
      - label: errors
        description: The messages from the last run.
      - label: 'string[]'
  - items:
      - label: value
        description: The value the control last reported.
      - label: unknown
  - items:
      - label: initialValue
        description: The value the control registered with.
      - label: unknown
  - items:
      - label: pending
        description: An asynchronous run is in flight.
      - label: boolean
---
::

### `Field.Label`

[`Label`](/components/label), re-exported. Its props, slot props and errors are
documented there.

### Composable

`useMirrorField(id)` reaches a field from anywhere in the app.

::docs-table
---
columns:
  - label: Key
  - label: Type
rows:
  - items:
      - label: state
        description: The field state set.
      - label: ComputedRef<FieldControlState>
        escape: true
  - items:
      - label: messages
        description: The messages from the last run.
      - label: ComputedRef<Array<string>>
        escape: true
  - items:
      - label: value
        description: The value the control last reported.
      - label: ComputedRef<unknown>
        escape: true
  - items:
      - label: initialValue
        description: The value the control registered with.
      - label: ComputedRef<unknown>
        escape: true
  - items:
      - label: validity
        description: The flags the last run took off the native control, `null` before the first run.
      - label: ComputedRef<FieldValidityFlags | null>
        escape: true
  - items:
      - label: pending
        description: An asynchronous run is in flight.
      - label: ComputedRef<boolean>
        escape: true
  - items:
      - label: validate
        description: Runs validation immediately.
      - label: () => Promise<FieldValidityResult>
        escape: true
  - items:
      - label: reset
        description: Clears the messages and the run history. Takes the current value as the initial one.
      - label: () => void
        escape: true
---
::

### Data attributes

Every part writes the [field state set](/components/styling#the-field-state-set). `data-scope`
tells them apart, and `Field.Root` gets `data-field` instead.

::data-attributes
::

### CSS variables

None. `Field` renders no functional CSS.

### Errors

::docs-table
---
columns:
  - label: Code
rows:
  - items:
      - label: missing_field_context
        description: '`Field.Control`, `Field.Description`, `Field.Error` or `Field.Validity` rendered outside `Field.Root`.'
  - items:
      - label: duplicate_field_control
        description: A second control registered in the same `Field.Root`.
  - items:
      - label: invalid_validation_mode
        description: '`validationMode` received an undocumented value, at mount or later.'
---
::

## Accessibility

`Field` sets up relationships rather than keyboard behaviour. The label gets
`for`, the control gets `aria-describedby`, `aria-invalid` and `aria-required`,
and the error is a `role="alert"`.

The `aria-describedby` list runs descriptions first and errors after, and an
error that is not rendered is not registered. `aria-disabled` appears only where
the rendered element has no native `disabled` to set, `aria-readonly` follows
the read-only state, and a control that cannot be the target of a `<label for>`,
or whose label rendered as a `<span>`, is named through `aria-labelledby`
instead.
