# Forms

How to build a form with fields, a settings card, validation errors and a save bar.

ferry-ui gives the layout and the accessibility of a form. Your code holds the values and does the validation.

## Stack a label on a control

[Field](/docs/components/field) puts a label above a control, and a hint or an error below it. It connects them with the `id` and the ARIA attributes. Use it in a dialog, a popover or a sign-in form.

Put one control in a `Field`. For a control with many parts, such as [Select](/docs/components/select), pass a function as the child. Spread its `control` argument on the part that gets the focus.

```tsx
import { Button, Field, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, toast } from 'ferry-ui'

export default function StackedFields() {
  return (
    <form
      className="flex w-full max-w-sm flex-col gap-5"
      onSubmit={(event) => {
        event.preventDefault()
        toast.success('Invitation sent')
      }}
    >
      <Field label="Email" hint="The member gets a link by email.">
        <Input type="email" name="email" placeholder="maya@example.com" />
      </Field>
      <Field label="Role" hint="An admin can manage billing and members.">
        {/* A control with many parts: spread `control` on the part that gets the focus. */}
        {(control) => (
          <Select name="role" defaultValue="member">
            <SelectTrigger {...control} className="w-full">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="admin">Admin</SelectItem>
              <SelectItem value="member">Member</SelectItem>
              <SelectItem value="viewer">Viewer</SelectItem>
            </SelectContent>
          </Select>
        )}
      </Field>
      <Field label="Team" optional>
        <Input name="team" placeholder="Design" />
      </Field>
      <Button type="submit" variant="primary" size="md" className="self-end">
        Send invitation
      </Button>
    </form>
  )
}
```

A [Button](/docs/components/button) has `type="button"` by default. Set `type="submit"` on the button that submits the form.

## Build a settings section

[Form Card](/docs/components/form-card) is a card for one subject of a settings page. A `FormRow` has a label on the left and a control on the right. `FormActions` is the footer. Its Save button stays disabled until `dirty` is `true`.

```tsx
import * as React from 'react'
import {
  Checkbox,
  FormActions,
  FormCard,
  FormRow,
  Input,
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
  toast,
} from 'ferry-ui'

const SETTINGS = { name: 'Billing portal', currency: 'eur', digest: true }

export default function SettingsSection() {
  const [saved, setSaved] = React.useState(SETTINGS)
  const [draft, setDraft] = React.useState(SETTINGS)
  const dirty = draft.name !== saved.name || draft.currency !== saved.currency || draft.digest !== saved.digest

  return (
    <FormCard
      title="General"
      description="These settings apply to all the members of the project."
      onSubmit={(event) => {
        event.preventDefault()
        setSaved(draft)
        toast.success('Settings saved')
      }}
      footer={<FormActions dirty={dirty} onReset={() => setDraft(saved)} />}
    >
      <FormRow label="Name" description="The name shows in the list of projects." htmlFor="settings-name">
        <Input value={draft.name} onChange={(event) => setDraft({ ...draft, name: event.target.value })} />
      </FormRow>
      <FormRow label="Currency" description="The currency of new invoices." htmlFor="settings-currency">
        {(control) => (
          <Select value={draft.currency} onValueChange={(currency) => setDraft({ ...draft, currency })}>
            <SelectTrigger {...control} className="w-full">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="usd">US dollar</SelectItem>
              <SelectItem value="eur">Euro</SelectItem>
              <SelectItem value="gbp">Pound sterling</SelectItem>
            </SelectContent>
          </Select>
        )}
      </FormRow>
      <FormRow label="Weekly digest" description="A summary by email each Monday." htmlFor="settings-digest">
        <Checkbox checked={draft.digest} onCheckedChange={(checked) => setDraft({ ...draft, digest: checked === true })} />
      </FormRow>
    </FormCard>
  )
}
```

Set `htmlFor` on a `FormRow`. The row then gives the `id` to its control. Do not set `id`, `aria-describedby` or `aria-invalid` on the control.

## Show validation errors

ferry-ui does not validate. Your code finds the errors and gives each message to a component.

| Problem | Component |
| --- | --- |
| One field is not valid | `error` on `Field` or `FormRow` |
| The submit failed | [Callout](/docs/components/callout) with `tone="destructive"`, near the submit button |
| A save failed | `error` on `SaveBar` |

In this demo, submit the empty form. Then enter a name. Submit again.

