# FormField

> Package `@umriss-ui/core`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/core/formfield/>. Every page in one line: [llms.txt](https://romanhaendler.github.io/umriss-ui/core/llms.txt); every page in full: [llms-full.txt](https://romanhaendler.github.io/umriss-ui/core/llms-full.txt).

The frame around a field: its label, a hint below it and the error that replaces the hint. Wrap every field in one, so the question, the help and the problem are read out together.

```ts
import { FormField, useFormField } from "@umriss-ui/core";
```

Demo page: https://romanhaendler.github.io/umriss-ui/core/formfield/

Every field of Forms, Choosing and Dates and times reads its id, description, invalid and required state from the surrounding `FormField`; an `error` marks the field invalid by itself. A control of your own gets the same through [`useFormField`](https://romanhaendler.github.io/umriss-ui/core/api/#useFormField).

`required` sets the mark and `aria-required`; it checks nothing. When and how a value is checked stays with the application.

## Examples

### Label and hint

Give every field a `label`; a `hint` below it says what the field expects and is read out with it.

```tsx
import { FormField, Input } from "@umriss-ui/core";

export default function LabelAndHint() {
  return (
    <FormField label="Cost centre" hint="As on the budget sheet, for example CC-2100." style={{ maxWidth: 360 }}>
      <Input placeholder="CC-0000" />
    </FormField>
  );
}
```

### Show an error

Pass `error` once a value fails your check; it replaces the hint and marks the field invalid.

```tsx
import { FormField, Grid, Input } from "@umriss-ui/core";

export default function ShowAnError() {
  return (
    <Grid minItemWidth="240px" gap={4}>
      <FormField label="Cost centre" hint="As on the budget sheet, for example CC-2100.">
        <Input defaultValue="CC-2100" />
      </FormField>
      <FormField
        label="Cost centre"
        hint="As on the budget sheet, for example CC-2100."
        error="CC-2700 is not on the budget sheet."
      >
        <Input defaultValue="CC-2700" />
      </FormField>
      <FormField
        label="Reason for the overspend, as the budget owner will read it in the monthly review"
        error="Give a reason of at least ten characters, so the review can follow the decision without asking again."
      >
        <Input defaultValue="Events" />
      </FormField>
    </Grid>
  );
}
```

### Mark a field as required

Set `required` for the mark on the label and `aria-required` on the field; checking the value stays yours.

```tsx
import { Checkbox, FormField, Grid, Input } from "@umriss-ui/core";

export default function MarkAFieldAsRequired() {
  return (
    <Grid minItemWidth="240px" gap={4}>
      <FormField label="Invoice number" required>
        <Input placeholder="INV-0000" />
      </FormField>
      <FormField label="Purchase order" hint="Optional.">
        <Input placeholder="PO-0000" />
      </FormField>
      <FormField label="Approval" required>
        <Checkbox label="I checked the amounts against the delivery note" />
      </FormField>
    </Grid>
  );
}
```

### Check an invoice on submit

Hold the values, check them all on submit, and give each `FormField` its own message; the fields follow.

```tsx
import { useState } from "react";
import type { FormEvent } from "react";
import { Button, DatePicker, FormField, Grid, Input, NumberInput, Select, Stack, Text } from "@umriss-ui/core";

interface Invoice {
  number: string;
  centre: string;
  amount: number | null;
  due: Date | null;
}

type Errors = Partial<Record<keyof Invoice, string>>;

function check(invoice: Invoice): Errors {
  const errors: Errors = {};
  if (!/^INV-\d{4}$/.test(invoice.number)) errors.number = "An invoice number reads INV- and four digits.";
  if (invoice.centre === "") errors.centre = "Choose the cost centre the invoice is booked on.";
  if (invoice.amount === null || invoice.amount <= 0) errors.amount = "Give the net amount.";
  if (invoice.due === null) errors.due = "Give the due date from the invoice.";
  return errors;
}

export default function CheckAnInvoiceOnSubmit() {
  const [invoice, setInvoice] = useState<Invoice>({ number: "INV-31", centre: "", amount: 4860, due: null });
  const [errors, setErrors] = useState<Errors>({});
  const [booked, setBooked] = useState(false);
  const set = (patch: Partial<Invoice>) => setInvoice((previous) => ({ ...previous, ...patch }));

  const submit = (event: FormEvent) => {
    event.preventDefault();
    const next = check(invoice);
    setErrors(next);
    setBooked(Object.keys(next).length === 0);
  };

  return (
    <form onSubmit={submit} noValidate style={{ maxWidth: 560 }}>
      <Stack gap={4}>
        <Grid minItemWidth="220px" gap={4}>
          <FormField label="Invoice number" required error={errors.number}>
            <Input numeric value={invoice.number} onChange={(event) => set({ number: event.target.value })} />
          </FormField>
          <FormField label="Cost centre" required error={errors.centre}>
            <Select value={invoice.centre} onChange={(event) => set({ centre: event.target.value })}>
              <option value="" disabled>
                Choose a cost centre
              </option>
              <option value="CC-1200">CC-1200 Marketing</option>
              <option value="CC-2200">CC-2200 Design</option>
              <option value="CC-4300">CC-4300 IT</option>
            </Select>
          </FormField>
          <FormField label="Net amount" required error={errors.amount}>
            <NumberInput value={invoice.amount} onChange={(amount) => set({ amount })} min={0} decimals={2} suffix="€" />
          </FormField>
          <FormField label="Due" required error={errors.due}>
            <DatePicker value={invoice.due} onChange={(due) => set({ due })} clearable />
          </FormField>
        </Grid>
        <Stack direction="row" gap={3} align="center">
          <Button type="submit" variant="primary" size="sm">
            Book the invoice
          </Button>
          {booked && (
            <Text size="sm" tone="muted">
              {invoice.number} booked on {invoice.centre}.
            </Text>
          )}
        </Stack>
      </Stack>
    </form>
  );
}
```

## Keyboard

The keys of [Input](https://romanhaendler.github.io/umriss-ui/core/input/#keyboard-input), [NumberInput](https://romanhaendler.github.io/umriss-ui/core/numberinput/#keyboard-numberinput), [Checkbox](https://romanhaendler.github.io/umriss-ui/core/checkbox/#keyboard-checkbox), [Select](https://romanhaendler.github.io/umriss-ui/core/select/#keyboard-select), [DatePicker](https://romanhaendler.github.io/umriss-ui/core/datepicker/#keyboard-datepicker) and [Button](https://romanhaendler.github.io/umriss-ui/core/button/#keyboard-button) apply here.

## API

### `FormFieldProps`

| Prop | Type | Default | Description |
|---|---|---|---|
| `label` *required* | `ReactNode` | — | The label of the field. It is mandatory: a field without a name is a field without a question for a screen reader.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/formfield/#configurator-formfield), [Label and hint](https://romanhaendler.github.io/umriss-ui/core/formfield/#label-and-hint), [Show an error](https://romanhaendler.github.io/umriss-ui/core/formfield/#show-an-error) and 77 more |
| `hint` | `ReactNode` | — | Help text below the field. Replaced by `error`.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/formfield/#configurator-formfield), [Label and hint](https://romanhaendler.github.io/umriss-ui/core/formfield/#label-and-hint), [Show an error](https://romanhaendler.github.io/umriss-ui/core/formfield/#show-an-error) and 33 more |
| `error` | `ReactNode` | — | Error message; it marks the field as invalid at the same time.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/formfield/#configurator-formfield), [Show an error](https://romanhaendler.github.io/umriss-ui/core/formfield/#show-an-error), [Check an invoice on submit](https://romanhaendler.github.io/umriss-ui/core/formfield/#check-an-invoice-on-submit) and 22 more |
| `required` | `boolean` | `false` | Sets the required mark on the label and `aria-required` on the field - on Input, Select, Textarea, NumberInput, Combobox, RadioGroup and Checkbox. The triggers of the pickers and of the MultiSelect are buttons, on which `aria-required` is not a permitted attribute; there the mark carries it alone. It validates nothing - validation belongs to the caller ([ADR-0060](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0060-validation-and-a-layout-editor-are-never-time-zones-not-yet.md)).<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/formfield/#configurator-formfield), [Mark a field as required](https://romanhaendler.github.io/umriss-ui/core/formfield/#mark-a-field-as-required), [Check an invoice on submit](https://romanhaendler.github.io/umriss-ui/core/formfield/#check-an-invoice-on-submit) and 9 more |
| `fieldId` | `string` | — | Optional: a fixed id for the input element (instead of a generated one).<br>Shown in: [Sum up a form's errors (Alert)](https://romanhaendler.github.io/umriss-ui/core/alert/#above-a-form) |
| `ref` | `Ref<HTMLDivElement>` | — | Receives the component's root element. From `RefProp`.<br>Shown in: [A row that holds still (Sizes)](https://romanhaendler.github.io/umriss-ui/core/sizes/#a-row-that-holds-still), [An anchored surface (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#anchored-surface), [Take the width of the trigger (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#width-from-anchor) and 2 more |

Also takes every attribute of `<div>`.

## Known limits

- No validation: the caller's form library decides; pass `error` ([ADR-0060](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0060-validation-and-a-layout-editor-are-never-time-zones-not-yet.md)). No form state and no submit handling either.
- One field per `FormField`; a group of fields that share one label is a fieldset of your own.

What umriss deliberately does not build, and why: [ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/).
