UNPKG

laif-ds

Version:

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

308 lines (262 loc) 10.2 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 type AppFormItem<TAsyncOption = unknown> = { /** Field label displayed above the component. */ label: string; /** The type of form component to render. */ component: | "input" | "select" | "textarea" | "checkbox" | "multiselect" | "datepicker" | "radio" | "switch" | "slider" | "async" | "async-multiple" | "custom"; /** Field name used for React Hook Form state binding and validation. */ name: string; /** * HTML input type. Only applies when `component` is `"input"`. * @example "text" | "email" | "password" | "number" | "url" | "search" | "tel" */ inputType?: ComponentProps<"input">["type"]; /** Initial value for the field. */ defaultValue?: string | boolean | number | string[] | Date | number[]; /** Options list for `select`, `multiselect`, and `radio` components. */ options?: AppSelectOption[]; /** Disables the field, preventing user interaction. @default false */ disabled?: boolean; /** Placeholder text shown when no value is selected or entered. */ placeholder?: string; /** Helper text displayed below the field. */ caption?: string; /** * Enables range mode on the `datepicker` component when provided. * The two dates define the selectable calendar boundaries. */ calendarRange?: [Date, Date]; /** Minimum value for the `slider` component. @default 0 */ min?: number; /** Maximum value for the `slider` component. @default 100 */ max?: number; /** Step increment for the `slider` component. @default 1 */ step?: number; /** * Async data loader for `async` and `async-multiple` components. * Called with the current search query; must return a promise of options. */ fetcher?: (query?: string) => Promise<TAsyncOption[]>; /** * Custom render function for each option item in the async dropdown. * Required for `async` and `async-multiple` components. */ renderOptionItem?: (option: TAsyncOption) => React.ReactNode; /** * Extracts the unique string value from an async option object. * Required for `async` and `async-multiple` components. */ resolveOptionValue?: (option: TAsyncOption) => string; /** * Custom render function for the selected value chip/label. * Required for `async` and `async-multiple` components. */ renderSelectedValue?: (option: TAsyncOption) => React.ReactNode; /** * Pre-loaded options used to hydrate the async select on mount, * avoiding an initial fetch when the value is already known. */ initialOptions?: TAsyncOption[]; /** Custom node displayed when the async fetcher returns no results at all. */ notFound?: React.ReactNode; /** Message shown when the async search query returns no matching results. */ noResultsMessage?: string; /** Debounce delay in milliseconds for the async search input. @default 300 */ debounce?: number; /** Allows the user to clear the selected value in the async select. @default false */ clearable?: boolean; /** Extra props forwarded directly to the `DatePicker` component. */ datePickerProps?: Partial<DatePickerProps>; /** Column span for the item in the grid. @default undefined (auto, or "full" for the last item) */ colSpan?: "1" | "2" | "3" | "full"; /** Icon on the left side. Only applies when `component` is `"input"`. */ iconLeft?: IconName; /** Icon on the right side. Only applies when `component` is `"input"`. */ iconRight?: IconName; /** Enables search/filter inside `select` and `multiselect` dropdowns. @default true */ searchable?: boolean; /** Hides the label above the field. @default false */ hideLabel?: boolean; /** Additional CSS class name applied to the item container div. */ className?: string; /** * Additional props forwarded to the underlying UI component. * Strongly typed based on the chosen `component` value. */ componentProps?: AppFormItemComponentProps[TComponent]; /** * Custom render function for the component. * Required when `component` is `"custom"`. */ render?: (props: { field: ControllerRenderProps<FieldValues, string>; error?: string; label: React.ReactNode; }) => React.ReactNode; }; ``` ### 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 field configurations | | `form` | `UseFormReturn<any>` | **required** | React Hook Form instance returned by `useForm` | | `cols` | `"1" \| "2" \| "3"` | `"2"` | Number of grid columns | | `submitText` | `string` | `"Invia"` | Text label for the internal submit button | | `onSubmit` | `(data: any) => void` | `undefined` | Callback fired with validated form data on submission | | `isSubmitting` | `boolean` | `false` | Shows loading spinner on the 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. Use `colSpan` on items for fine-grained control. - **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 unless `colSpan` is explicitly set. - **Error Highlighting**: Fields with errors get red border styling --- ## Field Types ### input Standard text input field. Supports `iconLeft` and `iconRight`. When `component: "input"`, use the `inputType` property to control the underlying HTML input type (e.g. `"text"`, `"email"`, `"password"`, `"number"`, `"url"`). ### select / multiselect Single or multiple selection dropdown using `AppSelect`. Supports `searchable: true`. ### custom Allows rendering any arbitrary content within the form grid. Requires the `render` function. ```tsx { label: "Custom Section", component: "custom", name: "customInfo", render: ({ field, error, label }) => ( <div className="p-4 border rounded"> {label} <input {...field} className="border p-2" /> {error && <span className="text-red-500">{error}</span>} </div> ), colSpan: "full" } ``` ### async / async-multiple Use `AsyncSelect` for server-side driven selects. `async` handles a single string value, `async-multiple` an array of strings. Required props for both: `fetcher`, `renderOptionItem`, `resolveOptionValue`, `renderSelectedValue`. ### datepicker Date picker component with optional range selection via `calendarRange`. Pass `datePickerProps` for locale, format, or other `DatePicker` customisation. ### radio / checkbox / switch / slider Standard form components for various input types. --- ## Examples ### Grid and Icons Example ```tsx const items: AppFormItem[] = [ { label: "First Name", component: "input", name: "firstName", colSpan: "1", }, { label: "Last Name", component: "input", name: "lastName", colSpan: "1", }, { label: "Email", component: "input", name: "email", iconLeft: "Mail", colSpan: "full", }, { label: "Department", component: "select", name: "dept", options: [ /* ... */ ], colSpan: "full", }, ]; return <AppForm form={form} items={items} cols="2" showSubmitButton />; ``` ### Advanced componentProps Use `componentProps` to pass specific properties to the underlying UI components that are not exposed directly in `AppFormItem`. ```tsx { label: "Bio", component: "textarea", name: "bio", componentProps: { rows: 10, className: "resize-none" } } ``` --- ## Notes - **React Hook Form Required**: This component requires React Hook Form to be installed and configured - **Grid Layout**: The grid uses Tailwind's grid system with `grid-cols-{n}` classes - **Type Safety**: `componentProps` is strictly typed based on the `component` chosen.