```tsx
import * as React from 'react'
import { Button, Callout, Field, Input } from 'ferry-ui'

export default function ValidationErrors() {
  const [name, setName] = React.useState('')
  const [nameError, setNameError] = React.useState<string>()
  const [submitError, setSubmitError] = React.useState<string>()

  function submit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setSubmitError(undefined)
    if (name.trim() === '') {
      setNameError('Enter a project name.')
      return
    }
    setNameError(undefined)
    // A real app sends the form here. In this demo, the request always fails.
    setSubmitError('The server did not save the project. Try again.')
  }

  return (
    <form noValidate onSubmit={submit} className="flex w-full max-w-sm flex-col gap-5">
      <Field label="Project name" hint="The members see this name." error={nameError}>
        <Input value={name} onChange={(event) => setName(event.target.value)} />
      </Field>
      {submitError && (
        <Callout tone="destructive" size="sm">
          {submitError}
        </Callout>
      )}
      <Button type="submit" variant="primary" size="md" className="self-end">
        Create project
      </Button>
    </form>
  )
}
```

## Add a save bar to a long form

[Save Bar](/docs/components/save-bar) is the footer of an editor or of a long form. The status is on the left. The buttons are on the right. With `sticky`, the bar stays at the bottom of the area that scrolls.

```tsx
import * as React from 'react'
import { Card, CardContent, CardHeader, CardTitle, Field, SaveBar, Textarea } from 'ferry-ui'

const MESSAGE = 'Thank you for your order. The invoice is in the attachment.'

export default function LongForm() {
  const [saved, setSaved] = React.useState(MESSAGE)
  const [message, setMessage] = React.useState(MESSAGE)
  const [saving, setSaving] = React.useState(false)
  const empty = message.trim() === ''

  function save() {
    setSaving(true)
    window.setTimeout(() => {
      setSaved(message)
      setSaving(false)
    }, 1000)
  }

  return (
    <Card className="w-full max-w-md">
      <CardHeader>
        <CardTitle>Invoice email</CardTitle>
      </CardHeader>
      <CardContent>
        <Field label="Message" hint="Each customer gets this text." error={empty ? 'Enter a message.' : undefined}>
          <Textarea value={message} disabled={saving} onChange={(event) => setMessage(event.target.value)} />
        </Field>
      </CardContent>
      <SaveBar
        dirty={message !== saved}
        invalid={empty}
        saving={saving}
        hint="All changes saved"
        onReset={() => setMessage(saved)}
        onSave={save}
      />
    </Card>
  )
}
```

<Callout tone="info" title="invalid has two meanings">
  On `SaveBar`, `invalid` disables Save. On `FormActions`, Save stays enabled, and the user can submit again.
</Callout>

## Choose the control

Each control fits one type of choice. A `Switch` applies a setting immediately. A `Checkbox` waits for the submit.

```tsx
import { Checkbox, Field, Label, RadioGroup, RadioGroupItem, Switch } from 'ferry-ui'

export default function Choices() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-5">
      {/* One value among a few options. `labelAs="span"`: a group has no single control to focus. */}
      <Field label="Billing period" labelAs="span">
        <RadioGroup defaultValue="monthly">
          <Label>
            <RadioGroupItem value="monthly" /> Monthly
          </Label>
          <Label>
            <RadioGroupItem value="yearly" /> Yearly
          </Label>
        </RadioGroup>
      </Field>
      {/* A choice that the form saves on submit. */}
      <Label>
        <Checkbox defaultChecked /> Send each invoice by email
      </Label>
      {/* A setting that applies immediately. */}
      <div className="flex items-center gap-2">
        <Switch id="choices-notifications" defaultChecked />
        <Label htmlFor="choices-notifications">Email notifications</Label>
      </div>
    </div>
  )
}
```

| Need | Use |
| --- | --- |
| One line of text | [Input](/docs/components/input) |
| Many lines of text | [Textarea](/docs/components/textarea) |
| One value among 2 to 5 options | [Radio Group](/docs/components/radio-group) |
| One value among 4 to 15 options | `Select` |
| Options with a description or an icon | [Radio Card Group](/docs/components/radio-card-group) |
| One value in a long list | [Command](/docs/components/command) in a [Popover](/docs/components/popover) |
| A setting that applies immediately | [Switch](/docs/components/switch) |
| A choice that the form saves on submit | [Checkbox](/docs/components/checkbox) |
| A compact switch between views | [Toggle Group](/docs/components/toggle-group) |
| A list of keys and values | [Key Value Editor](/docs/components/key-value-editor) |
| An action with one click, such as Delete | `ActionRow` in a `FormCard` |

## Next steps

- See a full settings page in the [Settings](/docs/examples/settings) example.
- Read [Composition](/docs/handbook/composition) for controlled and uncontrolled state.
