UNPKG

wcz-layout

Version:

105 lines (91 loc) 3.95 kB
--- name: forms description: "Use when building or changing forms, including fields, validation, submission, reset, date values, async initial values, or reusable subforms." metadata: type: convention library: wcz-layout --- # Form patterns > Mechanics (`createFormHook`, `withForm`, field API, validation lifecycle) belong to > TanStack Form v1. `useLayoutForm` is its `useAppForm` and `withLayoutForm` its `withForm`, > pre-bound to the MUI field set below. ## Rules - Always use `useLayoutForm` with the pre-defined components. - Fields are not importable; reach them through render props. `form.AppField` yields `field.Autocomplete`, `field.Checkbox`, `field.DatePicker`, `field.DateTimePicker`, `field.NumberField`, `field.RadioGroup`, `field.Slider`, `field.Switch`, `field.TextField` and `field.TimePicker`. `form.AppForm` yields `form.SubmitButton`, and nothing else. Pick the field that matches the value. - Define `width` for all form fields based on the expected content length. - Reuse Zod schemas from `src/lib/schemas/`, derived from Drizzle tables with `createSelectSchema`. - `field.DatePicker`, `field.TimePicker` and `field.DateTimePicker` hold `Dayjs`, not `Date`. A Drizzle `timestamp` column gives you a `Date`, so convert at both boundaries: `dayjs(row.dueAt)` into `defaultValues`, `.toDate()` on the way out in `onSubmit`. - Use translation for labels, helper text, validation messages and submit. - Reset the form after a successful create, and let the submit handler reject on failure so a failed save keeps what the user typed. - `SubmitButton` calls `form.handleSubmit()` and owns its loading and disabled state. - `reset()` restores the `defaultValues` captured when the form was constructed. If those contain a generated `uuidv7()` id, pass a fresh one to `reset({ ...defaults, id: uuidv7() })` or the second create collides on the primary key. - `defaultValues` only re-syncs into a mounted form while it is still untouched. On edit routes, preload the query in the loader so the row is there before the form mounts. - When fetching data for a form, always use a suspense query. - Split a long form with `withLayoutForm`, which binds a sub-form to the same `defaultValues` shape as the parent. Custom field components read `useFieldContext<T>()`. ## Examples ```tsx // Form component interface FormProps { defaultValues: Feature; onSubmit: (value: Feature) => Promise<void>; } export const Form: FC<FormProps> = ({ defaultValues, onSubmit }) => { const { t } = useTranslation(); const form = useLayoutForm({ defaultValues, validators: { onChange: FeatureSchema }, onSubmit: async ({ value, formApi }) => { await onSubmit(value); formApi.reset(); }, }); return ( <form onSubmit={async (event) => { event.preventDefault(); event.stopPropagation(); await form.handleSubmit(); }} > <form.AppField name="property1"> {(field) => <field.TextField label={t("Feature.Property1")} required sx={{ width: 420 }} />} </form.AppField> <form.AppField name="property2"> {(field) => ( <field.Autocomplete options={options} autoHighlight autoSelect autoComplete loading={isLoading} sx={{ width: 250 }} textFieldProps={{ label: t("Feature.Property2"), required: true }} /> )} </form.AppField> <form.AppField name="property3"> {(field) => ( <field.NumberField label={t("Feature.Property3")} required sx={{ width: 120 }} /> )} </form.AppField> <form.AppField name="property4"> {(field) => <field.Switch label={t("Feature.Property4")} />} </form.AppField> <form.AppForm> <form.SubmitButton variant="contained">{t("Submit")}</form.SubmitButton> </form.AppForm> </form> ); }; ```