# Form

A form that validates every field it holds before it lets a submit through, and shows the errors your server sends back.

Form wraps a set of `Field`s and takes care of the two things a plain `<form>`
leaves to you: it runs every field’s validation before the submit goes
anywhere, and it hands the errors your server sends back to the field each one
belongs to. Reach for it whenever a form has more than one field, or talks to a
server that can disagree with it.

::component-preview{name="FormPreview"}
```vue
<template>
  <Form
    v-model:errors="errors"
    class="flex w-[17rem] flex-col gap-5"
    @submit="onSubmit"
  >
    <Field.Root
      class="group flex flex-col gap-1.5"
      :options="{ name: 'email', validate: validateEmail }"
    >
      <div :class="box">
        <div :class="stack">
          <span :class="line">
            <Input
              v-model="email"
              :class="control"
              type="email"
              placeholder=" "
              autocomplete="email"
            />
          </span>
          <Label :class="label">Email</Label>
        </div>
      </div>

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

    <Field.Root
      class="group flex flex-col gap-1.5"
      :options="{ name: 'password', validate: validatePassword }"
    >
      <div :class="box">
        <div :class="stack">
          <span :class="line">
            <Input
              v-model="password"
              :class="control"
              type="password"
              placeholder=" "
              autocomplete="new-password"
            />
          </span>
          <Label :class="label">Password</Label>
        </div>
      </div>

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

    <Button :class="button" type="submit" :loading="pending">
      {{ pending ? 'Creating account…' : 'Create account' }}
    </Button>

    <p :class="hint">
      Sign up with taken@example.com to see what a server error looks like.
    </p>
  </Form>
</template>

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

import type { FormErrors } from '@maas/mirror/vue'

const email = ref('')
const password = ref('')
const pending = ref(false)
const errors = ref<FormErrors>({})

async function onSubmit(event: SubmitEvent) {
  const data = new FormData(event.target as HTMLFormElement)

  pending.value = true
  errors.value = await signUp(String(data.get('email') ?? ''))
  pending.value = false
}

function signUp(address: string): Promise<FormErrors> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(
        address === 'taken@example.com'
          ? { email: 'That email is already taken.' }
          : {}
      )
    }, 600)
  })
}

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

  if (entry.length === 0) {
    return 'Enter an email address.'
  }

  return entry.includes('@') ? null : 'Enter a valid email address.'
}

function validatePassword(value: unknown) {
  return String(value ?? '').length >= 8
    ? null
    : 'Use eight characters or more.'
}

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',
].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',
].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',
].join(' ')

const button = [
  'inline-flex h-12 items-center justify-center gap-1.5 px-[1.125rem] whitespace-nowrap',
  'rounded-component-lg border-2 border-[transparent] type-component-lg',
  'bg-primary-solid text-primary-on-solid',
  'transition-all duration-100 ease-linear',
  'outline-4 outline-transparent focus-visible:focus-ring',
  'hover:bg-primary-solid-hover active:bg-primary-solid-active',
  'data-[loading=true]:bg-disabled-solid data-[loading=true]:text-disabled-on-solid',
].join(' ')

const hint = 'type-component-2xs text-surface-muted'
const error = 'type-component-2xs text-danger-muted'
</script>
```
::

Signing up with `taken@example.com` fails on the server, and the message clears
again as soon as the email changes.

## Usage guidelines

- Errors are keyed by the `name` on `Field.Root`, so a field without a name
  cannot be told anything. Name every field that talks to a server.
- The form always calls `preventDefault()`, because validation can be
  asynchronous and the browser would otherwise navigate before the answer
  arrives. Read the values off the event in your handler, or off your own model.
- Both `Field` and `Form` validate on submit. Inside a `Form` the field steps
  back and lets the form drive, so a validator never runs twice for one submit.
- A field that fails through the server still counts as valid to the form, since
  the message did not come from a validator. Submit to the server again rather
  than reading the form’s own validity.

## Anatomy

One form around any number of fields.

::component-anatomy
---
parts:
  - name: Form
    required: true
    description: Renders a <form novalidate>. Owns the fields and the server errors.
    children:
      - name: Field.Root
        description: Registers itself with the form around it. Any number of them.
---
::

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

