laif-ds
Version:
Design System di Laif con componenti React basati su principi di Atomic Design
308 lines (262 loc) • 10.2 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 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.