Component System
@saastro/forms is UI-agnostic. It doesn’t ship any visual components — you provide them. This page explains the three ways to inject components, how auto-discovery works, and utilities for detecting missing components.
Three Modes
1. Zero-Config (Recommended)
Pass components directly to the <Form> component. Only provide the components your form actually needs.
import { Form } from '@saastro/forms';
import { Input, Button, Label, Checkbox } 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,
Checkbox,
Field,
FieldLabel,
FieldDescription,
FieldError,
FormField,
FormControl,
}}
onSubmit={(values) => console.log(values)}
/>;
This is the simplest approach. No setup, no context providers. If a component is missing, you’ll see a helpful warning with install instructions.
2. Glob Auto-Discovery (Vite projects)
Use FormComponentsProvider with Vite’s import.meta.glob to automatically discover all your shadcn components. Set it up once in your root layout:
import { FormComponentsProvider } from '@saastro/forms';
export default function RootLayout({ children }) {
return (
<FormComponentsProvider components={import.meta.glob('@/components/ui/*.tsx', { eager: true })}>
{children}
</FormComponentsProvider>
);
}
Then use <Form> anywhere without passing components:
<Form config={config} onSubmit={handleSubmit} />
The provider extracts all named exports from your components/ui/ directory and builds a registry automatically. File names are converted to PascalCase (radio-group.tsx becomes RadioGroup).
3. Legacy Provider
For apps with many forms, wrap your tree with ComponentProvider and a full registry:
import { ComponentProvider, createComponentRegistry } from '@saastro/forms';
import * as ui from '@/lib/form-components';
const registry = createComponentRegistry(ui);
<ComponentProvider components={registry}>
<App />
</ComponentProvider>;
createComponentRegistry() accepts an object with all component exports and returns a typed ComponentRegistry.
The legacy provider is marked
@deprecatedin favor of zero-config. It still works and will continue to work.
How Resolution Works
When a field renders, the system calls useComponents() to get the registry. Resolution order:
- Legacy mode — If
ComponentProviderexists in the tree, use its full registry - Zero-config mode — If
<Form components={...}>orFormComponentsProviderexists, use that partial registry - Error — If neither exists, throw with a helpful message showing both setup options
The zero-config and legacy providers can coexist. Legacy takes precedence for backward compatibility.
ComponentRegistry
The full registry has 49 component slots organized by category:
| Category | Components |
|---|---|
| Inputs | Input, Textarea, Button, Label |
| Checkbox & Switch | Checkbox, Switch |
| Radio | RadioGroup, RadioGroupItem |
| Select | Select, SelectTrigger, SelectContent, SelectItem, SelectValue |
| Native Select | NativeSelect |
| Slider | Slider |
| Popover | Popover, PopoverTrigger, PopoverContent |
| Tooltip | Tooltip, TooltipTrigger, TooltipContent, TooltipProvider |
| Separator | Separator |
| Dialog | Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription |
| Command | Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem |
| Input OTP | InputOTP, InputOTPGroup, InputOTPSlot |
| Accordion | Accordion, AccordionItem, AccordionTrigger, AccordionContent |
| Calendar | Calendar |
| Form | FormField, FormControl |
| Field | Field, FieldLabel, FieldDescription, FieldError |
You don’t need all 49. Only provide the components your form’s field types require. Use getRequiredComponents() to find out exactly which ones you need.
Missing Component Handling
When a field tries to render but a required component isn’t in the registry, it shows a MissingComponentFallback — a styled warning with:
- The field name and type
- The list of missing components
- A
npx shadcn@latest add ...command to install them
import { MissingComponentFallback } from '@saastro/forms';
// Or create placeholder components:
import { createMissingComponentPlaceholder } from '@saastro/forms';
const PlaceholderInput = createMissingComponentPlaceholder('Input');
Hooks
| Hook | Purpose |
|---|---|
useComponents() | Get the full registry (throws if no provider) |
usePartialComponents() | Get a partial registry (returns {} if no provider) |
useHasComponentProvider() | Check if any provider exists |
useComponentMode() | Returns 'legacy', 'zero-config', or 'none' |
Utility Functions
mergeComponentRegistries()
Merge multiple partial registries. Later registries override earlier ones.
import { mergeComponentRegistries } from '@saastro/forms';
const merged = mergeComponentRegistries(baseRegistry, overrides);
parseGlobModules()
Convert Vite’s import.meta.glob result into a component registry. Used internally by FormComponentsProvider.
import { parseGlobModules } from '@saastro/forms';
const modules = import.meta.glob('@/components/ui/*.tsx', { eager: true });
const registry = parseGlobModules(modules);
ComponentResolver
Singleton for async component resolution with caching. Useful for advanced setups with dynamic imports.
import { getComponentResolver, configureComponents } from '@saastro/forms';
const resolver = getComponentResolver({ uiPath: '@/components/ui' });
const button = await resolver.resolve('Button');
// Or pre-cache components synchronously:
configureComponents({ Button, Input, Label });
Related
- Utilities —
getRequiredComponents(),getMissingComponents(),getInstallCommand() - Types Reference — Full
ComponentRegistryinterface - Installation — Initial setup