Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ cd examples/svelte && npm i && npm run dev
## Public API

- `conformal`: `getPath`, `setPath`, `decode`, `parseFormData`, `serialize`; types: `PathsFromObject`, `Submission`
- `conformal/zod`: `string`, `number`, `boolean`, `date`, `bigint`, `enum`, `file`, `url`, `email`
- `conformal/zod`: `string`, `number`, `boolean`, `date`, `bigint`, `enum`, `file`, `url`, `email`, `object`, `array`

Exports live in `src/index.ts` and `src/zod/index.ts`.

Expand All @@ -43,7 +43,7 @@ Exports live in `src/index.ts` and `src/zod/index.ts`.

```bash
# Fast local CI
npm run format:check && npm run typecheck && npm run test && npm run build
npm run format:check && npm run typecheck && npm run test

# Focus tests
npx vitest run -t "<name>" # by test name
Expand Down
260 changes: 40 additions & 220 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,262 +2,82 @@

> Type-safe form submissions for the modern web.

Conformal helps you work with native [`FormData`](https://developer.mozilla.org/docs/Web/API/FormData) the way frameworks are moving: directly. It solves two major pain points:
Conformal helps you work with native [`FormData`](https://developer.mozilla.org/docs/Web/API/FormData). It solves two major pain points:

- ✅ **Strongly typed FormData parsing** – Turn native `FormData` into real objects with full TypeScript inference (nested objects and arrays with dot/bracket notation).
- ✅ **Canonical submission flow** – A single `Submission` object that preserves raw input, separates field vs. form errors, and standardizes the success/error states.

Works everywhere: In browsers, Node.js, and edge runtimes with React, Vue, Svelte, or vanilla JavaScript. No framework lock-in.
Works everywhere: In browsers, Node.js, and edge runtimes with React, Vue, Svelte, or vanilla JavaScript.

### Table of Contents

- [Installation](#installation)
- [Getting Started](#getting-started)
- [Live Examples](#live-examples)
- [Usage](#usage)
- [parseFormData](#parseformdata)
- [Submission](#submission)
- [decode](#decode)
- [serialize](#serialize)
- [getPath](#getpath)
- [setPath](#setpath)
- [PathsFromObject](#pathsfromobject)
- [Zod Field Schemas](#zod-field-schemas)
- [API Reference](#api-reference)
- [License](#license)

## Installation
## Getting Started

Install Conformal via npm or the package manager of your choice:

```bash
npm install conformal
```

## Live Examples

- **React** - Form actions with useActionState: [StackBlitz](https://stackblitz.com/github/marcomuser/conformal/tree/main/examples/react?embed=1&theme=dark&preset=node&file=src/Form.tsx) | [Source](https://github.com/marcomuser/conformal/tree/main/examples/react)

- **SvelteKit** - Server-side form actions: [StackBlitz](https://stackblitz.com/github/marcomuser/conformal/tree/main/examples/svelte?embed=1&theme=dark&preset=node&file=src/routes/%2Bpage.server.ts) | [Source](https://github.com/marcomuser/conformal/tree/main/examples/svelte)

## Usage

### parseFormData

The `parseFormData` function parses and validates [FormData](https://developer.mozilla.org/docs/Web/API/FormData) against a [Standard Schema](https://standardschema.dev). It internally uses the [decode](#decode) function to first convert the `FormData` into a structured object before applying schema validation.

**🚀 Try it yourself**: This example includes an import map and can be run directly in a browser!

```html
<body>
<form id="userForm">
<input type="text" name="name" placeholder="Name" />
<input type="number" name="age" placeholder="Age" />
<input type="text" name="hobbies" placeholder="Hobby 1" />
<input type="text" name="hobbies" placeholder="Hobby 2" />
<button type="submit">Submit</button>
</form>

<script type="importmap">
{
"imports": {
"conformal": "https://cdn.jsdelivr.net/npm/conformal/+esm",
"zod": "https://cdn.jsdelivr.net/npm/zod/+esm"
}
}
</script>

<script type="module">
import { parseFormData } from "conformal";
import * as z from "zod";

const schema = z.object({
name: z.string(),
age: z.coerce.number(),
hobbies: z.string().array(),
});

