S

Component System

How @saastro/forms discovers and injects UI components — zero-config, providers, auto-discovery, and utilities.

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

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 @deprecated in 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:

  1. Legacy mode — If ComponentProvider exists in the tree, use its full registry
  2. Zero-config mode — If <Form components={...}> or FormComponentsProvider exists, use that partial registry
  3. 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:

CategoryComponents
InputsInput, Textarea, Button, Label
Checkbox & SwitchCheckbox, Switch
RadioRadioGroup, RadioGroupItem
SelectSelect, SelectTrigger, SelectContent, SelectItem, SelectValue
Native SelectNativeSelect
SliderSlider
PopoverPopover, PopoverTrigger, PopoverContent
TooltipTooltip, TooltipTrigger, TooltipContent, TooltipProvider
SeparatorSeparator
DialogDialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription
CommandCommand, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem
Input OTPInputOTP, InputOTPGroup, InputOTPSlot
AccordionAccordion, AccordionItem, AccordionTrigger, AccordionContent
CalendarCalendar
FormFormField, FormControl
FieldField, 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

HookPurpose
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 });