@railway-ts/use-form 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +206 -37
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -2,11 +2,29 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@railway-ts/use-form.svg)](https://www.npmjs.com/package/@railway-ts/use-form) [![Build Status](https://github.com/sakobu/railway-ts-use-form/workflows/CI/badge.svg)](https://github.com/sakobu/railway-ts-use-form/actions) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue)](https://www.typescriptlang.org/) [![Coverage](https://img.shields.io/codecov/c/github/sakobu/railway-ts-use-form)](https://codecov.io/gh/sakobu/railway-ts-use-form)
4
4
 
5
- React form hook where the schema is the source of truth. Define validation once, get TypeScript types, field autocomplete, error handling, and native HTML bindings for free.
5
+ Schema-first React forms with full TypeScript safety, composable validation, and native HTML bindings.
6
6
 
7
7
  **~3.6 kB** minified + brotli
8
8
 
9
- > **Part of the [@railway-ts](https://github.com/sakobu) ecosystem.** Uses [@railway-ts/pipelines](https://github.com/sakobu/railway-ts-pipelines) for composable validation and Result types. Also accepts any [Standard Schema v1](https://github.com/standard-schema/standard-schema) validator (Zod, Valibot, ArkType).
9
+ ## Why?
10
+
11
+ Most React form solutions split validation from types from bindings. You define a schema in one place, extract types in another, wire up resolvers in a third, and manually plumb errors into your UI. Every layer is a seam where things drift.
12
+
13
+ This library treats the **schema as the single source of truth**. One declaration gives you:
14
+
15
+ - TypeScript types (inferred, never duplicated)
16
+ - Validation (composable, accumulates all errors in one pass)
17
+ - Field bindings (spread onto native HTML elements)
18
+ - Error handling (three layers with deterministic priority)
19
+
20
+ Bring your own Zod, Valibot, or ArkType via [Standard Schema](https://github.com/standard-schema/standard-schema), or use [@railway-ts/pipelines](https://github.com/sakobu/railway-ts-pipelines) natively for cross-field validation, targeted error placement, and `Result` types.
21
+
22
+ ## Design
23
+
24
+ - **Schema-driven** -- define once, get types + validation + field paths
25
+ - **Three error layers** -- client, field async, server -- with deterministic priority
26
+ - **Native HTML bindings** -- spread onto inputs, selects, checkboxes, files, radios
27
+ - **Railway Result** -- `handleSubmit` returns `Result<T, E>` for pattern matching
10
28
 
11
29
  ## Install
12
30
 
@@ -14,7 +32,7 @@ React form hook where the schema is the source of truth. Define validation once,
14
32
  bun add @railway-ts/use-form @railway-ts/pipelines # or npm, pnpm, yarn
15
33
  ```
16
34
 
17
- Requires React 18+ and @railway-ts/pipelines ^0.1.12.
35
+ Requires React 18+ and @railway-ts/pipelines ^0.1.13.
18
36
 
19
37
  ## Quick Start
20
38
 
@@ -65,52 +83,173 @@ export function LoginForm() {
65
83
  }
66
84
  ```
67
85
 
68
- ## Examples
86
+ ## Real-World Use Case
69
87
 
70
- Clone and run:
88
+ Registration form with cross-field validation (`refineAt` for password confirmation), per-field async validation (`fieldValidators` for username availability), and server errors -- all in one component:
71
89
 
72
- ```bash
73
- git clone https://github.com/sakobu/railway-ts-use-form.git
74
- cd railway-ts-use-form
75
- bun install
76
- bun run example
77
- ```
90
+ ```tsx
91
+ import { useNavigate } from 'react-router-dom';
92
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
93
+ import { useForm } from '@railway-ts/use-form';
94
+ import {
95
+ object,
96
+ string,
97
+ required,
98
+ chain,
99
+ refineAt,
100
+ nonEmpty,
101
+ email,
102
+ minLength,
103
+ ROOT_ERROR_KEY,
104
+ type InferSchemaType,
105
+ } from '@railway-ts/pipelines/schema';
78
106
 
79
- Then open http://localhost:3000. The interactive app has tabs for:
107
+ // --- Schema ---
80
108
 
81
- - **Sync** -- Basic form with schema validation
82
- - **Async (Cross-field)** -- Async schema with cross-field rules (password confirmation, date ranges)
83
- - **Zod** -- Standard Schema integration with Zod
84
- - **Valibot** -- Standard Schema integration with Valibot
85
- - **Field Validators** -- Per-field async validation with loading indicators
109
+ const schema = chain(
110
+ object({
111
+ username: required(chain(string(), nonEmpty(), minLength(3))),
112
+ email: required(chain(string(), nonEmpty(), email())),
113
+ password: required(chain(string(), nonEmpty(), minLength(8))),
114
+ confirmPassword: required(chain(string(), nonEmpty())),
115
+ }),
116
+ refineAt(
117
+ 'confirmPassword',
118
+ (d) => d.password === d.confirmPassword,
119
+ 'Passwords must match'
120
+ )
121
+ );
86
122
 
87
- Or try it live on [StackBlitz](https://stackblitz.com/edit/vitejs-vite-c3zpmon9?embed=1&file=src%2FApp.tsx).
123
+ type Registration = InferSchemaType<typeof schema>;
124
+
125
+ // --- API layer ---
126
+
127
+ type UsernameCheck = { available: boolean };
128
+ type ServerErrors = Record<string, string>;
129
+
130
+ const toJsonIfOk = (res: Response) =>
131
+ res.ok ? res.json() : Promise.reject(`HTTP ${res.status}`);
132
+
133
+ const checkUsername = (username: string): Promise<UsernameCheck> =>
134
+ fetch(`/api/check-username?u=${encodeURIComponent(username)}`).then(
135
+ toJsonIfOk
136
+ );
137
+
138
+ class ApiValidationError extends Error {
139
+ constructor(public fieldErrors: ServerErrors) {
140
+ super('Validation failed');
141
+ }
142
+ }
143
+
144
+ const registerUser = async (values: Registration): Promise<void> => {
145
+ const res = await fetch('/api/register', {
146
+ method: 'POST',
147
+ headers: { 'Content-Type': 'application/json' },
148
+ body: JSON.stringify(values),
149
+ });
150
+
151
+ if (!res.ok) throw new ApiValidationError(await res.json());
152
+ };
153
+
154
+ // --- Component ---
155
+
156
+ export function RegistrationForm() {
157
+ const navigate = useNavigate();
158
+ const queryClient = useQueryClient();
159
+
160
+ const mutation = useMutation({
161
+ mutationFn: registerUser,
162
+ onSuccess: () => navigate('/welcome'),
163
+ onError: (error) => {
164
+ if (error instanceof ApiValidationError) {
165
+ form.setServerErrors(error.fieldErrors);
166
+ } else {
167
+ form.setServerErrors({
168
+ [ROOT_ERROR_KEY]: 'Network error. Please try again.',
169
+ });
170
+ }
171
+ },
172
+ });
173
+
174
+ const form = useForm<Registration>(schema, {
175
+ initialValues: {
176
+ username: '',
177
+ email: '',
178
+ password: '',
179
+ confirmPassword: '',
180
+ },
181
+ fieldValidators: {
182
+ username: async (value) => {
183
+ const username = value as string;
184
+ if (username.length < 3) return undefined;
185
+
186
+ try {
187
+ const { available } = await queryClient.fetchQuery({
188
+ queryKey: ['check-username', username],
189
+ queryFn: () => checkUsername(username),
190
+ staleTime: 30_000,
191
+ });
192
+ return available ? undefined : 'Username is already taken';
193
+ } catch {
194
+ return 'Unable to check username availability';
195
+ }
196
+ },
197
+ },
198
+ onSubmit: (values) => mutation.mutate(values),
199
+ });
200
+
201
+ return (
202
+ <form onSubmit={(e) => void form.handleSubmit(e)}>
203
+ <input {...form.getFieldProps('username')} />
204
+ {form.validatingFields.username && <span>Checking...</span>}
205
+ {form.touched.username && form.errors.username && (
206
+ <span>{form.errors.username}</span>
207
+ )}
208
+
209
+ <input type="email" {...form.getFieldProps('email')} />
210
+ {form.touched.email && form.errors.email && (
211
+ <span>{form.errors.email}</span>
212
+ )}
213
+
214
+ <input type="password" {...form.getFieldProps('password')} />
215
+ {form.touched.password && form.errors.password && (
216
+ <span>{form.errors.password}</span>
217
+ )}
218
+
219
+ <input type="password" {...form.getFieldProps('confirmPassword')} />
220
+ {form.touched.confirmPassword && form.errors.confirmPassword && (
221
+ <span>{form.errors.confirmPassword}</span>
222
+ )}
223
+
224
+ {form.errors[ROOT_ERROR_KEY] && (
225
+ <span>{form.errors[ROOT_ERROR_KEY]}</span>
226
+ )}
227
+
228
+ <button
229
+ type="submit"
230
+ disabled={mutation.isPending || form.isValidating || !form.isValid}
231
+ >
232
+ {mutation.isPending ? 'Registering...' : 'Create Account'}
233
+ </button>
234
+ </form>
235
+ );
236
+ }
237
+ ```
238
+
239
+ Cross-field validation, async username check, server errors, and React Query integration -- production patterns, zero glue code.
88
240
 
89
241
  ## What's Included
90
242
 
91
243
  - **Type-safe field paths** -- autocomplete for nested fields, dot-notation everywhere
92
244
  - **Railway validation** -- composable validators that accumulate all errors in one pass
93
- - **Standard Schema v1** -- bring your own Zod, Valibot, or ArkType schema instead
245
+ - **Standard Schema v1** -- bring your own Zod, Valibot, or ArkType schema
94
246
  - **Native HTML bindings** -- spread `getFieldProps` onto inputs, selects, checkboxes, files, radios, sliders
95
- - **Three error layers** -- client validation, per-field async validators, and server errors with automatic priority
247
+ - **Three error layers** -- client, per-field async, and server errors with automatic priority
96
248
  - **Array helpers** -- type-safe `push`, `remove`, `swap`, `move`, `insert`, `replace` with field bindings
97
- - **Four validation modes** -- `live`, `blur`, `mount`, `submit` for different UX needs
249
+ - **Four validation modes** -- `live`, `blur`, `mount`, `submit`
98
250
  - **Auto-submission** -- `useFormAutoSubmission` for search/filter forms with debounced submit
99
251
  - **React 18 + 19** -- compatible with both, tree-shakeable ESM
100
252
 
101
- ## API at a Glance
102
-
103
- The `useForm` hook returns:
104
-
105
- | Category | Properties / Methods |
106
- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
107
- | **State** | `values`, `touched`, `errors`, `clientErrors`, `serverErrors`, `fieldErrors`, `isValid`, `isDirty`, `isSubmitting`, `isValidating`, `validatingFields`, `submitCount` |
108
- | **Field Management** | `setFieldValue(field, value, shouldValidate?)`, `setFieldTouched(field, touched?, shouldValidate?)`, `setValues(values, shouldValidate?)` |
109
- | **Server Errors** | `setServerErrors(errors)`, `clearServerErrors()` |
110
- | **Form Actions** | `handleSubmit(e?)` → `Promise<Result>`, `resetForm()`, `validateForm(values)` |
111
- | **Field Bindings** | `getFieldProps`, `getSelectFieldProps`, `getCheckboxProps`, `getSwitchProps`, `getSliderProps`, `getFileFieldProps`, `getCheckboxGroupOptionProps`, `getRadioGroupOptionProps` |
112
- | **Arrays** | `arrayHelpers(field)` → `{ values, push, remove, insert, swap, move, replace, getFieldProps, ... }` |
113
-
114
253
  ## Works With
115
254
 
116
255
  Any [Standard Schema v1](https://github.com/standard-schema/standard-schema) library works out of the box -- no adapters, no wrappers. Pass the schema directly to `useForm`:
@@ -120,16 +259,46 @@ Any [Standard Schema v1](https://github.com/standard-schema/standard-schema) lib
120
259
  - **ArkType** 2.0+
121
260
  - **@railway-ts/pipelines** (native)
122
261
 
123
- See [Recipes: Standard Schema](./docs/RECIPES.md#standard-schema--bring-your-own-validator) for Zod and Valibot examples.
262
+ See [Recipes: Standard Schema](./docs/RECIPES.md#standard-schema-bring-your-own-validator) for Zod and Valibot examples.
263
+
264
+ ## Ecosystem
265
+
266
+ `@railway-ts/use-form` is built on [@railway-ts/pipelines](https://github.com/sakobu/railway-ts-pipelines) -- composable, type-safe validation with Railway-oriented Result types. Use pipelines standalone for backend validation, or pair it with this hook for full-stack type safety.
124
267
 
125
268
  ## Documentation
126
269
 
127
270
  - **[Getting Started](docs/GETTING_STARTED.md)** -- Step-by-step from first form to arrays
128
271
  - **[Recipes](./docs/RECIPES.md)** -- Patterns and techniques, each recipe self-contained
129
- - **[Advanced](./docs/ADVANCED.md)** -- Error priority, discriminated unions, custom validators
130
272
  - **[API Reference](./docs/API.md)** -- Complete API documentation
131
273
 
132
- For a real-world example combining schema validation, async field validators, server errors, cross-field validation, and Result pattern-matching, see the [Full Registration Form](./docs/RECIPES.md#capstone-full-registration-form) recipe.
274
+ ## Examples
275
+
276
+ Clone and run:
277
+
278
+ ```bash
279
+ git clone https://github.com/sakobu/railway-ts-use-form.git
280
+ cd railway-ts-use-form
281
+ bun install
282
+ bun run example
283
+ ```
284
+
285
+ Then open http://localhost:3000. The interactive app has tabs for:
286
+
287
+ - **Sync** -- Basic form with schema validation
288
+ - **Async (Cross-field)** -- Async schema with cross-field rules (password confirmation, date ranges)
289
+ - **Zod** -- Standard Schema integration with Zod
290
+ - **Valibot** -- Standard Schema integration with Valibot
291
+ - **Field Validators** -- Per-field async validation with loading indicators
292
+
293
+ Or try it live on [StackBlitz](https://stackblitz.com/edit/vitejs-vite-c3zpmon9?embed=1&file=src%2FApp.tsx).
294
+
295
+ ## Philosophy
296
+
297
+ - Schema is the single source of truth
298
+ - Validation should accumulate, not short-circuit
299
+ - Types should be inferred, never duplicated
300
+ - Form state should be explicit and predictable
301
+ - Native HTML first, adapters never
133
302
 
134
303
  ## Contributing
135
304
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@railway-ts/use-form",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "React form hook that works with @railway-ts/pipelines to manage form state, validation, and submission.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -70,7 +70,7 @@
70
70
  }
71
71
  ],
72
72
  "peerDependencies": {
73
- "@railway-ts/pipelines": "^0.1.12",
73
+ "@railway-ts/pipelines": "^0.1.13",
74
74
  "react": "^18.0.0 || ^19.0.0",
75
75
  "typescript": "^5"
76
76
  },