<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.
import { Form } from '@saastro/forms';
<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 |
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 or write the object by hand — the shape is documented in the Types Reference. 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:
// 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 thecomponentsprop is effectively ignored. Use one mechanism or the other. See Component System 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:
onSubmitfires 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 callconfig.onError), so the native handler never sees them.onErrorfires only on that same native path, and — because pipeline errors are swallowed internally — in practice only when your ownonSubmitcallback throws.- Clicking the rendered submit button never calls either prop.
For callbacks that fire on every successful or failed submission, use the config instead:
const config = FormBuilder.create('contact')
// ...fields and steps...
.onSuccess((values) => console.log('submitted', values))
.onError((error, values) => console.error(error))
.build();
See Submitting Forms 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 and Layout System.
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. The handle exposes exactly four methods, all passed through from the underlying React Hook Form instance:
import type { UseFormReturn } from 'react-hook-form';
interface FormRef {
setValue: UseFormReturn['setValue']; // setValue(name, value, options?)
reset: UseFormReturn['reset']; // reset(values?, options?)
getValues: UseFormReturn['getValues']; // getValues() / getValues(name)
trigger: UseFormReturn['trigger']; // trigger(name?) => Promise<boolean>
}
| Method | Signature | Description |
|---|---|---|
setValue | (name, value, options?) => void | Set a field’s value programmatically |
reset | (values?, options?) => void | Reset the form to its defaults, or to the values you pass |
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 |
Example
import { useRef } from 'react';
import { Form, FormBuilder, type FormRef } from '@saastro/forms';
const config = FormBuilder.create('newsletter')
.addField('email', (f) => f.type('email').label('Email').required().email())
.addStep('main', ['email'])
.build();
export function Newsletter() {
const formRef = useRef<FormRef>(null);
return (
<div>
<button type="button" onClick={() => formRef.current?.setValue('email', '[email protected]')}>
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 — seemanualsubmit 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, 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:
import DOMPurify from 'dompurify';
import { setHtmlSanitizer } from '@saastro/forms';
setHtmlSanitizer((html) => DOMPurify.sanitize(html));
Related
- Quickstart — Build and render your first form
- Component System — All three component-injection modes and auto-discovery details
- Submitting Forms — What happens when the user clicks submit
- Types Reference —
FormConfigand every other exported type - HubForm — A wrapper around
<Form>that fetches its config from the hosted submit service