const form = document.getElementById("userForm");
form.addEventListener("submit", (event) => {
event.preventDefault();

const formData = new FormData(form);
const submission = parseFormData(schema, formData).submission();

if (submission.status === "success") {
console.log(submission.value); // Successful result value
console.log(submission.input); // Raw parsed form data
} else {
console.log(submission.fieldErrors); // Field-specific validation errors
console.log(submission.formErrors); // Form-level validation errors
console.log(submission.input); // Raw parsed form data
}
});
</script>
</body>
```

This will result in the following data structure:
Here's a quick example showing how Conformal handles form validation with a user registration form:

```typescript
const value = {
name: "John Doe",
age: 30,
hobbies: ["Music", "Coding"],
};
```

The `parseFormData` function returns a `SchemaResult` object that extends the standard schema validation result with a `submission()` method. This method provides a consistent `Submission` object that makes it easy to handle both successful and failed validation results:
import { parseFormData } from "conformal";
import * as z from "zod"; // Tip: Use conformal/zod for automatic form input preprocessing

const schema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.email("Invalid email address"),
age: z.coerce.number().min(18, "Must be at least 18 years old"),
acceptTerms: z.coerce.boolean(),
});

```typescript
const submission = parseFormData(schema, formData).submission();
// In your form action or handler
const result = parseFormData(schema, formData);
const submission = result.submission();

if (submission.status === "success") {
// Access validated data
const validatedData = submission.value;
// Access raw parsed form data
const rawInput = submission.input;
// submission.value is fully typed: { name: string, email: string, age: number, acceptTerms: boolean }
console.log("User registered:", submission.value);
} else {
// Handle validation errors
const fieldErrors = submission.fieldErrors; // Field-specific errors
const formErrors = submission.formErrors; // Form-level errors
// Access raw parsed form data even on failure
const rawInput = submission.input;
// submission.fieldErrors contains validation errors: { email: ["Invalid email address"] }
console.log("Validation errors:", submission.fieldErrors);
// submission.input preserves the raw user input for re-display
console.log("User input:", submission.input);
}
```

### Submission

The `Submission` type represents the result of form validation and provides a clean interface for handling both successful and failed validation results. This is the type that the `submission()` method returns from `parseFormData`.

**Properties:**

- **`status`**: A string that tells you the outcome - either `"success"` when validation passes, `"error"` when it fails, or `"idle"` for initial states
- **`value`**: Contains your validated and typed data when `status` is `"success"`. This is `undefined` when there are validation errors
- **`input`**: Always contains the raw user input that was submitted, regardless of validation success or failure. This is useful for preserving user input even when validation fails
- **`fieldErrors`**: An object that maps field names to arrays of error messages. For example, `{ "email": ["Invalid email format"], "age": ["Must be a number"] }`. This is empty when validation succeeds
- **`formErrors`**: An array of form-level validation errors that aren't tied to specific fields. For example, `["Passwords don't match", "Terms must be accepted"]`. This is empty when validation succeeds
That's it! Conformal automatically handles FormData parsing, type coercion, and provides a clean submission interface.

**Key Benefits:**

- **Type Safety**: Full TypeScript support with automatic type inference for your data
- **Data Preservation**: Raw input is always available, even on validation failure
- **Granular Error Handling**: Separate field and form-level errors for precise UI feedback
- **Immutable**: All properties are read-only, preventing accidental mutations

### decode

The `decode` function allows you to convert a `FormData` object into a structured object with typed values. It supports both dot notation for nested objects and square bracket notation for arrays. You can mix dot and square bracket notation to create complex structures. The `decode` function allows you to create your own schema validator in cases where `parseFormData` does not support your use case.

```typescript
import { decode } from "conformal";

const formData = new FormData();
formData.append("user.name", "John Doe");
formData.append("user.age", "30");
formData.append("user.contacts[0].type", "email");
formData.append("user.contacts[0].value", "john.doe@example.com");
formData.append("user.contacts[1].type", "phone");
formData.append("user.contacts[1].value", "123-456-7890");

const result = decode<{
user: {
name: string;
age: string;
contacts: { type: string; value: string }[];
};
}>(formData);
```

This will result in the following data structure:

```typescript
const result = {
user: {
name: "John Doe",
age: "30",
contacts: [
{ type: "email", value: "john.doe@example.com" },
{ type: "phone", value: "123-456-7890" },
],
},
};
```

### serialize

The `serialize` function transforms fully typed values back to the InputValue shape for use in form elements. This is particularly useful for setting default values in form fields when you have validated data from a previous submission and want to pre-fill forms with existing data from a database.

```typescript
import { serialize } from "conformal";

