---
name: forms-validation
description: >
  Build forms with useLayoutForm hook (primary) and withLayoutForm for
  composable sub-forms. 13 pre-registered MUI field components via
  form.AppField: TextField, NumberField, Autocomplete, Checkbox, Switch,
  RadioGroup, Slider, DatePicker, DateRangePicker, TimePicker,
  TimeRangePicker, DateTimePicker, DateTimeRangePicker. SubmitButton in
  form.AppForm. Zod onChange validators. FormOmittedProps type. Activate
  when creating or modifying forms with validation.
type: core
library: wcz-layout
library_version: "7.6.1"
sources:
  - "wcz-layout:src/hooks/FormHooks.ts"
  - "wcz-layout:src/components/form/"
  - "wcz-layout:src/lib/utils.ts"
references:
  - "references/field-components.md"
---

# Forms & Validation

## Setup

Import the form hook and Zod schema:

```typescript
import { useLayoutForm } from "wcz-layout/hooks";
import { TodoSchema } from "~/schemas/todo";
```

## Core Patterns

### Basic form with useLayoutForm

```typescript
import { useLayoutForm } from "wcz-layout/hooks";
import { TodoSchema } from "~/schemas/todo";
import type { Todo } from "~/schemas/todo";
import { todoCollection } from "~/lib/db/collections/todoCollection";
import { uuidv7 } from "uuidv7";

function TodoForm() {
  const form = useLayoutForm({
    defaultValues: {
      id: uuidv7(),
      name: "",
      description: "",
      isCompleted: false,
    } as Todo,
    validators: {
      onChange: TodoSchema,
    },
    onSubmit: ({ value }) => {
      todoCollection.insert(value);
    },
  });

  return (
    <form.AppForm>
      <form.AppField
        name="name"
        children={(field) => <field.TextField label="Name" required />}
      />
      <form.AppField
        name="description"
        children={(field) => <field.TextField label="Description" multiline rows={3} />}
      />
      <form.AppField
        name="isCompleted"
        children={(field) => <field.Checkbox label="Completed" />}
      />
      <form.SubmitButton />
    </form.AppForm>
  );
}
```

### Available field components

All 13 field components are accessed through `field.*` inside `form.AppField`:

| Component                   | MUI base                    | Use case                      |
| --------------------------- | --------------------------- | ----------------------------- |
| `field.TextField`           | TextField                   | Text input, multiline         |
| `field.NumberField`         | TextField (number)          | Numeric input                 |
| `field.Autocomplete`        | Autocomplete                | Search/select with options    |
| `field.Checkbox`            | FormControlLabel + Checkbox | Boolean toggle                |
| `field.Switch`              | FormControlLabel + Switch   | Boolean toggle (switch)       |
| `field.RadioGroup`          | RadioGroup                  | Single selection from options |
| `field.Slider`              | Slider                      | Range/value slider            |
| `field.DatePicker`          | DatePicker                  | Date only                     |
| `field.DateRangePicker`     | DateRangePicker             | Date range                    |
| `field.TimePicker`          | TimePicker                  | Time only                     |
| `field.TimeRangePicker`     | TimeRangePicker             | Time range                    |
| `field.DateTimePicker`      | DateTimePicker              | Date + time                   |
| `field.DateTimeRangePicker` | DateTimeRangePicker         | Date + time range             |

See references/field-components.md for detailed prop surfaces.

### Edit form with existing data

```typescript
function TodoEditForm({ todo }: { todo: Todo }) {
  const form = useLayoutForm({
    defaultValues: todo,
    validators: {
      onChange: TodoSchema,
    },
    onSubmit: ({ value }) => {
      todoCollection.update(value);
    },
  });

  return (
    <form.AppForm>
      <form.AppField
        name="name"
        children={(field) => <field.TextField label="Name" required />}
      />
      <form.SubmitButton />
    </form.AppForm>
  );
}
```

### Sub-form composition with withLayoutForm

Use `withLayoutForm` when splitting a large form into reusable sub-form components:

```typescript
import { withLayoutForm } from "wcz-layout/hooks";
import { TodoSchema } from "~/schemas/todo";

const TodoDetailsSubForm = withLayoutForm({
  defaultValues: { name: "", description: "" },
  render: ({ form }) => (
    <>
      <form.AppField
        name="name"
        children={(field) => <field.TextField label="Name" required />}
      />
      <form.AppField
        name="description"
        children={(field) => <field.TextField label="Description" multiline />}
      />
    </>
  ),
});
```

