|
2 | 2 |
|
3 | 3 | > Type-safe form submissions for the modern web. |
4 | 4 |
|
5 | | -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: |
| 5 | +Conformal helps you work with native [`FormData`](https://developer.mozilla.org/docs/Web/API/FormData). It solves two major pain points: |
6 | 6 |
|
7 | 7 | - ✅ **Strongly typed FormData parsing** – Turn native `FormData` into real objects with full TypeScript inference (nested objects and arrays with dot/bracket notation). |
8 | 8 | - ✅ **Canonical submission flow** – A single `Submission` object that preserves raw input, separates field vs. form errors, and standardizes the success/error states. |
9 | 9 |
|
10 | | -Works everywhere: In browsers, Node.js, and edge runtimes with React, Vue, Svelte, or vanilla JavaScript. No framework lock-in. |
| 10 | +Works everywhere: In browsers, Node.js, and edge runtimes with React, Vue, Svelte, or vanilla JavaScript. |
11 | 11 |
|
12 | 12 | ### Table of Contents |
13 | 13 |
|
14 | | -- [Installation](#installation) |
| 14 | +- [Getting Started](#getting-started) |
15 | 15 | - [Live Examples](#live-examples) |
16 | | -- [Usage](#usage) |
17 | | - - [parseFormData](#parseformdata) |
18 | | - - [Submission](#submission) |
19 | | - - [decode](#decode) |
20 | | - - [serialize](#serialize) |
21 | | - - [getPath](#getpath) |
22 | | - - [setPath](#setpath) |
23 | | - - [PathsFromObject](#pathsfromobject) |
24 | | -- [Zod Field Schemas](#zod-field-schemas) |
| 16 | +- [API Reference](#api-reference) |
25 | 17 | - [License](#license) |
26 | 18 |
|
27 | | -## Installation |
| 19 | +## Getting Started |
28 | 20 |
|
29 | 21 | Install Conformal via npm or the package manager of your choice: |
30 | 22 |
|
31 | 23 | ```bash |
32 | 24 | npm install conformal |
33 | 25 | ``` |
34 | 26 |
|
35 | | -## Live Examples |
36 | | - |
37 | | -- **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) |
38 | | - |
39 | | -- **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) |
40 | | - |
41 | | -## Usage |
42 | | - |
43 | | -### parseFormData |
44 | | - |
45 | | -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. |
46 | | - |
47 | | -**🚀 Try it yourself**: This example includes an import map and can be run directly in a browser! |
48 | | - |
49 | | -```html |
50 | | -<body> |
51 | | - <form id="userForm"> |
52 | | - <input type="text" name="name" placeholder="Name" /> |
53 | | - <input type="number" name="age" placeholder="Age" /> |
54 | | - <input type="text" name="hobbies" placeholder="Hobby 1" /> |
55 | | - <input type="text" name="hobbies" placeholder="Hobby 2" /> |
56 | | - <button type="submit">Submit</button> |
57 | | - </form> |
58 | | - |
59 | | - <script type="importmap"> |
60 | | - { |
61 | | - "imports": { |
62 | | - "conformal": "https://cdn.jsdelivr.net/npm/conformal/+esm", |
63 | | - "zod": "https://cdn.jsdelivr.net/npm/zod/+esm" |
64 | | - } |
65 | | - } |
66 | | - </script> |
67 | | - |
68 | | - <script type="module"> |
69 | | - import { parseFormData } from "conformal"; |
70 | | - import * as z from "zod"; |
71 | | -
|
72 | | - const schema = z.object({ |
73 | | - name: z.string(), |
74 | | - age: z.coerce.number(), |
75 | | - hobbies: z.string().array(), |
76 | | - }); |
77 | | -
|
78 | | - const form = document.getElementById("userForm"); |
79 | | - form.addEventListener("submit", (event) => { |
80 | | - event.preventDefault(); |
81 | | -
|
82 | | - const formData = new FormData(form); |
83 | | - const submission = parseFormData(schema, formData).submission(); |
84 | | -
|
85 | | - if (submission.status === "success") { |
86 | | - console.log(submission.value); // Successful result value |
87 | | - console.log(submission.input); // Raw parsed form data |
88 | | - } else { |
89 | | - console.log(submission.fieldErrors); // Field-specific validation errors |
90 | | - console.log(submission.formErrors); // Form-level validation errors |
91 | | - console.log(submission.input); // Raw parsed form data |
92 | | - } |
93 | | - }); |
94 | | - </script> |
95 | | -</body> |
96 | | -``` |
97 | | - |
98 | | -This will result in the following data structure: |
| 27 | +Here's a quick example showing how Conformal handles form validation with a user registration form: |
99 | 28 |
|
100 | 29 | ```typescript |
101 | | -const value = { |
102 | | - name: "John Doe", |
103 | | - age: 30, |
104 | | - hobbies: ["Music", "Coding"], |
105 | | -}; |
106 | | -``` |
107 | | - |
108 | | -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: |
| 30 | +import { parseFormData } from "conformal"; |
| 31 | +import * as z from "zod"; // Tip: Use conformal/zod for automatic form input preprocessing |
| 32 | + |
| 33 | +const schema = z.object({ |
| 34 | + name: z.string().min(2, "Name must be at least 2 characters"), |
| 35 | + email: z.email("Invalid email address"), |
| 36 | + age: z.coerce.number().min(18, "Must be at least 18 years old"), |
| 37 | + acceptTerms: z.coerce.boolean(), |
| 38 | +}); |
109 | 39 |
|
110 | | -```typescript |
111 | | -const submission = parseFormData(schema, formData).submission(); |
| 40 | +// In your form action or handler |
| 41 | +const result = parseFormData(schema, formData); |
| 42 | +const submission = result.submission(); |
112 | 43 |
|
113 | 44 | if (submission.status === "success") { |
114 | | - // Access validated data |
115 | | - const validatedData = submission.value; |
116 | | - // Access raw parsed form data |
117 | | - const rawInput = submission.input; |
| 45 | + // submission.value is fully typed: { name: string, email: string, age: number, acceptTerms: boolean } |
| 46 | + console.log("User registered:", submission.value); |
118 | 47 | } else { |
119 | | - // Handle validation errors |
120 | | - const fieldErrors = submission.fieldErrors; // Field-specific errors |
121 | | - const formErrors = submission.formErrors; // Form-level errors |
122 | | - // Access raw parsed form data even on failure |
123 | | - const rawInput = submission.input; |
| 48 | + // submission.fieldErrors contains validation errors: { email: ["Invalid email address"] } |
| 49 | + console.log("Validation errors:", submission.fieldErrors); |
| 50 | + // submission.input preserves the raw user input for re-display |
| 51 | + console.log("User input:", submission.input); |
124 | 52 | } |
125 | 53 | ``` |
126 | 54 |
|
127 | | -### Submission |
128 | | - |
129 | | -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`. |
130 | | - |
131 | | -**Properties:** |
132 | | - |
133 | | -- **`status`**: A string that tells you the outcome - either `"success"` when validation passes, `"error"` when it fails, or `"idle"` for initial states |
134 | | -- **`value`**: Contains your validated and typed data when `status` is `"success"`. This is `undefined` when there are validation errors |
135 | | -- **`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 |
136 | | -- **`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 |
137 | | -- **`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 |
| 55 | +That's it! Conformal automatically handles FormData parsing, type coercion, and provides a clean submission interface. |
138 | 56 |
|
139 | | -**Key Benefits:** |
140 | | - |
141 | | -- **Type Safety**: Full TypeScript support with automatic type inference for your data |
142 | | -- **Data Preservation**: Raw input is always available, even on validation failure |
143 | | -- **Granular Error Handling**: Separate field and form-level errors for precise UI feedback |
144 | | -- **Immutable**: All properties are read-only, preventing accidental mutations |
145 | | - |
146 | | -### decode |
147 | | - |
148 | | -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. |
149 | | - |
150 | | -```typescript |
151 | | -import { decode } from "conformal"; |
152 | | - |
153 | | -const formData = new FormData(); |
154 | | -formData.append("user.name", "John Doe"); |
155 | | -formData.append("user.age", "30"); |
156 | | -formData.append("user.contacts[0].type", "email"); |
157 | | -formData.append("user.contacts[0].value", "john.doe@example.com"); |
158 | | -formData.append("user.contacts[1].type", "phone"); |
159 | | -formData.append("user.contacts[1].value", "123-456-7890"); |
160 | | - |
161 | | -const result = decode<{ |
162 | | - user: { |
163 | | - name: string; |
164 | | - age: string; |
165 | | - contacts: { type: string; value: string }[]; |
166 | | - }; |
167 | | -}>(formData); |
168 | | -``` |
169 | | - |
170 | | -This will result in the following data structure: |
171 | | - |
172 | | -```typescript |
173 | | -const result = { |
174 | | - user: { |
175 | | - name: "John Doe", |
176 | | - age: "30", |
177 | | - contacts: [ |
178 | | - { type: "email", value: "john.doe@example.com" }, |
179 | | - { type: "phone", value: "123-456-7890" }, |
180 | | - ], |
181 | | - }, |
182 | | -}; |
183 | | -``` |
184 | | - |
185 | | -### serialize |
186 | | - |
187 | | -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. |
188 | | - |
189 | | -```typescript |
190 | | -import { serialize } from "conformal"; |
191 | | - |
192 | | -console.log(serialize(123)); // "123" |
193 | | -console.log(serialize(true)); // "on" |
194 | | -console.log(serialize(new Date())); // "2025-01-17T17:04:25.059Z" |
195 | | -console.log(serialize({ username: "test", age: 100 })); // { username: "test", age: "100" } |
196 | | -``` |
197 | | - |
198 | | -### getPath |
199 | | - |
200 | | -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. |
201 | | - |
202 | | -```typescript |
203 | | -import { getPath } from "conformal"; |
204 | | - |
205 | | -const value = getPath({ a: { b: { c: ["hey", "Hi!"] } } }, "a.b.c[1]"); |
206 | | -// Returns 'Hi!' |
207 | | -``` |
208 | | - |
209 | | -### setPath |
210 | | - |
211 | | -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. |
212 | | - |
213 | | -```typescript |
214 | | -import { setPath } from "conformal"; |
215 | | - |
216 | | -const newObj = setPath({ a: { b: { c: [] } } }, "a.b.c[1]", "hey"); |
217 | | -// Returns { a: { b: { c: [<empty>, 'hey'] } } } |
218 | | -``` |
219 | | - |
220 | | -### PathsFromObject |
| 57 | +## Live Examples |
221 | 58 |
|
222 | | -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. |
| 59 | +- **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) |
223 | 60 |
|
224 | | -```typescript |
225 | | -import type { PathsFromObject } from "conformal"; |
| 61 | +- **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) |
226 | 62 |
|
227 | | -interface UserForm { |
228 | | - user: { |
229 | | - name: string; |
230 | | - profilePicture: File; |
231 | | - contacts: { type: string; value: string }[]; |
232 | | - }; |
233 | | -} |
| 63 | +## API Reference |
234 | 64 |
|
235 | | -type Paths = PathsFromObject<UserForm>; |
236 | | -// Paths will be "user" | "user.name" | "user.profilePicture" | "user.contacts" | `user.contacts[${number}]` | `user.contacts[${number}].type` | `user.contacts[${number}].value` |
237 | | -``` |
| 65 | +### Core Functions |
238 | 66 |
|
239 | | -## Zod Field Schemas |
| 67 | +- **[`parseFormData`](src/README.md#parseformdata)** - Parse FormData with schema validation and get Submission object |
| 68 | +- **[`decode`](src/README.md#decode)** - Convert FormData to structured objects (no validation) |
| 69 | +- **[`serialize`](src/README.md#serialize)** - Transform typed values back to form-compatible strings |
| 70 | +- **[`getPath`](src/README.md#getpath)** - Safely access nested values using dot/bracket notation |
| 71 | +- **[`setPath`](src/README.md#setpath)** - Immutably set nested values using dot/bracket notation |
240 | 72 |
|
241 | | -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**. |
| 73 | +### Types |
242 | 74 |
|
243 | | -**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. |
| 75 | +- **[`Submission`](src/README.md#submission)** - Standardized submission result with success/error states |
| 76 | +- **[`PathsFromObject`](src/README.md#pathsfromobject)** - Type utility to extract all possible object paths |
244 | 77 |
|
245 | | -```typescript |
246 | | -import * as zf from "conformal/zod"; |
| 78 | +### Zod Utilities |
247 | 79 |
|
248 | | -const formSchema = zf.object({ |
249 | | - name: zf.string().optional(), |
250 | | - email: zf.email(), |
251 | | - age: zf.number().min(13, "Must be at least 13 years old"), |
252 | | - hobbies: zf.array(zf.string()), |
253 | | - birthDate: zf.date(), |
254 | | - acceptTerms: zf.boolean(), |
255 | | - profilePicture: zf.file(), |
256 | | - accountType: zf.enum(["personal", "business"]), |
257 | | - website: zf.url().optional(), |
258 | | - transactionAmount: zf.bigint(), |
259 | | -}); |
260 | | -``` |
| 80 | +- **[Zod Field Schemas](src/zod/README.md#field-schemas)** - Zod schemas with automatic form input preprocessing |
261 | 81 |
|
262 | 82 | ## License |
263 | 83 |
|
|
0 commit comments