laif-ds
Version:
Design System di Laif con componenti React basati su principi di Atomic Design
370 lines (305 loc) • 10.7 kB
Markdown
# 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