### Autocomplete with API data

```typescript
<form.AppField
  name="assigneeId"
  children={(field) => (
    <field.Autocomplete
      label="Assignee"
      options={users}
      getOptionLabel={(user) => user.displayName}
      isOptionEqualToValue={(option, value) => option.id === value.id}
    />
  )}
/>
```

### Date pickers

```typescript
<form.AppField
  name="dueDate"
  children={(field) => <field.DatePicker label="Due Date" />}
/>

<form.AppField
  name="dateRange"
  children={(field) => (
    <field.DateRangePicker
      localeText={{ start: "Start Date", end: "End Date" }}
    />
  )}
/>
```

## Common Mistakes

### CRITICAL Using raw MUI TextField instead of form.AppField

Wrong:

```typescript
<TextField
  name="title"
  value={title}
  onChange={(e) => setTitle(e.target.value)}
/>
```

Correct:

```typescript
<form.AppField
  name="title"
  children={(field) => <field.TextField label="Title" />}
/>
```

`useLayoutForm` pre-registers all field components. Using raw MUI inputs bypasses TanStack Form state management, validation, and error display.

Source: wcz-layout:src/hooks/FormHooks.ts

### HIGH Passing name/value/onChange to AppField components

Wrong:

```typescript
<form.AppField
  name="title"
  children={(field) => (
    <field.TextField
      label="Title"
      name="title"
      value={field.state.value}
      onChange={(e) => field.handleChange(e.target.value)}
    />
  )}
/>
```

Correct:

```typescript
<form.AppField
  name="title"
  children={(field) => <field.TextField label="Title" />}
/>
```

`FormOmittedProps` explicitly strips `name`, `value`, `onChange`, `onBlur`, `error`, `helperText`, `renderInput`, `type`, and `aria-label`. These are managed internally by `useFieldContext`. Passing them causes conflicts or is silently ignored.

Source: wcz-layout:src/lib/utils.ts

### HIGH Not wrapping SubmitButton in form.AppForm

Wrong:

```typescript
<div>
  <form.AppField name="name" children={(field) => <field.TextField label="Name" />} />
  <form.SubmitButton /> {/* Outside AppForm — crashes */}
</div>
```

Correct:

```typescript
<form.AppForm>
  <form.AppField name="name" children={(field) => <field.TextField label="Name" />} />
  <form.SubmitButton />
</form.AppForm>
```

`SubmitButton` uses `useFormContext()` to read `canSubmit` and `isSubmitting` state. It must be rendered inside `form.AppForm`.

Source: wcz-layout:src/components/form/FormSubmitButton.tsx

### HIGH Using useMemo or useCallback in form components

Wrong:

```typescript
const handleSubmit = useCallback(() => form.handleSubmit(), [form]);
```

Correct:

```typescript
const handleSubmit = () => form.handleSubmit();
```

React Compiler handles memoization. Manual `useMemo` / `useCallback` is forbidden per project conventions.

Source: copilot-instructions.md

Cross-skill: See also skills/ui-pages/SKILL.md § Common Mistakes

### MEDIUM FormRadioGroup numeric values become strings

Wrong:

```typescript
<field.RadioGroup
  options={[
    { label: "Low", value: 1 },
    { label: "High", value: 2 },
  ]}
/>
// field.state.value is "1" not 1
```

Correct:

```typescript
<field.RadioGroup
  options={[
    { label: "Low", value: "1" },
    { label: "High", value: "2" },
  ]}
/>
// Use string values consistently, or convert in onSubmit
```

Radio group `onChange` always returns `event.target.value` as a string. Numeric option values round-trip as strings unless explicitly converted.

Source: wcz-layout:src/components/form/FormRadioGroup.tsx

### HIGH Tension: Type safety vs. rapid prototyping

Quick forms using `useState` + manual validation bypass the enforced pattern. Always use `useLayoutForm` + Zod schema derived from Drizzle — even for simple forms. The boilerplate pays off in type safety and consistency.

See also: skills/database-schema/SKILL.md § Common Mistakes

---

See also:

- skills/database-schema/SKILL.md — Zod schemas used as form validators.
- skills/dialogs-notifications/SKILL.md — Form submissions typically show notifications.
- skills/ui-pages/SKILL.md — Create/edit pages embed forms.
