# Form integration

Field owns a control’s relationship to the form. Read this for the validation modes, and for how a control of your own joins in.

`Field.Root` owns one control’s relationship to the form: its name, its
validity, and the label, description and error around it.

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

const email = ref('')
</script>

<template>
  <Field.Root :options="{ name: 'email' }">
    <Field.Label>Email</Field.Label>
    <Field.Description>We only use it to sign you in.</Field.Description>
    <Input v-model="email" type="email" required />
    <Field.Error>Enter a valid email address.</Field.Error>
  </Field.Root>
</template>
```

`Field.Label` is the `Label` component re-exported, so a field’s anatomy reads
in one piece.

## Validation modes

Validation is configured through the root’s bundled `options` object.

```vue
<template>
  <Field.Root
    :options="{
      name: 'email',
      validate: validateEmail,
      validationMode: 'onChange',
      validationDebounce: 200,
    }"
  />
</template>
```

::docs-table
---
columns:
  - label: Option
  - label: Default
rows:
  - items:
      - label: validate
        description: Runs against the value and the values of the sibling fields, and returns a message, an array of messages, or `null`. May be async.
      - label: none
        plaintext: true
  - items:
      - label: validationMode
        description: 'Runs validation on `''onSubmit''`, `''onBlur''` or `''onChange''`.'
      - label: '''onBlur'''
  - items:
      - label: validationDebounce
        description: 'Milliseconds to wait before running. Applies in `''onChange''` mode only.'
      - label: '0'
---
::

Submitting a surrounding `<form>` always runs validation, whatever the mode is
set to, since the mode only decides what happens before submission. Once an
`'onSubmit'` field has run, it keeps running on every later change, so a
message already on screen updates as the reader fixes the value.

`validate` receives a second argument: the values of every field registered
with the surrounding [`Form`](/components/form), keyed by name. Outside a
`Form` it is an empty object.

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

## Native constraint validation

`Field` finds the native control inside itself and reads its `ValidityState`,
so `required`, `type="email"`, `min`, `maxlength` and the rest keep working
with no configuration.

A browser refuses to fire `submit` while one of those constraints fails, and
`'onSubmit'` mode depends on that event. `Form` renders `novalidate` and
validates its fields itself, so the run happens and the native flags reach
`Field.Error`. On a plain `<form>`, set `novalidate` yourself, or the field
never receives the submit event.

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

Native messages come first and whatever `validate` returns follows, both in the
same array.

```vue
<template>
  <Field.Error v-slot="{ messages }">{{ messages.join(' ') }}</Field.Error>
</template>
```

To render one error per failure reason, match a single constraint with the
`match` prop, which takes a `ValidityState` key or a predicate over the
snapshot the last run took. A message from `validate` sets `customError`.

```vue
<template>
  <Field.Error match="valueMissing">Enter an email address.</Field.Error>
  <Field.Error match="typeMismatch">That is not an email address.</Field.Error>
</template>
```

## Styling from field state

Every control and every `Field` part writes the same state attributes, so a
label can be styled from the control’s validity with no wrapper class.

```css
[data-scope='label'][data-invalid='true'] {
  color: var(--app-color-danger-fg-muted);
}
```

The full table is in [Styling](/components/styling#the-field-state-set).

## Form participation

Controls that are not a native input render a visually hidden input, so
submission, reset and `FormData` work with no wiring from you.

::docs-table
---
columns:
  - label: Component
  - label: Hidden input
rows:
  - items:
      - label: Checkbox
      - label: One `checkbox`, plus a `hidden` input carrying `uncheckedValue` when set.
        plaintext: true
  - items:
      - label: Radio
      - label: One `radio` per registered item.
        plaintext: true
  - items:
      - label: Switch
      - label: One `checkbox`.
        plaintext: true
  - items:
      - label: Toggle
      - label: One `checkbox`, only with a `name`.
        plaintext: true
  - items:
      - label: '`Select`, `Combobox`'
        plaintext: true
      - label: One `hidden` input per selected value, only with a `name`.
        plaintext: true
  - items:
      - label: Slider
      - label: One `range` input per thumb. A range slider submits two values under one name.
        plaintext: true
---
::

`Input`, `Textarea` and `Field.Control` render the native element directly and
need none of this, while `Progress`, `Avatar` and `Label` take no part in
forms.

`readOnly` is a state rather than a submission rule, so the hidden input gets
no `readonly` attribute and a read-only control still submits. Only `disabled`
takes a control out of the payload.

::note
`Select` and `Combobox` submit through `input[type="hidden"]`, which browsers
exclude from constraint validation, so validate those two through the field’s
`validate` option rather than through `required`.
::

## Joining your own control

Wrap your control in `Field.Control` with `asChild`, so the part renders no
element of its own and merges onto the single child you give it instead. The
field passes down `id`, `name`, `disabled`, `readonly`, `required` and `value`,
binds focus, blur and input, and adds the state and `aria-*` attributes.

```vue
<template>
  <Field.Root :options="{ name: 'colour' }">
    <Field.Label>Colour</Field.Label>

    <Field.Control as-child>
      <app-colour-picker v-model="colour" />
    </Field.Control>

    <Field.Error />
  </Field.Root>
</template>
```

Only one control registers per field, and a second one raises
`duplicate_field_control` rather than quietly taking over.

To drive a field from outside its subtree, give the root an `id` and read it
back with `useMirrorField`.

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

const { state, messages, value, validity, pending, validate, reset } =
  useMirrorField('email')
</script>
```

`Field.Validity` does the same job inside the field’s own subtree: it renders
nothing and hands the flags, the messages and the values to its slot.

## Further reading

- [Field](/components/field): the full anatomy, props and errors.
- [Styling](/components/styling): the four places every part leaves the
  appearance to you.
