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

# @liam-michel/simple-form

[![CI](https://github.com/liam-michel/simple-form/actions/workflows/ci.yml/badge.svg)](https://github.com/liam-michel/simple-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/simple-form)](https://www.npmjs.com/package/@liam-michel/simple-form)
[![npm downloads](https://img.shields.io/npm/dm/@liam-michel/simple-form)](https://www.npmjs.com/package/@liam-michel/simple-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/simple-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 { SimpleForm, TextField, NumberField, CheckboxField } from '@liam-michel/simple-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 (
    <SimpleForm
      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" />
    </SimpleForm>
  );
}
```

## `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/simple-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>
  );
}
```

## `useSimpleForm` — external form control

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

```tsx
import { SimpleForm, TextField, useSimpleForm } from '@liam-michel/simple-form';
import { z } from 'zod';

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

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

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

## 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 }[]` |

## `SimpleForm` 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 `useSimpleForm` |
| `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
<SimpleForm
  schema={schema}
  onSubmit={async (data) => {
    const res = await api.createUser(data);
    if (!res.ok) {
      // Field-level errors
      throw { data: { fieldErrors: { email: 'Email already taken' } } };
    }
  }}
>
  {/* ... */}
</SimpleForm>
```

## 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/simple-form';
```

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

## License

MIT