UNPKG

wcz-layout

Version:

332 lines (263 loc) 8.84 kB
--- 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.