# Form Validation
Typed form primitives and three pre-built forms for Next.js 14+ — built on React Hook Form + Zod, with server-side validation and a multi-step hook included.
## What's included
**Field components**
- `Field` — input for text, email, password, number, tel; shows inline error and optional hint
- `TextareaField` — resizable textarea with optional character counter
- `SelectField` — styled native select with chevron, placeholder support, inline error
- `CheckboxField` — checkbox with ReactNode label (supports JSX like links inside label)
**Pre-built forms**
- `RegisterForm` — name + email + password + GitHub username; server error display built in
- `ContactForm` — name/email grid + subject + message with char count; success state built in
- `ChangePasswordForm` — current + new + confirm password; calls `onSuccess` callback on completion
**Schemas (Zod)**
- `Schemas` — individual reusable primitives: `email`, `password`, `name`, `url`, `phone`, `githubUsername`, `positiveInt`, `uuid`
- `LoginSchema`, `RegisterSchema`, `ContactSchema`, `ChangePasswordSchema`, `ProfileSchema` — composed schemas ready to use
- Matching TypeScript types: `LoginInput`, `RegisterInput`, `ContactInput`, `ChangePasswordInput`, `ProfileInput`
**Utilities**
- `validateBody(schema, body)` — server-side Zod parse; throws a readable error string on failure, returns typed data on success
- `useMultiStep(totalSteps)` — returns `step`, `next`, `prev`, `goTo`, `isFirst`, `isLast`, `progress`
## Setup
### 1. Install dependencies
```bash
npm install react-hook-form @hookform/resolvers zod
```
### 2. Add to your project
```tsx
// Any client component — mark with 'use client'
import { Field, LoginSchema, type LoginInput } from '@/blocks/formvalidation'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
const form = useForm<LoginInput>({ resolver: zodResolver(LoginSchema) })
```
## Usage examples
```tsx
// Custom form using primitives
'use client'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { Field, TextareaField, ProfileSchema, type ProfileInput } from '@/blocks/formvalidation'
export function ProfileForm({ onSubmit }: { onSubmit: (d: ProfileInput) => Promise<void> }) {
const form = useForm<ProfileInput>({ resolver: zodResolver(ProfileSchema) })
return (
<form onSubmit={form.handleSubmit(onSubmit)} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<Field form={form} name="name" label="Name" required />
<Field form={form} name="email" label="Email" type="email" required />
<Field form={form} name="website" label="Website" placeholder="https://..." hint="Optional" />
<TextareaField form={form} name="bio" label="Bio" maxLength={280} showCount />
<button type="submit">Save</button>
</form>
)
}
```
```tsx
// Drop-in pre-built forms
import { RegisterForm, ContactForm } from '@/blocks/formvalidation'
<RegisterForm onSubmit={async (data) => {
await fetch('/api/auth/register', { method: 'POST', body: JSON.stringify(data) })
}} />
<ContactForm onSubmit={async (data) => {
await fetch('/api/contact', { method: 'POST', body: JSON.stringify(data) })
}} />
```
```ts
// Server-side validation in an API route
import { validateBody, RegisterSchema } from '@/blocks/formvalidation'
export async function POST(req: Request) {
const body = await req.json()
const data = validateBody(RegisterSchema, body) // throws on invalid input
// data is fully typed as RegisterInput
}
```
## Notes
- All field components are `'use client'` — the file has the directive at the top, so don't import field components from server components directly; re-export them from a client boundary if needed
- Styling uses inline styles with CSS variables (`--text`, `--border`, `--bg`, `--accent`) — override these on `:root` to match your theme; no Tailwind dependency
- `validateBody` throws a plain `Error` with a semicolon-joined message like `email: Invalid email; password: Min 8 chars` — catch it in your API route and return a 400
- `useMultiStep` manages step state only — it doesn't validate each step before advancing; call `form.trigger(fieldsOnThisStep)` before calling `ms.next()` if you want per-step validation