<template>
  <Form v-model:errors="errors" @submit="onSubmit">
    <Field.Root :options="{ name: 'email', validate: validateEmail }">
      <Label>Email</Label>
      <Input v-model="email" type="email" />
      <Field.Error v-slot="{ messages }">{{ messages.join(' ') }}</Field.Error>
    </Field.Root>

    <Button type="submit">Create account</Button>
  </Form>
</template>
```

## Examples

### What a submit does

A submit runs every field’s validation, waits for all of them, and only emits
`submit` once they all pass. If any of them fails, the form sends focus to the
first one that did and the event never leaves the component.

```vue
<template>
  <Form @submit="onSubmit">
    <Field.Root :options="{ name: 'email', validate: validateEmail }">
      <Label>Email</Label>
      <Input v-model="email" type="email" />
    </Field.Root>

    <Button type="submit">Create account</Button>
  </Form>
</template>

<script setup lang="ts">
function onSubmit(event: SubmitEvent) {
  const data = new FormData(event.target as HTMLFormElement)

  console.log(data.get('email'))
}
</script>
```

Fields answer in the order they appear in the document, whatever order they
mount in, so the focus always lands on the first problem the reader can see.

### Errors from the server

`errors` is an object keyed by field name, and each value is one message or
several. The matching `Field.Error` renders it exactly as if a validator had
returned it.

```vue
<template>
  <Form v-model:errors="errors" @submit="onSubmit">
    <Field.Root :options="{ name: 'email' }">
      <Label>Email</Label>
      <Input v-model="email" type="email" />
      <Field.Error v-slot="{ messages }">{{ messages.join(' ') }}</Field.Error>
    </Field.Root>
  </Form>
</template>

<script setup lang="ts">
import { ref } from 'vue'

import type { FormErrors } from '@maas/mirror/vue'

const email = ref('')
const errors = ref<FormErrors>({})

async function onSubmit() {
  errors.value = await signUp(email.value)
}
</script>
```

The message belongs to the value that produced it, so the first keystroke in
that field takes it off again. That is what `v-model:errors` is for: the form
hands back the object with the key removed. If you would rather keep the object
yourself, listen for `clearErrors`, which carries the same payload.

An error keyed to a name no field claims stays in the object and renders
nowhere, so a server that reports on fields the current step does not show
breaks nothing.

### Validating without submitting

`useMirrorForm(id)` reaches the form from anywhere, which is what a wizard needs
when the button that moves to the next step is not a submit button.

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

const { validate, invalid, submitting } = useMirrorForm('sign-up')

async function next() {
  const { valid } = await validate()

  if (valid) {
    step.value++
  }
}
</script>

<template>
  <Form id="sign-up">
    <Field.Root :options="{ name: 'email', validate: validateEmail }">
      <Label>Email</Label>
      <Input v-model="email" type="email" />
    </Field.Root>
  </Form>

  <Button :disabled="invalid" :loading="submitting" @click="next">Next</Button>
</template>
```

`validate()` reports which fields failed by name, so a summary above the form
can list them.

```ts
const { valid, invalid } = await validate()
// { valid: false, invalid: ['email', 'password'] }
```

### Starting over

`reset()` clears the messages, the run history and the server errors, and a
native `<button type="reset">` does the same thing. Values belong to your model,
so neither of them touches those.

```vue
<template>
  <Form @submit="onSubmit">
    <Field.Root :options="{ name: 'email', validate: validateEmail }">
      <Label>Email</Label>
      <Input v-model="email" type="email" />
    </Field.Root>

    <Button type="submit">Create account</Button>
    <Button type="reset" @click="email = ''">Start over</Button>
  </Form>
</template>
```

### Showing that something is happening

`submitting` is on while the form validates, and it is yours to hold on for as
long as the request takes.

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

const { submitting } = useMirrorForm('sign-up')

async function onSubmit() {
  submitting.value = true
  errors.value = await signUp(email.value)
  submitting.value = false
}
</script>

<template>
  <Form id="sign-up" v-model:errors="errors" @submit="onSubmit">
    <Button type="submit" :loading="submitting">Create account</Button>
  </Form>
</template>
```

The same state is on the element as `data-submitting` and in the default slot,
so a spinner somewhere inside the form needs neither the composable nor an ID.

```vue
<template>
  <Form v-slot="{ submitting, invalid }">
    <span v-if="submitting">Checking…</span>
  </Form>
