@saastro/forms
v0.1.3
Published
Sistema de formularios dinámicos para React + Zod + React Hook Form
Maintainers
Readme
@saastro/forms
Dynamic form system for React with Zod validation, React Hook Form, and a plugin system.
Features
- 30+ field types (text, select, date, slider, OTP, button-card, etc.)
- Zero-config mode: Just pass your shadcn components inline
- Multi-step forms with conditional navigation
- Zod schema validation per field
- Plugin system (localStorage, analytics, autosave)
- Modular submit actions (HTTP, webhook, email)
- Dependency injection for UI components
- Builder pattern API
- Responsive 12-column grid layout
- TypeScript strict mode
Installation
npm install @saastro/forms
# or
bun add @saastro/formsPeer Dependencies
npm install react react-dom react-hook-form zod date-fns react-day-pickershadcn/ui Components
Install only the components you need. For a basic text form:
npx shadcn@latest add input button label field formFor all available field types:
npx shadcn@latest add input button label textarea select checkbox radio-group popover calendar tooltip slider switch separator dialog command input-otp field formTailwind CSS 4
If you're using Tailwind CSS 4, add the following @source directive to your main CSS file so Tailwind can detect the classes used by this package:
@source "node_modules/@saastro/forms/dist/**/*.js";Quick Start (Zero-Config with Auto-Discovery)
The simplest way to use @saastro/forms — auto-discover all your shadcn components with Vite's glob:
import { Form, FormBuilder } from '@saastro/forms';
// Auto-discover all shadcn components at build time
const uiComponents = import.meta.glob('@/components/ui/*.tsx', { eager: true });
const config = FormBuilder.create('contact')
.addField('name', (f) => f.type('text').label('Name').required().minLength(2))
.addField('email', (f) => f.type('email').label('Email').required().email())
.addStep('main', ['name', 'email'])
.buttons({ submit: { type: 'submit', label: 'Send' } })
.build();
export function ContactForm() {
return (
<Form
config={config}
components={uiComponents}
onSubmit={(values) => console.log('Submitted:', values)}
/>
);
}That's it! No FormProvider, no barrel file, no manual component registration. The Form auto-discovers components by parsing the glob result.
Alternative: Explicit Components
If you prefer explicit imports or need to override specific components:
import { Form, FormBuilder } from '@saastro/forms';
import { Input, Button, Label } from '@/components/ui';
import { Field, FieldLabel, FieldDescription, FieldError } from '@/components/ui/field';
import { FormField, FormControl } from '@/components/ui/form';
<Form
config={config}
components={{
Input,
Button,
Label,
Field,
FieldLabel,
FieldDescription,
FieldError,
FormField,
FormControl,
}}
/>;Benefits of Zero-Config
- One line of setup — just
import.meta.globand you're done - Auto-discovers components — no manual registration needed
- Missing component fallback — shows helpful warnings with install commands
- No Provider boilerplate — just render
<Form /> - Override per-form — mix glob with explicit overrides
Quick Start (Provider Mode)
For apps with many forms, you can still use the provider pattern:
import { Form, FormBuilder, ComponentProvider, createComponentRegistry } from '@saastro/forms';
import * as shadcn from '@/lib/form-components'; // Your barrel file
const registry = createComponentRegistry(shadcn);
export function App() {
return (
<ComponentProvider components={registry}>
<ContactForm />
<SignupForm />
<CheckoutForm />
</ComponentProvider>
);
}
function ContactForm() {
const config = FormBuilder.create('contact')
.addField('name', (f) => f.type('text').label('Name').schema(z.string().min(2)))
.addStep('main', ['name'])
.build();
return <Form config={config} />;
}Which Components Do I Need?
Each field type requires certain components. When a component is missing, you'll see a helpful warning with install instructions.
| Field Type | Required Components |
| ---------------------- | ------------------------------------------------------------------------- |
| text, email, tel | Input, Label, Field, FieldLabel, FieldError, FormField, FormControl |
| textarea | Textarea, Label, Field, FieldLabel, FieldError, FormField, FormControl |
| select | Select, SelectTrigger, SelectContent, SelectItem, SelectValue, Field, ... |
| checkbox, switch | Checkbox/Switch, Label, Field, ... |
| date | Calendar, Popover, PopoverTrigger, PopoverContent, Button, Field, ... |
| combobox | Command, CommandInput, CommandList, Popover, Button, Field, ... |
Use getRequiredComponents(config) to programmatically check:
import { getRequiredComponents, getInstallCommand } from '@saastro/forms';
const required = getRequiredComponents(config);
// ['Input', 'Button', 'Label', 'Field', ...]
const installCmd = getInstallCommand(required);
// 'npx shadcn@latest add input button label field form'Multi-Step Form
const config = FormBuilder.create('signup')
.layout('manual')
.columns(2)
.addField('email', (f) => f.type('email').label('Email').schema(z.string().email()))
.addField('password', (f) => f.type('text').label('Password').schema(z.string().min(8)))
.addField('name', (f) => f.type('text').label('Full Name').schema(z.string().min(2)))
.addField('plan', (f) =>
f
.type('button-radio')
.label('Choose Plan')
.schema(z.string())
.options([
{ label: 'Free', value: 'free' },
{ label: 'Pro', value: 'pro' },
]),
)
.addStep('account', ['email', 'password'])
.addStep('profile', ['name', 'plan'])
.initialStep('account')
.buttons({
submit: { type: 'submit', label: 'Create Account' },
next: { type: 'next', label: 'Next' },
back: { type: 'back', label: 'Back' },
})
.build();Plugins
import { FormBuilder, PluginManager, localStoragePlugin, analyticsPlugin } from '@saastro/forms';
const plugins = new PluginManager();
plugins.register(localStoragePlugin({ key: 'my-form' }));
plugins.register(analyticsPlugin());
const config = FormBuilder.create('my-form')
.usePlugins(plugins)
// ... fields and steps
.build();Submit Actions
const config = FormBuilder.create('form')
// ... fields and steps
.build();
// Add submit actions to config
config.submitActions = {
sendToAPI: {
id: 'sendToAPI',
action: {
name: 'Send to API',
type: 'http',
config: {
url: 'https://api.example.com/submit',
method: 'POST',
},
},
trigger: { type: 'onSubmit' },
order: 1,
},
};Conditional Fields
// Hide field based on another field's value
.addField("company", (f) =>
f.type("text")
.label("Company")
.schema(z.string().optional())
.hidden((values) => values.type !== "business")
)
// Responsive visibility
.addField("sidebar", (f) =>
f.type("html")
.hidden({ default: "hidden", lg: "visible" })
)
// Declarative conditions
.addField("discount", (f) =>
f.type("text")
.label("Discount Code")
.schema(z.string().optional())
.hidden({
operator: "and",
conditions: [
{ field: "plan", operator: "equals", value: "free" },
],
})
)Component Override
Override specific components per-form, even when using a provider:
import { MyCustomDatePicker } from './components/MyCustomDatePicker';
<Form
config={config}
components={{
Calendar: MyCustomDatePicker, // Override just the Calendar
}}
/>;Field Types
| Type | Description |
| ---------------------- | ------------------------------------- |
| text, email, tel | Text inputs |
| textarea | Multi-line text |
| select | Dropdown select |
| native-select | Native HTML select |
| combobox | Searchable select (Popover + Command) |
| command | Command palette select |
| checkbox, switch | Boolean toggles |
| radio | Radio group |
| button-radio | Button-style radio |
| button-checkbox | Button-style multi-select |
| button-card | Card-style selection |
| checkbox-group | Multiple checkboxes |
| switch-group | Multiple switches |
| date | Date picker (simple or popover) |
| daterange | Date range picker |
| slider | Range slider |
| otp | OTP input |
| input-group | Input with prefix/suffix |
| html | Raw HTML content |
API Reference
Form Props
interface FormProps {
/** Form configuration */
config: FormConfig;
/** Component overrides (zero-config mode) */
components?: PartialComponentRegistry;
/** Called on successful submit */
onSubmit?: (values: Record<string, unknown>) => void | Promise<void>;
/** Called on submit error */
onError?: (error: Error) => void;
/** CSS class for form element */
className?: string;
}Utility Functions
// Get components needed for a form
getRequiredComponents(config: FormConfig): ComponentName[]
// Get missing components
getMissingComponents(required: ComponentName[], provided: Partial<...>): ComponentName[]
// Generate install command
getInstallCommand(missing: ComponentName[]): stringLicense
MIT
