# 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.
