# Component System

URL: https://docs.forms.saastro.io/docs/components
> 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 bundle a UI kit — you provide the primitives (inputs, buttons, selects, …), typically from [shadcn/ui](https://ui.shadcn.com). 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.

```tsx

<Form
  config={config}
  components={{
    Input,
    Button,
    Label,
    Checkbox,
    Field,
    FieldLabel,
    FieldDescription,
    FieldError,
    FormField,
    FormControl,
  }}
/>;
```

This is the simplest approach. No setup, no context providers. If a component is missing, you'll see a helpful warning with install instructions. (Submission is configured on the form config itself — see the [Submit guide](/docs/submit).)

### 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:

```tsx

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

Then use `<Form>` anywhere without passing components:

```tsx
<Form config={config} />
```

The provider extracts all named exports from your `components/ui/` directory and builds a registry automatically. Named exports are registered under their export name; default exports are registered under the PascalCased file name (`radio-group.tsx` becomes `RadioGroup`).

### 3. Legacy Provider

For apps with many forms, wrap your tree with `ComponentProvider` and a full registry:

```tsx

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:

| 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. Five of them are needed by every form — the package exports them as `coreComponents`:

```ts
// ['Button', 'Field', 'FieldLabel', 'FieldDescription', 'FieldError']
```

Use [`getRequiredComponents()`](/docs/utilities#component-utilities) to find out exactly which components a given form config needs (it always includes the five core ones).

---

## 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

```tsx

// Or create placeholder components (component name + field type):
const PlaceholderInput = createMissingComponentPlaceholder('Input', 'text');
```

---

## Hooks

Four hooks expose the component registry. All are importable from `@saastro/forms` and follow the same resolution order described above (legacy registry first, then zero-config).

### useComponents()

```ts
function useComponents(): ComponentRegistry;
```

Returns the active registry. **Throws** when no provider is found — the error message explains both setup options (zero-config `components` prop vs. `ComponentProvider`). Use it in components rendered inside a `<Form>` (or under a provider), such as custom field renderers:

```tsx

function MyCustomField() {
  const { Input, Button } = useComponents();
  return <Input placeholder="Custom field" />;
}
```

Note that the return type is the full `ComponentRegistry` even in zero-config mode, where only a partial registry was provided — components your form doesn't use may still be missing at runtime.

### usePartialComponents()

```ts
function usePartialComponents(): PartialComponentRegistry;
```

Same resolution as `useComponents()`, but never throws — it returns an empty object `{}` when no provider exists. Every slot is optional, so check before rendering:

```tsx

function OptionalTooltip({ children }) {
  const { Tooltip } = usePartialComponents();
  if (!Tooltip) return children;
  return <Tooltip>{children}</Tooltip>;
}
```

### useHasComponentProvider()

```ts
function useHasComponentProvider(): boolean;
```

Returns `true` if any component provider (legacy or zero-config) is present in the tree. Useful for rendering a setup hint instead of crashing.

### useComponentMode()

```ts
function useComponentMode(): 'legacy' | 'zero-config' | 'none';
```

Reports which injection mode is active: `'legacy'` when a `ComponentProvider` registry is present, `'zero-config'` when components come from `<Form components={...}>` or `FormComponentsProvider`, and `'none'` when there is no provider at all.

---

## Utility Functions

### mergeComponentRegistries()

Merge multiple partial registries. Later registries override earlier ones.

```tsx

const merged = mergeComponentRegistries(baseRegistry, overrides);
```

### parseGlobModules()

Convert Vite's `import.meta.glob` result into a component registry. Used internally by `FormComponentsProvider`.

```tsx

const modules = import.meta.glob('@/components/ui/*.tsx', { eager: true });
const registry = parseGlobModules(modules);
```

### withComponents()

Higher-order component that injects a `components` prop (the full registry from `useComponents()`) into the wrapped component. Like `useComponents()`, it throws when no provider exists.

```tsx

const FieldWithComponents = withComponents(MyField);
// MyField receives `components: ComponentRegistry` as a prop
```

### ComponentResolver

Singleton for async component resolution with caching. Useful for advanced setups with code-split components. It only resolves components you pre-cache or register an importer for — `resolve()` returns `null` for anything else.

```tsx

// Pre-cache components you've already imported:
configureComponents({ Button, Input, Label });

// Or register lazy importers for code splitting:
const resolver = getComponentResolver();
resolver.registerImporter('Calendar', () =>
  import('@/components/ui/calendar').then((m) => m.Calendar),
);

const button = await resolver.resolve('Button'); // ComponentType | null
```

---

## Related

- [Utilities](/docs/utilities) — `getRequiredComponents()`, `getMissingComponents()`, `getInstallCommand()`
- [Types Reference](/docs/types) — Full `ComponentRegistry` interface
- [Installation](/docs/installation) — Initial setup
