npm package: @liam-michel/validated-form
Description: A type-safe form library built on React Hook Form and Zod
Latest version: 0.2.1

# @liam-michel/validated-form

[![CI](https://github.com/liam-michel/validated-form/actions/workflows/ci.yml/badge.svg)](https://github.com/liam-michel/validated-form/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![npm version](https://img.shields.io/npm/v/@liam-michel/validated-form)](https://www.npmjs.com/package/@liam-michel/validated-form)
[![npm downloads](https://img.shields.io/npm/dm/@liam-michel/validated-form)](https://www.npmjs.com/package/@liam-michel/validated-form)
[![TypeScript](https://img.shields.io/badge/TypeScript-5-blue.svg)](https://www.typescriptlang.org/)

A type-safe, schema-driven form library for React. Define a [Zod](https://zod.dev) schema and get validated, fully-typed form fields out of the box — built on [React Hook Form](https://react-hook-form.com) and [Radix UI](https://www.radix-ui.com).

## Installation

```bash
npm install @liam-michel/validated-form
```

### Peer dependencies

You'll also need these in your project:

```bash
npm install react react-dom react-hook-form zod @hookform/resolvers \
  @radix-ui/react-checkbox @radix-ui/react-select @radix-ui/react-slider \
  @radix-ui/react-popover cmdk react-day-picker date-fns lucide-react \
  clsx tailwind-merge
```

> The library uses Tailwind CSS class names. Make sure your project has Tailwind configured.

## Quick start

```tsx
import {
  ValidatedForm,
  TextField,
  NumberField,
  CheckboxField,
} from '@liam-michel/validated-form';
import { z } from 'zod';

const schema = z.object({
  name: z.string().min(1, 'Name is required'),
  age: z.number().min(1, 'Age is required'),
  agree: z.boolean().refine((v) => v, 'You must agree'),
});

function MyForm() {
  return (
    <ValidatedForm
      schema={schema}
      defaultValues={{ name: '', age: 0, agree: false }}
      onSubmit={async (data) => {
        // data is fully typed as { name: string; age: number; agree: boolean }
        console.log(data);
      }}
    >
      <TextField<typeof schema>
        name="name"
        label="Name"
        placeholder="Enter your name"
      />
      <NumberField<typeof schema> name="age" label="Age" />
      <CheckboxField<typeof schema> name="agree" label="I agree to the terms" />
    </ValidatedForm>
  );
}
```

## `createForm` — schema-bound factory

For a cleaner API without manual generics, use `createForm` to generate a full set of typed components from a single schema:

```tsx
import { createForm } from '@liam-michel/validated-form';
import { z } from 'zod';

const schema = z.object({
  email: z.string().email(),
  bio: z.string(),
  role: z.enum(['admin', 'user', 'viewer']),
});

const { Form, TextField, TextAreaField, SelectField } = createForm(schema);

function ProfileForm() {
  return (
    <Form
      onSubmit={async (data) => console.log(data)}
      defaultValues={{ email: '', bio: '', role: 'user' }}
    >
      <TextField name="email" label="Email" />
      <TextAreaField
        name="bio"
        label="Bio"
        placeholder="Tell us about yourself"
      />
      <SelectField
        name="role"
        label="Role"
        options={[
          { value: 'admin', label: 'Admin' },
          { value: 'user', label: 'User' },
          { value: 'viewer', label: 'Viewer' },
        ]}
      />
    </Form>
  );
}
```

## `useValidatedForm` — external form control

Use the `useValidatedForm` hook when you need access to the form instance outside of the `<ValidatedForm>` component (e.g. for programmatic control, multi-step forms, or watching values):

```tsx
import {
  ValidatedForm,
  TextField,
  useValidatedForm,
} from '@liam-michel/validated-form';
import { z } from 'zod';

const schema = z.object({
  name: z.string().min(1),
});

function ControlledForm() {
  const form = useValidatedForm({
    schema,
    defaultValues: { name: '' },
  });

  return (
    <ValidatedForm
      schema={schema}
      form={form}
      onSubmit={async (data) => console.log(data)}
    >
      <TextField<typeof schema> name="name" label="Name" />
    </ValidatedForm>
  );
}
```

## Field components

All fields share these common props:

| Prop          | Type                  | Description                                         |
| ------------- | --------------------- | --------------------------------------------------- |
| `name`        | typed key from schema | The field name (type-safe based on your Zod schema) |
| `label`       | `string`              | Label text displayed above the field                |
| `description` | `string?`             | Helper text displayed below the field               |
| `placeholder` | `string?`             | Placeholder text                                    |
| `disabled`    | `boolean?`            | Disable the field                                   |
| `className`   | `string?`             | Additional CSS class for the wrapper                |
| `showReset`   | `boolean?`            | Show a reset button to restore the default value    |

### Available fields

| Component          | Schema type           | Extra props                                                    |
| ------------------ | --------------------- | -------------------------------------------------------------- |
| `TextField`        | `z.string()`          | `type?: HTMLInputTypeAttribute` (e.g. `"password"`, `"email"`) |
| `TextAreaField`    | `z.string()`          | —                                                              |
| `NumberField`      | `z.number()`          | —                                                              |
| `CheckboxField`    | `z.boolean()`         | —                                                              |
| `SelectField`      | `z.enum(...)`         | `options: { value, label }[]`                                  |
| `SliderField`      | `z.number()`          | `min?`, `max?`, `step?`                                        |
| `DateField`        | `z.date()`            | —                                                              |
| `MultiSelectField` | `z.array(z.string())` | `options: { value, label }[]`                                  |

## `ValidatedForm` props

| Prop            | Type                               | Description                                              |
| --------------- | ---------------------------------- | -------------------------------------------------------- |
| `schema`        | `ZodType`                          | Zod schema for validation                                |
| `onSubmit`      | `(data: Output) => Promise<void>`  | Submit handler (receives validated & typed data)         |
| `defaultValues` | `DefaultValues?`                   | Initial form values                                      |
| `form`          | `UseFormReturn?`                   | External form instance from `useValidatedForm`           |
| `submitLabel`   | `string?`                          | Custom label for the submit button (default: `"Submit"`) |
| `hideSubmit`    | `boolean?`                         | Hide the default submit button                           |
| `renderSubmit`  | `(state) => ReactNode`             | Custom submit button renderer                            |
| `renderError`   | `(message) => ReactNode`           | Custom root error renderer                               |
| `children`      | `ReactNode \| (form) => ReactNode` | Fields, or a render function receiving the form instance |

### Error handling

When `onSubmit` throws, errors are handled automatically:

- **Field-level errors**: Throw `{ data: { fieldErrors: { fieldName: "message" } } }` to set errors on specific fields.
- **Root-level errors**: Any other thrown error displays its `message` as a form-level error.

```tsx
<ValidatedForm
  schema={schema}
  onSubmit={async (data) => {
    const res = await api.createUser(data);
    if (!res.ok) {
      // Field-level errors
      throw { data: { fieldErrors: { email: 'Email already taken' } } };
    }
  }}
>
  {/* ... */}
</ValidatedForm>
```

## Type helpers

The library exports type helpers to extract field names by type from a schema:

```tsx
import type {
  StringFieldsOf,
  NumberFieldsOf,
  BooleanFieldsOf,
  DateFieldsOf,
  SelectFieldsOf,
} from '@liam-michel/validated-form';
```

These are used internally by the field components and can be useful if you're building custom fields.

## License

MIT