# Form Component

URL: https://docs.forms.saastro.io/docs/form-component
> API reference for the <Form /> component — props, the onSubmit caveat, and the FormRef imperative handle.

# `<Form />` Component

The main entry point of `@saastro/forms`. It takes a `FormConfig`, builds the Zod schema, wires up React Hook Form, renders the current step's fields through your injected UI components, and runs the full submit pipeline.

```tsx

<Form
  config={config}
  components={import.meta.glob('@/components/ui/*.tsx', { eager: true })}
/>;
```

---

## Props

`<Form>` accepts exactly five props (`FormProps`):

| Prop         | Type                                                         | Default     | Description                                                                                                                  |
| ------------ | ------------------------------------------------------------ | ----------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `config`     | `FormConfig`                                                 | — required  | The full form configuration: fields, steps, layout, buttons, submit actions, plugins. See [Types Reference](/docs/types)      |
| `components` | `ComponentOverrides \| GlobModules`                          | `undefined` | UI components for the form — a component object or a raw `import.meta.glob(..., { eager: true })` result (auto-detected)       |
| `onSubmit`   | `(values: Record<string, unknown>) => void \| Promise<void>` | `undefined` | **Only fires on native form submission** (e.g. pressing Enter) — not when the rendered submit button is clicked. See below    |
| `onError`    | `(error: Error) => void`                                     | `undefined` | Native-path only, like `onSubmit` — and submit-pipeline failures never reach it (they're caught internally). See below        |
| `className`  | `string`                                                     | `undefined` | CSS class for the `<form>` element. When omitted, the form falls back to `w-full`                                             |

### `config`

The only required prop. Build it with [`FormBuilder`](/docs/formbuilder) or write the object by hand — the shape is documented in the [Types Reference](/docs/types). Note that the identifier property is `formId` (not `id`), `steps` is a `Record<string, Step>` keyed by step id, and column count lives under `layout`.

Most behavior callbacks live on the config, not on the component: `config.onSuccess`, `config.onError`, `config.onStepChange`, `config.successMessage`, `config.redirect`, and so on.

### `components`

Accepts either form:

```tsx
// 1. Glob modules (Vite) — auto-discovers every component in the folder.
//    `eager: true` is required.
<Form config={config} components={import.meta.glob('@/components/ui/*.tsx', { eager: true })} />

// 2. Explicit component object — provide only what your form needs
<Form config={config} components={{ Input, Button, Field, FieldLabel, FieldDescription, FieldError, FormField, FormControl }} />
```

If you render `<Form>` with **no** `components` prop and **no** surrounding provider, the form is not rendered — you get a "Missing UI Components" error panel with setup instructions instead. A partial registry renders the form, and any individually missing components show a per-field fallback with install instructions.

> **Provider precedence:** inside a legacy `ComponentProvider`, the provider's registry wins and the `components` prop is effectively ignored. Use one mechanism or the other. See [Component System](/docs/components) for all three injection modes.

### `onSubmit` and `onError` — read this before using them

The submit button that `<Form>` renders is deliberately `type="button"`: clicking it goes through the library's internal pipeline (confirmation UX → per-step validation → submit actions → `config.onSuccess`), **bypassing native form submission entirely**. The `onSubmit` prop is attached to the native `<form onSubmit>` handler, so:

- `onSubmit` fires only when the form is submitted natively — for example, pressing **Enter** in a field — and only on the last step, *after* the internal submit pipeline finishes. Note it fires even if the pipeline **failed**: pipeline errors are caught internally (they set the error UI and call `config.onError`), so the native handler never sees them.
- `onError` fires only on that same native path, and — because pipeline errors are swallowed internally — in practice only when your own `onSubmit` callback throws.
- Clicking the rendered submit button never calls either prop.

For callbacks that fire on **every** successful or failed submission, use the config instead:

```tsx
const config = FormBuilder.create('contact')
  // ...fields and steps...
  .onSuccess((values) => console.log('submitted', values))
  .onError((error, values) => console.error(error))
  .build();
```

See [Submitting Forms](/docs/submit) for the full pipeline.

### `className`

Applied to the underlying `<form>` element. Defaults to `w-full` when omitted. For styling fields and the grid, see [Styling](/docs/styling) and [Layout System](/docs/layout).

---

## Imperative Handle: `FormRef`

`<Form>` is a `forwardRef` component. Attach a ref to control the form from outside — prefill values, reset it, read the current values, or trigger validation:

```tsx

interface FormRef {
  setValue: UseFormReturn['setValue']; // setValue(name, value, options?)
  reset: UseFormReturn['reset']; // reset(values?, options?) — raw RHF reset
  getValues: UseFormReturn['getValues']; // getValues() / getValues(name)
  trigger: UseFormReturn['trigger']; // trigger(name?) => Promise<boolean>
  resetForm: (options?: ResetFormOptions) => void; // granular reset (see below)
  getDirtyValues: () => Record<string, unknown>; // user-edited values only
  getDirtyPaths: () => string[]; // dirty leaf dot-paths
  isDirty: () => boolean;
}
```

| Method      | Signature                            | Description                                                                              |
| ----------- | ------------------------------------ | ---------------------------------------------------------------------------------------- |
| `setValue`  | `(name, value, options?) => void`    | Set a field's value programmatically                                                      |
| `reset`     | `(values?, options?) => void`        | Raw RHF reset — note it **cannot** dismiss the success screen or clear the submit error   |
| `getValues` | `(name?) => values`                  | Read current values — all of them, or a single field by name                              |
| `trigger`   | `(name?) => Promise<boolean>`        | Run validation for one field, several, or the whole form; resolves `true` when valid      |
| `resetForm` | `(options?) => void`                 | Granular reset: RHF `reset` **plus** clearing the package's submit state (success/error)  |
| `getDirtyValues` | `() => Record<string, unknown>` | Values filtered to user-edited top-level fields (machine writes excluded)                |
| `getDirtyPaths`  | `() => string[]`                | Dirty leaf dot-paths, e.g. `['email', 'contacts.0.name']`                                |
| `isDirty`   | `() => boolean`                      | Whether any field is user-dirty                                                           |

### Granular reset: `resetForm(options?)`

`resetForm` composes RHF's reset with the package's bespoke submit state. Each option maps to RHF's `KeepStateOptions` — plus one package-specific flag:

| Option              | Maps to                       | Effect                                                             |
| ------------------- | ----------------------------- | ------------------------------------------------------------------ |
| `values`            | `reset(values)`               | New values (and new defaults, unless `keepDefaultValues`)          |
| `keepValues`        | RHF `keepValues`              | Keep current values, reset the rest of the state                   |
| `keepErrors`        | RHF `keepErrors`              | Keep field validation errors                                       |
| `keepTouched`       | RHF `keepTouched`             | Keep touched flags                                                 |
| `keepDirty`         | RHF `keepDirty`               | Keep dirty flags                                                   |
| `keepDefaultValues` | RHF `keepDefaultValues`       | New `values` don't become the new defaults                         |
| `keepIsSubmitted`   | RHF `keepIsSubmitted`         | Keep RHF's `isSubmitted` flag                                      |
| `keepSubmitCount`   | RHF `keepSubmitCount`         | Keep RHF's `submitCount`                                           |
| `keepSubmitState`   | *(package)*                   | Don't clear the success screen / submit error / actions result     |

Common recipes: `resetForm()` — full reset back to a pristine form view after a successful submit; `resetForm({ keepValues: true })` — clear errors/flags but keep what the user typed; `resetForm({ values })` — programmatic prefill.

### Example

```tsx

const config = FormBuilder.create('newsletter')
  .addField('email', (f) => f.type('email').label('Email').required().email())
  .addStep('main', ['email'])
  .build();

  const formRef = useRef<FormRef>(null);

  return (
    <div>
      <button type="button" onClick={() => formRef.current?.setValue('email', 'jane@example.com')}>
        Prefill
      </button>
      <button type="button" onClick={() => formRef.current?.reset()}>
        Clear
      </button>
      <button type="button" onClick={() => console.log(formRef.current?.getValues())}>
        Log values
      </button>
      <button
        type="button"
        onClick={async () => {
          const valid = await formRef.current?.trigger();
          console.log(valid ? 'Valid' : 'Has errors');
        }}
      >
        Validate
      </button>

      <Form
        ref={formRef}
        config={config}
        components={import.meta.glob('@/components/ui/*.tsx', { eager: true })}
      />
    </div>
  );
}
```

> `trigger()` validates and shows error messages, but it does **not** submit. There is no imperative submit method — submission goes through the rendered submit button. (To fire individual submit actions imperatively — outside the full pipeline — see [`manual` submit action triggers](/docs/use-submit-action-triggers).)

---

## Success and error UI

After a successful submission, the entire form is replaced by a success panel. The default message is `'✅ Thank you!'` — one of the library's built-in English defaults (along with button labels like `Submit`). Override it per form with `config.successMessage`, per locale via [Internationalization](/docs/i18n), or globally with `setDefaultMessages`. When a submission fails, an error panel renders below the form with `config.errorMessage` (or its default).

String success/error messages are rendered as HTML, so `successMessage: 'Done! <a href="/next">Continue</a>'` works. The HTML is **sanitized** (same for HTML field labels): `<script>`/`<iframe>`/`<svg>`-class elements, `on*` handlers, and `javascript:` URLs are stripped — formatting tags and safe links pass through. The `html` field type is the unsanitized escape hatch for trusted markup.

Hardened apps can replace the built-in sanitizer globally:

```ts

setHtmlSanitizer((html) => DOMPurify.sanitize(html));
```

---

## Related

- [Quickstart](/docs/quickstart) — Build and render your first form
- [Component System](/docs/components) — All three component-injection modes and auto-discovery details
- [Submitting Forms](/docs/submit) — What happens when the user clicks submit
- [Types Reference](/docs/types) — `FormConfig` and every other exported type
- [HubForm](/docs/hub-form) — A wrapper around `<Form>` that fetches its config from the hosted submit service
