|
| 1 | +--- |
| 2 | +title: Form |
| 3 | +category: Component Patterns |
| 4 | +layer: components/ |
| 5 | +composedWith: use-case-hook |
| 6 | +--- |
| 7 | + |
| 8 | +## Form |
| 9 | + |
| 10 | +Context-driven form built on the project's `useForm` / `FormProvider` abstraction over react-hook-form. Use for any user input that submits data — creation forms, edit forms, settings panels, inline editors. Skip for single uncontrolled inputs (search bars, filters) that don't need validation or submit handling. |
| 11 | + |
| 12 | +### Constraints |
| 13 | + |
| 14 | +- ALWAYS add `noValidate` to the `<form>` element — without it, native browser validation fires before react-hook-form, blocking error messages |
| 15 | +- ALWAYS type the form with an explicit `interface` for field values — pass it as the generic to `useForm<MyValues>()` |
| 16 | +- Use `FormProvider` at the form root. Field components (`TextInput`, `SelectInput`, `NumberInput`, `MoneyInput`, `TextareaInput`) read `control` and `configuration` from context — no prop drilling |
| 17 | +- Use the `register` prop on field components for custom validation rules beyond `isRequired` |
| 18 | +- Wire `onSubmit` through `form.handleSubmit(handler)` — never call the handler directly |
| 19 | +- Set `configuration` in `useForm()` for form-wide visual settings (`size`, `variant`, `autoValidation`). Use `setConfiguration` only for runtime changes |
| 20 | + |
| 21 | +### Conditional Fields |
| 22 | + |
| 23 | +- MUST define conditional field components at **module scope** — never inside the parent component. Inline definitions cause React to remount the field on every parent re-render, destroying field state |
| 24 | +- Use `useFieldBasedCondition` to toggle visibility based on another field's value. It handles value caching (when `keepHiddenFieldValue: true`) and cleanup automatically |
| 25 | +- Use `useCondition` (lower-level) when you already have a derived boolean from outside the form |
| 26 | + |
| 27 | +### Available Field Components |
| 28 | + |
| 29 | +All fields accept: `name` (required), `label`, `isRequired`, `isDisabled`, `register`. They auto-display validation errors. |
| 30 | + |
| 31 | +| Component | Stored type | Notes | |
| 32 | +| --------------- | ------------------ | -------------------------------------------------------------------------- | |
| 33 | +| `TextInput` | `string` | Supports `type` (`email`, `password`, etc.) | |
| 34 | +| `SelectInput` | `Value \| Value[]` | `isMulti` for multi-select, trigger is `<button role="combobox">` in tests | |
| 35 | +| `NumberInput` | `number \| null` | Chakra stepper + input | |
| 36 | +| `MoneyInput` | `number \| null` | Left addon with `symbol` prop (`$`, `€`) | |
| 37 | +| `TextareaInput` | `string` | Same as TextInput minus `type` and `autofocus` | |
| 38 | + |
| 39 | +### Example |
| 40 | + |
| 41 | +```tsx |
| 42 | +// ✅ Correct — conditional field at module scope, noValidate, typed values |
| 43 | +interface ProductValues { |
| 44 | + name: string; |
| 45 | + price: number; |
| 46 | + hasCoupon: string; |
| 47 | + couponCode: string; |
| 48 | +} |
| 49 | + |
| 50 | +const CouponField = () => { |
| 51 | + const isVisible = useFieldBasedCondition<ProductValues>("couponCode", { |
| 52 | + name: "hasCoupon", |
| 53 | + condition: (val) => (val as unknown as string) === "yes", |
| 54 | + defaultValue: "", |
| 55 | + }); |
| 56 | + |
| 57 | + if (!isVisible) return null; |
| 58 | + return <TextInput name="couponCode" label="Coupon Code" />; |
| 59 | +}; |
| 60 | + |
| 61 | +const CreateProductForm = ({ |
| 62 | + onSubmit, |
| 63 | +}: { |
| 64 | + onSubmit: (v: ProductValues) => void; |
| 65 | +}) => { |
| 66 | + const form = useForm<ProductValues>({ |
| 67 | + configuration: { autoValidation: true, size: "md" }, |
| 68 | + }); |
| 69 | + |
| 70 | + return ( |
| 71 | + <FormProvider {...form}> |
| 72 | + <form onSubmit={form.handleSubmit(onSubmit)} noValidate> |
| 73 | + <TextInput name="name" label="Product Name" isRequired /> |
| 74 | + <MoneyInput name="price" label="Price" symbol="$" isRequired /> |
| 75 | + <SelectInput |
| 76 | + name="hasCoupon" |
| 77 | + label="Has Coupon?" |
| 78 | + options={[ |
| 79 | + { label: "Yes", value: "yes" }, |
| 80 | + { label: "No", value: "no" }, |
| 81 | + ]} |
| 82 | + /> |
| 83 | + <CouponField /> |
| 84 | + <button type="submit">Create</button> |
| 85 | + </form> |
| 86 | + </FormProvider> |
| 87 | + ); |
| 88 | +}; |
| 89 | +``` |
| 90 | + |
| 91 | +### Anti-Patterns |
| 92 | + |
| 93 | +- **Prop-drilling control/configuration** — passing `control` or field config as props instead of using `FormProvider` context. Breaks the abstraction and creates coupling |
| 94 | +- **Skipping the `register` prop** — adding validation via raw `useController` or `useFormContext` when the field component already accepts `register` for custom rules |
| 95 | +- **Testing SelectInput with `getByLabelText`** — the select trigger is `<button role="combobox">`, use `getByRole("combobox", { name: "Label" })` instead |
| 96 | + |
| 97 | +### References |
| 98 | + |
| 99 | +- `@src/lib/components/Form/` — source for `useForm`, `FormProvider`, all field components |
| 100 | +- `@src/lib/components/Form/Form.mdx` — full API documentation with props tables |
0 commit comments