Skip to Content
Prism UI
Getting Started

Your First Form

A complete signup form using the CLI-installed components and the real adapters.

This page assumes you've run Project Setup and have text-field, number-field, select-field, and checkbox-field installed.

Build the registry

components/prism/fields/registry.ts
import { defineRegistry } from '@prismrepo/forms';
import { TextField } from './text-field';
import { NumberField } from './number-field';
import { SelectField } from './select-field';
import { CheckboxField } from './checkbox-field';

export const registry = defineRegistry({
  text: TextField,
  number: NumberField,
  select: SelectField,
  checkbox: CheckboxField,
});

This is the same file prismrepo add updates for you automatically as you install more field components.

Define and render the form

SignupForm.tsx
import { z } from 'zod';
import { createFormBuilder } from '@prismrepo/forms';
import { usePrismForm, PrismField } from '@prismrepo/adapter-rhf';
import { createZodResolver } from '@prismrepo/adapter-zod';
import { registry } from '@/components/prism/fields/registry';

const schema = z.object({
  name: z.string().min(1, 'Name is required'),
  email: z.string().min(1, 'Email is required').email('Invalid email address'),
  age: z.number().min(18, 'Must be at least 18'),
  plan: z.enum(['free', 'pro', 'enterprise']),
  acceptTerms: z.boolean().refine((v) => v === true, 'You must accept the terms'),
});

type SignupValues = z.infer<typeof schema>;

const definition = createFormBuilder(registry).defineForm<SignupValues>({
  name: { type: 'text', label: 'Full name' },
  email: { type: 'text', label: 'Email' },
  age: { type: 'number', label: 'Age' },
  plan: { type: 'select', label: 'Plan', options: ['free', 'pro', 'enterprise'] },
  acceptTerms: { type: 'checkbox', label: 'I accept the terms' },
});

export function SignupForm() {
  const form = usePrismForm(definition, {
    defaultValues: {
      name: '',
      email: '',
      age: 18,
      plan: 'free',
      acceptTerms: false,
    },
    resolver: createZodResolver(schema),
  });

  return (
    <form
      onSubmit={form.handleSubmit((values) => {
        // values is fully typed as SignupValues here.
        console.log(values);
      })}
    >
      <PrismField form={form} name="name" />
      <PrismField form={form} name="email" />
      <PrismField form={form} name="age" />
      <PrismField form={form} name="plan" />
      <PrismField form={form} name="acceptTerms" />
      <button type="submit" disabled={form.formState.isSubmitting}>
        Create account
      </button>
    </form>
  );
}

Submitting with an empty name, an invalid email, an age under 18, or an unchecked terms box all block submission and surface their message through the field component's own field.error, there's no separate error channel to wire up.

What's actually happening

  • createFormBuilder(registry).defineForm<SignupValues>(...) checks, at compile time, that every field's type exists in the registry and that its component's declared value type exactly matches the corresponding key of SignupValues. Configuring age against a text-typed component would fail to compile, age is a number.
  • usePrismForm returns the real, complete React Hook Form UseFormReturn<SignupValues>, plus registry and fieldsConfig. Every RHF method, watch, setValue, reset, getValues, and the rest, is still there.
  • createZodResolver(schema) plugs directly into RHF's own options.resolver, the Zod schema's inferred type becomes the form's value type with no hand-written generic.

See Forms for the full field-configuration model, and Custom Fields to register a field component that isn't in Prism's own catalog.

On this page