</template>
```

## API reference

**Standalone in shape, module underneath.** One part, a bundled `options`
object, and `useMirrorForm(id)` as the programmatic API. `Field` registers with
the form it finds around it and needs no wiring of its own.

### `Form`

Renders a `<form novalidate>`.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: 'The instance ID, for [`useMirrorForm`](#composable).'
      - label: string
      - label: generated
        plaintext: true
  - items:
      - label: options
        description: The bundled configuration below.
      - label: FormOptions
      - label: '{}'
  - items:
      - label: errors
        description: 'Server errors, keyed by [field name](#errors-from-the-server).'
      - label: 'Record<string, string | string[]>'
        escape: true
      - label: undefined
---
::

#### Options

::docs-table
---
columns:
  - label: Option
  - label: Type
  - label: Default
rows:
  - items:
      - label: validateOnSubmit
        description: 'Runs every field’s validation [on submit](#what-a-submit-does).'
      - label: boolean
      - label: 'true'
  - items:
      - label: focusOnError
        description: Sends focus to the first field that failed.
      - label: boolean
      - label: 'true'
  - items:
      - label: novalidate
        description: Sets the native attribute.
      - label: boolean
      - label: 'true'
---
::

#### Emits

::docs-table
---
columns:
  - label: Emit
  - label: Payload
rows:
  - items:
      - label: submit
        description: Every field passed.
      - label: SubmitEvent
  - items:
      - label: update:errors
        description: The form cleared an error.
      - label: 'Record<string, string | string[]>'
        escape: true
  - items:
      - label: clearErrors
        description: The same moment and the same payload as `update:errors`.
      - label: 'Record<string, string | string[]>'
        escape: true
---
::

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: invalid
        description: Mirrors `data-invalid`.
      - label: boolean
  - items:
      - label: submitting
        description: Mirrors `data-submitting`.
      - label: boolean
  - items:
      - label: errors
        description: The errors the form currently holds.
      - label: 'Record<string, string | string[]>'
        escape: true
---
::

#### Exposed

::docs-table
---
columns:
  - label: Method
  - label: Returns
rows:
  - items:
      - label: validate()
        description: Runs every field’s validation and reports which ones failed.
      - label: 'Promise<{ valid: boolean; invalid: string[] }>'
        escape: true
  - items:
      - label: reset()
        description: Clears the messages and the server errors. Leaves the values alone.
      - label: void
---
::

### Composable

`useMirrorForm(id)` reaches a form from anywhere in the app.

::docs-table
---
columns:
  - label: Key
  - label: Type
rows:
  - items:
      - label: errors
        description: The errors the form currently holds.
      - label: ComputedRef<FormErrors>
        escape: true
  - items:
      - label: invalid
        description: True while any field in the form is invalid.
      - label: ComputedRef<boolean>
        escape: true
  - items:
      - label: submitting
        description: On while the form validates. Writable, so a request can hold it.
      - label: WritableComputedRef<boolean>
        escape: true
  - items:
      - label: validate
        description: Runs every field’s validation and reports which ones failed.
      - label: () => Promise<FormValidity>
        escape: true
  - items:
      - label: reset
        description: Clears the messages and the server errors.
      - label: () => void
        escape: true
---
::

### Data attributes

::data-attributes
::

### CSS variables

None. `Form` renders no functional CSS.

### Errors

None of its own. `Form` has one part and asserts nothing, so the only codes you
can see from it are the rack’s.

## Accessibility

`Form` renders a native `<form>` and adds no roles. `novalidate` turns off the
browser’s own validation bubbles, which cannot be styled and disappear on the
next keystroke, and hands the job to `Field.Error`, which is a `role="alert"`
and is announced where it sits.

On a failed submit the form sends focus to the first control that failed, in
document order, so the reader lands on the problem rather than at the top of the
page. `focusOnError` turns that off if you would rather move focus somewhere
else yourself.

### Keyboard

::docs-table
---
columns:
  - label: Key
  - label: Behaviour
rows:
  - items:
      - label: Enter
      - label: Submits the form from any single-line control inside it, as a native form does.
        plaintext: true
---
::
