UNPKG

laif-ds

Version:

Design System di Laif con componenti React basati su principi di Atomic Design

370 lines (305 loc) 10.7 kB
# AppForm ## Overview Dynamic form component that integrates with React Hook Form to provide a configurable form with multiple field types including input, textarea, select, multiselect, datepicker, radio, checkbox, switch, and slider. Automatically handles validation errors and form state. --- ## Types ### AppFormItem ```ts export interface AppFormItem { label: string; // Field label component: | "input" | "select" | "textarea" | "checkbox" | "multiselect" | "datepicker" | "radio" | "switch" | "slider" | "async" | "async-multiple"; // Field type name: string; // Field name (used for form state and validation) inputType?: string; // HTML input type for "input" component (e.g. "text", "email", "password", "number") defaultValue?: string | boolean | number | string[] | Date | number[]; // Initial value options?: AppSelectOption[]; // Options for select/multiselect/radio disabled?: boolean; // Disables the field placeholder?: string; // Placeholder text caption?: string; // Helper text below the field calendarRange?: [Date, Date]; // Date range for datepicker min?: number; // Minimum value for slider max?: number; // Maximum value for slider step?: number; // Step value for slider fetcher?: (query?: string) => Promise<any[]>; // Async select data loader renderOptionItem?: (option: any) => React.ReactNode; // Customize render of async option resolveOptionValue?: (option: any) => string; // Value extractor for async option renderSelectedValue?: (option: any) => React.ReactNode; // Customize render of selected async option initialOptions?: any[]; // Optional cache for async select hydration notFound?: React.ReactNode; noResultsMessage?: string; debounce?: number; clearable?: boolean; } ``` ### Submit Button Inside vs Outside ```tsx import { AppForm, Button } from "laif-ds"; import { useForm } from "react-hook-form"; export function SubmitInsideVsOutside() { const formInside = useForm({ mode: "onChange" }); const formOutside = useForm({ mode: "onChange" }); const onSubmitInside = (data: any) => console.log("inside", data); const onSubmitOutside = (data: any) => console.log("outside", data); return ( <div className="grid grid-cols-2 gap-6"> <div> {/* Internal submit button rendered by AppForm */} <AppForm form={formInside} items={[{ label: "Name", component: "input", name: "name" }]} onSubmit={onSubmitInside} showSubmitButton /> </div> <div> {/* External submit button managed outside */} <AppForm form={formOutside} items={[{ label: "Name", component: "input", name: "name" }]} onSubmit={onSubmitOutside} /> <div className="mt-4 flex justify-end"> <Button type="button" disabled={ !formOutside.formState.isValid || !formOutside.formState.isDirty } onClick={formOutside.handleSubmit(onSubmitOutside)} > Submit (external) </Button> </div> </div> </div> ); } ``` --- ## Props | Prop | Type | Default | Description | | ------------------ | --------------------- | ------------ | -------------------------------------------------------- | | `items` | `AppFormItem[]` | **required** | Array of form field configurations | | `form` | `UseFormReturn<any>` | **required** | React Hook Form instance | | `cols` | `"1" \| "2" \| "3"` | `"2"` | Number of grid columns | | `submitText` | `string` | `"Invia"` | Text for submit button | | `onSubmit` | `(data: any) => void` | `undefined` | Form submission callback | | `isSubmitting` | `boolean` | `false` | Shows loading state on submit button | | `showSubmitButton` | `boolean` | `false` | Renders an internal submit button at the end of the form | --- ## Behavior - **React Hook Form Integration**: Uses `Controller` from React Hook Form for each field - **Validation Display**: Shows validation errors inline with each field - **Grid Layout**: Automatically arranges fields in a responsive grid based on `cols` prop - **Submit Button**: Rendered only when `showSubmitButton` is `true`. When shown, it is disabled when the form is invalid or pristine. Otherwise, manage submit externally with `form.handleSubmit(...)`. - **Last Field Spanning**: The last field automatically spans full width - **Error Highlighting**: Fields with errors get red border styling --- ## Field Types ### input Standard text input field Standard text input field. When `component: "input"`, you can use the `inputType` property to control the underlying HTML input type (e.g. `"text"`, `"email"`, `"password"`, `"number"`, `"url"`). For a complete showcase of different input types, see the Storybook story `UI/AppForm/DifferentInputTypes`. ### textarea Multi-line text area ### select Single selection dropdown using AppSelect ### multiselect Multiple selection dropdown using AppSelect with `multiple` prop ### async / async-multiple Use `AsyncSelect` for server-side driven selects. `async` gestisce un valore singolo, `async-multiple` un array di stringhe. Richiede i seguenti prop sull'item: - `fetcher`: funzione che restituisce `Promise<Option[]>` - `renderOptionItem`, `resolveOptionValue`, `renderSelectedValue`: per gestire il rendering e la selezione - `initialOptions` (opzionale ma consigliata) per idratare i valori preimpostati - `defaultValue` va impostato nei `defaultValues` di React Hook Form Esempio: ```tsx const items: AppFormItem[] = [ { component: "async", name: "business", label: "Business (async)", placeholder: "Seleziona un business", fetcher: mockAsyncSelectFetcher, initialOptions: asyncSelectMockUsers, renderOptionItem: (user) => ( <div> <strong>{user.name}</strong> <span className="text-muted-foreground text-xs">{user.email}</span> </div> ), resolveOptionValue: (user) => user.id, renderSelectedValue: (user) => user.name, }, { component: "async-multiple", name: "team", label: "Team (async)", placeholder: "Seleziona i membri", fetcher: mockAsyncSelectFetcher, initialOptions: asyncSelectMockUsers, renderOptionItem: (user) => user.name, resolveOptionValue: (user) => user.id, renderSelectedValue: (user) => user.name, }, ]; const form = useForm({ defaultValues: { business: asyncSelectMockUsers[0]?.id ?? "", team: asyncSelectMockUsers.slice(0, 2).map((u) => u.id), }, }); ``` Ricorda che il fetch parte solo quando il menu è aperto; eventuali valori iniziali devono essere già presenti in `initialOptions`. ### datepicker Date picker component with optional range selection via `calendarRange` ### radio Radio button group with options ### checkbox Single checkbox with label ### switch Toggle switch with label and optional caption ### slider Range slider with min/max/step configuration --- ## Examples ### Basic Form ```tsx import { AppForm } from "laif-ds"; import { useForm } from "react-hook-form"; export function BasicForm() { const form = useForm(); return ( <AppForm form={form} items={[ { label: "Name", component: "input", name: "name", placeholder: "Enter your name", }, { label: "Email", component: "input", name: "email", inputType: "email", placeholder: "Enter your email", }, { label: "Message", component: "textarea", name: "message", placeholder: "Enter your message", }, ]} onSubmit={(data) => console.log(data)} /> ); } ``` ### Full Feature Form ```tsx import { AppForm } from "laif-ds"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; const schema = z.object({ name: z.string().min(1, "Name is required"), category: z.string(), isActive: z.boolean(), priority: z.number(), }); export function FullFeatureForm() { const form = useForm({ resolver: zodResolver(schema), defaultValues: { name: "", category: "", isActive: false, priority: 5, }, }); return ( <AppForm form={form} cols="2" items={[ { label: "Name", component: "input", name: "name", placeholder: "Enter name", }, { label: "Category", component: "select", name: "category", options: [ { value: "tech", label: "Technology" }, { value: "design", label: "Design" }, { value: "marketing", label: "Marketing" }, ], }, { label: "Active", component: "switch", name: "isActive", caption: "Enable this option", }, { label: "Priority", component: "slider", name: "priority", min: 1, max: 10, step: 1, }, ]} submitText="Save" onSubmit={(data) => console.log(data)} /> ); } ``` ### With Date Range ```tsx import { AppForm } from "laif-ds"; import { useForm } from "react-hook-form"; export function DateRangeForm() { const form = useForm(); const startDate = new Date(); const endDate = new Date(); endDate.setMonth(endDate.getMonth() + 1); return ( <AppForm form={form} items={[ { label: "Date Range", component: "datepicker", name: "dateRange", calendarRange: [startDate, endDate], }, ]} onSubmit={(data) => console.log(data)} /> ); } ``` --- ## Notes - **React Hook Form Required**: This component requires React Hook Form to be installed and configured - **Validation**: Use React Hook Form's resolver (e.g., Zod, Yup) for validation - **Grid Layout**: The grid uses Tailwind's grid system with `grid-cols-{n}` classes - **Submit Button**: Use `showSubmitButton` to render the internal submit button at the end of the form, or provide your own external submit control. - **Error Display**: Errors appear inline above each field and as border highlighting