# Form Reference ## File Location `apps/dashboard/modules//-form.tsx` ## Complete Template ```tsx "use client" import { AlertTriangle, Plus, Save } from "lucide-react" import { Button } from "@/shared/components/ui/button" import { Alert, AlertTitle } from "@/shared/components/ui/alert" import { FieldGroup } from "@/shared/components/ui/field" import { Rhform, RhfTextField, RhfSelectField, RhfAsyncSelectField, // RhfTextareaField, // RhfCheckboxField, // RhfAsyncMultiSelectField, } from "@/shared/components/form" import { toast } from "sonner" import { useAuthApi } from "@/shared/useApi" import { useResourceForm } from "@/shared/hooks/use-resource-form" import { useFormMutation } from "@/shared/hooks/use-form-mutation" import { toRelation, toId } from "@/shared/lib/utils" import { FormSchema, type FormValues, } from "./.schema" import { _ROUTES } from "@repo/api" // ── Constants ── // Static select options (if needed): // const STATUS_OPTIONS = [ // { value: "active", label: "Active" }, // { value: "inactive", label: "Inactive" }, // ] // ── Props ── export type FormProps = { resourceId?: string | null initialData?: unknown onSuccess?: () => void } // ── Default values ── const DEFAULT_VALUES: FormValues = { // Match every field in the Zod schema: // name: "", // email: "", // category: null, // relation fields default to null // is_active: true, // booleans } // ── Mapping helpers ── function mapToFormValues(data: unknown): FormValues { const d = (data as any)?.data ?? data ?? {} return { // String fields: // name: d.name || "", // email: d.email || "", // Relation fields (API returns id + name separately): // category: toRelation(d.category_id, d.category_name), // Booleans: // is_active: d.is_active ?? true, } } function mapFormToPayload(values: FormValues) { return { // String fields — use `|| undefined` to send null for empty strings: // name: values.name, // email: values.email || undefined, // Relation fields — extract the numeric ID: // category_id: toId(values.category), // Booleans: // is_active: values.is_active, } } // ── Shared mapOption for async selects ── const mapLookupOption = (item: any) => ({ value: String(item.id), label: item.name, }) const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } // ── Component ── export function Form({ resourceId, initialData, onSuccess }: FormProps) { const api = useAuthApi() const { form, isEditing } = useResourceForm<FormValues, any>({ schema: FormSchema, defaultValues: DEFAULT_VALUES, resourceId, initialData, initialize: (id) => api..show(id), queryKey: [_ROUTES.BY_ID, resourceId], mapToFormValues: mapToFormValues, }) const { mutate, error, isPending } = useFormMutation(form, { mutationFn: (values: FormValues) => { const payload = mapFormToPayload(values) const promise = isEditing && resourceId ? api..update(resourceId, payload) : api..create(payload) toast.promise(promise, { loading: isEditing ? "Updating ..." : "Creating ...", success: isEditing ? " updated successfully" : " created successfully", error: isEditing ? "Failed to update " : "Failed to create ", }) return promise }, onSuccess: () => { form.reset() onSuccess?.() }, }) return ( mutate(values)}> {error && ( {isEditing ? "Failed to update " : "Failed to create "} {error.message} )} {/* Text fields */} {/* */} {/* Grid layout for side-by-side fields */} {/*
*/} {/* Static select */} {/* */} {/* Async select (fetches options from API) */} {/* api.categories.list()} mapOption={mapLookupOption} {...STORE_OBJECT} /> */} {/* Textarea */} {/* */} {/* Checkbox */} {/* */}
) } ``` ## Key Patterns ### mapToFormValues Transforms API response → form values. Always handle: - Null safety: `d.field || ""` - Relation fields: `toRelation(d.relation_id, d.relation_name)` - Nested data: `(data as any)?.data ?? data ?? {}` - Booleans: `d.field ?? defaultValue` ### mapFormToPayload Transforms form values → API request body. Always handle: - Empty strings to undefined: `values.field || undefined` - Relation to ID: `toId(values.relation)` - Keep required fields as-is: `values.name` ### useResourceForm Initializes react-hook-form with Zod validation. Handles both create and edit modes: - `resourceId` null → create mode (uses `defaultValues`) - `resourceId` set → edit mode (fetches via `initialize`, maps with `mapToFormValues`) ### useFormMutation Wraps `useMutation` with automatic Laravel validation error mapping to form fields. No need to manually handle `ApiError.validationErrors`. ### Toast Pattern Always use `toast.promise()` wrapping the API call for consistent loading/success/error feedback. ## Layout Conventions - Use `` to wrap all fields - Use `
` for side-by-side fields - Place the submit button at the bottom inside `` - Show error alert above fields when mutation fails