console.log(serialize(123)); // "123"
console.log(serialize(true)); // "on"
console.log(serialize(new Date())); // "2025-01-17T17:04:25.059Z"
console.log(serialize({ username: "test", age: 100 })); // { username: "test", age: "100" }
```

### getPath

Retrieve a value from an object using a path. This function is a foundational tool for handling object paths using dot and square bracket notation. It's particularly useful for developers building custom client-side validation libraries or complex data manipulation patterns.

```typescript
import { getPath } from "conformal";

const value = getPath({ a: { b: { c: ["hey", "Hi!"] } } }, "a.b.c[1]");
// Returns 'Hi!'
```

### setPath

Set a value in an object using a path. The `setPath` function is used internally by the `decode` function and provides powerful object manipulation capabilities. **Note**: Creates copies only where needed to preserve immutability, avoiding unnecessary deep copying for better performance.

```typescript
import { setPath } from "conformal";

const newObj = setPath({ a: { b: { c: [] } } }, "a.b.c[1]", "hey");
// Returns { a: { b: { c: [<empty>, 'hey'] } } }
```

### PathsFromObject
## Live Examples

Extract all possible paths from an object type while automatically excluding paths that lead to browser-specific built-in types such as Blob, FileList, and Date. This type utility is useful for creating abstractions that enable type-safe access to specific fields within complex form data structures.
- **React** - Form actions with useActionState: [StackBlitz](https://stackblitz.com/github/marcomuser/conformal/tree/main/examples/react?embed=1&theme=dark&preset=node&file=src/Form.tsx) | [Source](https://github.com/marcomuser/conformal/tree/main/examples/react)

```typescript
import type { PathsFromObject } from "conformal";
- **SvelteKit** - Server-side form actions: [StackBlitz](https://stackblitz.com/github/marcomuser/conformal/tree/main/examples/svelte?embed=1&theme=dark&preset=node&file=src/routes/%2Bpage.server.ts) | [Source](https://github.com/marcomuser/conformal/tree/main/examples/svelte)

interface UserForm {
user: {
name: string;
profilePicture: File;
contacts: { type: string; value: string }[];
};
}
## API Reference

type Paths = PathsFromObject<UserForm>;
// Paths will be "user" | "user.name" | "user.profilePicture" | "user.contacts" | `user.contacts[${number}]` | `user.contacts[${number}].type` | `user.contacts[${number}].value`
```
### Core Functions

## Zod Field Schemas
- **[`parseFormData`](src/README.md#parseformdata)** - Parse FormData with schema validation and get Submission object
- **[`decode`](src/README.md#decode)** - Convert FormData to structured objects (no validation)
- **[`serialize`](src/README.md#serialize)** - Transform typed values back to form-compatible strings
- **[`getPath`](src/README.md#getpath)** - Safely access nested values using dot/bracket notation
- **[`setPath`](src/README.md#setpath)** - Immutably set nested values using dot/bracket notation

Conformal provides optional Zod utilities that are **thin preprocessing wrappers** around Zod schemas. They automatically handle form input patterns (empty strings, type coercion, boolean detection) while maintaining **100% Zod compatibility**.
### Types

**Zero learning curve** - use them exactly like regular Zod schemas with all methods (`.optional()`, `.min()`, `.max()` etc.). Import from `conformal/zod` to keep your bundle lean if you don't use Zod.
- **[`Submission`](src/README.md#submission)** - Standardized submission result with success/error states
- **[`PathsFromObject`](src/README.md#pathsfromobject)** - Type utility to extract all possible object paths

```typescript
import * as zf from "conformal/zod";
### Zod Utilities

const formSchema = zf.object({
name: zf.string().optional(),
email: zf.email(),
age: zf.number().min(13, "Must be at least 13 years old"),
hobbies: zf.array(zf.string()),
birthDate: zf.date(),
acceptTerms: zf.boolean(),
profilePicture: zf.file(),
accountType: zf.enum(["personal", "business"]),
website: zf.url().optional(),
transactionAmount: zf.bigint(),
});
```
- **[Zod Field Schemas](src/zod/README.md#field-schemas)** - Zod schemas with automatic form input preprocessing

## License

Expand Down
Loading