@maestro-js/form 1.0.0-alpha.0
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.
- package/README.md +386 -0
- package/dist/index.d.ts +183 -0
- package/dist/index.js +888 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# @maestro-js/form
|
|
2
|
+
|
|
3
|
+
Headless React form primitives with client and server validation, hidden inputs for custom components, controlled/uncontrolled state management, and Standard Schema (Zod) FormData parsing with automatic type coercion.
|
|
4
|
+
|
|
5
|
+
## Quick Setup
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { HeadlessForm } from '@maestro-js/form'
|
|
9
|
+
|
|
10
|
+
// 1. Wrap your app with GlobalProvider to enable server error integration
|
|
11
|
+
function App() {
|
|
12
|
+
return (
|
|
13
|
+
<HeadlessForm.GlobalProvider useServerErrors={useMyServerErrors}>
|
|
14
|
+
<MyPage />
|
|
15
|
+
</HeadlessForm.GlobalProvider>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 2. Use HeadlessForm as your form element
|
|
20
|
+
function LoginForm() {
|
|
21
|
+
return (
|
|
22
|
+
<HeadlessForm as="form" method="post" action="/login">
|
|
23
|
+
<input name="email" type="email" required />
|
|
24
|
+
<button type="submit">Log in</button>
|
|
25
|
+
</HeadlessForm>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Core Concepts
|
|
31
|
+
|
|
32
|
+
### Validation Flow
|
|
33
|
+
|
|
34
|
+
Custom validation via `validate` functions runs in realtime, setting `setCustomValidity()` on the referenced native element. Errors are committed (shown to the user) on input `change` or form `submit`. Server errors from `GlobalProvider` are shown immediately and cleared when the user edits the field. The first invalid input auto-focuses on submit.
|
|
35
|
+
|
|
36
|
+
### Hidden Inputs for Custom Components
|
|
37
|
+
|
|
38
|
+
Custom UI components (dropdowns, file pickers, date pickers) can't natively participate in HTML forms. `HiddenInput`, `HiddenFileInput`, and `HiddenSelect` bridge this gap with visually-hidden native elements that carry the value and participate in constraint validation.
|
|
39
|
+
|
|
40
|
+
### FormData Parsing
|
|
41
|
+
|
|
42
|
+
`parseFormData` and `parseFormDataClientSide` convert native FormData into typed objects using a Standard Schema (Zod). They handle type coercion (strings to numbers/booleans), nested objects via dot notation (`address.city`), arrays, and file uploads.
|
|
43
|
+
|
|
44
|
+
## API Reference
|
|
45
|
+
|
|
46
|
+
### HeadlessForm (Component)
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
<HeadlessForm as="form" method="post" id="profile-form">
|
|
50
|
+
{children}
|
|
51
|
+
</HeadlessForm>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Root form component. Wraps a native `<form>` with validation and server error support.
|
|
55
|
+
|
|
56
|
+
- `as` -- Component to render (must resolve to `HTMLFormElement`)
|
|
57
|
+
- `method` -- `'get'` | `'post'` | `'put'` | `'patch'` | `'delete'`
|
|
58
|
+
- `id` -- Optional (auto-generated via `useId()` if omitted)
|
|
59
|
+
- Plus all standard form element props
|
|
60
|
+
|
|
61
|
+
GET forms validate on submit and prevent submission if invalid. Non-GET forms inject a hidden `__formId` input for server-side form identification.
|
|
62
|
+
|
|
63
|
+
### GlobalProvider
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
<HeadlessForm.GlobalProvider useServerErrors={useMyServerErrors}>
|
|
67
|
+
{children}
|
|
68
|
+
</HeadlessForm.GlobalProvider>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Wraps your app to provide server error resolution to all forms. The `useServerErrors` prop is a hook with signature:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
(formId: string) => { path: string; message: string }[] | null
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### useValidation
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
HeadlessForm.useValidation<T>(
|
|
81
|
+
props: {
|
|
82
|
+
name: string | string[]
|
|
83
|
+
value: T
|
|
84
|
+
form: string
|
|
85
|
+
validate?: ValidationFunction<T>
|
|
86
|
+
isInvalid?: boolean
|
|
87
|
+
isRequired?: boolean
|
|
88
|
+
focus?: () => void
|
|
89
|
+
},
|
|
90
|
+
ref: RefObject<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
|
|
91
|
+
): {
|
|
92
|
+
isInvalid: boolean
|
|
93
|
+
validationErrors: string[]
|
|
94
|
+
validationDetails: ValidityState
|
|
95
|
+
validationMessage: string
|
|
96
|
+
commitValidation(): void
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Connects custom validation to the native Constraint Validation API. Use inside custom input components.
|
|
101
|
+
|
|
102
|
+
- Runs `validate(value)` in realtime, sets `setCustomValidity()` on the referenced element
|
|
103
|
+
- Commits errors on `change` event or form submit (`invalid` event)
|
|
104
|
+
- Auto-focuses the first invalid input on submission
|
|
105
|
+
- Integrates server errors from `GlobalProvider` -- clears when user edits
|
|
106
|
+
- Resets on form `reset` event
|
|
107
|
+
|
|
108
|
+
### useWarningState
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
HeadlessForm.useWarningState<T>(
|
|
112
|
+
props: { value: T; validate?: ValidationFunction<T>; isInvalid?: boolean }
|
|
113
|
+
): {
|
|
114
|
+
realtimeValidation: ValidationResult
|
|
115
|
+
displayValidation: ValidationResult
|
|
116
|
+
warningMessage: string
|
|
117
|
+
commitValidation(): void
|
|
118
|
+
resetValidation(): void
|
|
119
|
+
updateValidation(result: ValidationResult): void
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Like `useValidation` but for non-blocking warnings. Does not integrate with native constraint validation or server errors. Use for soft validation hints that don't prevent submission.
|
|
124
|
+
|
|
125
|
+
### useControlledState
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
HeadlessForm.useControlledState<T>(
|
|
129
|
+
value: T | undefined,
|
|
130
|
+
defaultValue: T,
|
|
131
|
+
onChange?: (v: T) => void
|
|
132
|
+
): [T, (value: T) => void]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Manages controlled/uncontrolled state for custom form components. When `value` is defined, the component is controlled; when `undefined`, it uses `defaultValue` and manages its own state.
|
|
136
|
+
|
|
137
|
+
### useReset
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
HeadlessForm.useReset(
|
|
141
|
+
ref: RefObject<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>,
|
|
142
|
+
initialValue: T,
|
|
143
|
+
onReset: (value: T) => void
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Calls `onReset` with the initial value when the parent form fires a `reset` event.
|
|
148
|
+
|
|
149
|
+
### useServerErrors
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
HeadlessForm.useServerErrors(formId?: string): { path: string; message: string }[]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Returns server errors for the given form ID (or the current form's ID from context if omitted). Requires `GlobalProvider` in the tree.
|
|
156
|
+
|
|
157
|
+
### useTopLevelFormValidation
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
HeadlessForm.useTopLevelFormValidation(
|
|
161
|
+
formElement: HTMLFormElement | string
|
|
162
|
+
): { revalidate(field: string | null): void }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Programmatically re-triggers validation on form fields. Pass `null` to revalidate all fields, or a field name prefix to revalidate only matching fields.
|
|
166
|
+
|
|
167
|
+
### composeValidators
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
HeadlessForm.composeValidators<T>(
|
|
171
|
+
...fns: (ValidationFunction<T> | undefined | null)[]
|
|
172
|
+
): ValidationFunction<T>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Combines multiple validation functions. Each is called with the value; all returned errors are collected into a single array.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const required = (v: string) => (!v ? 'Required' : null)
|
|
179
|
+
const minLen = (min: number) => (v: string) => (v.length < min ? `At least ${min} chars` : null)
|
|
180
|
+
|
|
181
|
+
const validate = HeadlessForm.composeValidators(required, minLen(5))
|
|
182
|
+
validate('') // ['Required', 'At least 5 chars']
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Hidden Input Components
|
|
186
|
+
|
|
187
|
+
#### HiddenInput
|
|
188
|
+
|
|
189
|
+
Visually hidden `<input>` that participates in form submission. Use inside custom components.
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
<HeadlessForm.HiddenInput ref={inputRef} name="country" value={selectedCountry} form={formId} isRequired />
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Props: `value`, `name`, `form`, `isRequired`, `type`, `min`, `max`
|
|
196
|
+
|
|
197
|
+
#### HiddenFileInput
|
|
198
|
+
|
|
199
|
+
Visually hidden `<input type="file">` with programmatic file assignment via `DataTransfer`.
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
<HeadlessForm.HiddenFileInput ref={fileRef} name="avatar" value={selectedFile} form={formId} isRequired />
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Props: `value` (`File | File[] | null`), `name`, `form`, `isRequired`
|
|
206
|
+
|
|
207
|
+
#### HiddenSelect
|
|
208
|
+
|
|
209
|
+
Visually hidden `<select>` for custom dropdown components.
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
<HeadlessForm.HiddenSelect ref={selectRef} name="role" value={selectedRole} form={formId} isRequired>
|
|
213
|
+
<option value="admin">Admin</option>
|
|
214
|
+
<option value="viewer">Viewer</option>
|
|
215
|
+
</HeadlessForm.HiddenSelect>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Props: `value`, `name`, `form`, `isRequired`, `children`, `autoComplete`, `onChange`, `label`
|
|
219
|
+
|
|
220
|
+
### FormData Parsing
|
|
221
|
+
|
|
222
|
+
#### parseFormData
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
HeadlessForm.parseFormData<T>(
|
|
226
|
+
schema: StandardSchema<T>,
|
|
227
|
+
formData: FormData | Promise<FormData>,
|
|
228
|
+
options?: {
|
|
229
|
+
handleFile?(file: File, path: string, data: T): Promise<string>
|
|
230
|
+
libraryOptions?: Record<string, unknown>
|
|
231
|
+
}
|
|
232
|
+
): Promise<ParseFormDataResult<T>>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Server-side FormData parser. Converts raw FormData into a typed object validated against a Standard Schema (Zod).
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
const schema = z.object({ name: z.string(), age: z.number(), avatar: z.string() })
|
|
239
|
+
|
|
240
|
+
const result = await HeadlessForm.parseFormData(schema, request.formData(), {
|
|
241
|
+
handleFile: async (file) => await uploadToS3(file)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
if (result.success) {
|
|
245
|
+
result.data // { name: 'Alice', age: 30, avatar: 'https://cdn.example.com/avatar.png' }
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### parseFormDataClientSide
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
HeadlessForm.parseFormDataClientSide<T>(
|
|
253
|
+
schema: StandardSchema<T>,
|
|
254
|
+
formData: FormData,
|
|
255
|
+
options?: ParseFormDataOptions<T>
|
|
256
|
+
): ParseFormDataResult<T> & { fileEntries: FoundFile[] }
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Synchronous client-side variant. Files are replaced with placeholder URLs for validation; actual file entries are returned separately in `fileEntries`.
|
|
260
|
+
|
|
261
|
+
### Coercion Rules
|
|
262
|
+
|
|
263
|
+
The parser uses the schema's JSON Schema representation to coerce FormData string values:
|
|
264
|
+
|
|
265
|
+
| Schema Type | FormData Value | Result |
|
|
266
|
+
| --- | --- | --- |
|
|
267
|
+
| `string` | `'Alice'` | `'Alice'` |
|
|
268
|
+
| `string` (nullable) | `''` | `null` |
|
|
269
|
+
| `number` / `integer` | `'42'` | `42` |
|
|
270
|
+
| `number` (empty) | `''` | `null` |
|
|
271
|
+
| `boolean` | `'true'` / `'false'` | `true` / `false` |
|
|
272
|
+
| `boolean` (absent) | *(missing)* | `false` |
|
|
273
|
+
| `array` | multiple values | `['a', 'b']` |
|
|
274
|
+
| `array` | JSON string `'[1,2]'` | `[1, 2]` |
|
|
275
|
+
| `array` (empty) | `''` | `[]` |
|
|
276
|
+
| nested object | `'addr.city'` key | `{ addr: { city: ... } }` |
|
|
277
|
+
| array of objects | `'items.0.name'` key | `[{ name: ... }]` |
|
|
278
|
+
|
|
279
|
+
### Types
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
type ValidationFunction<T> = (value: T) => string | string[] | null | undefined
|
|
283
|
+
|
|
284
|
+
interface ValidationResult {
|
|
285
|
+
isInvalid: boolean
|
|
286
|
+
validationDetails: ValidityState
|
|
287
|
+
validationErrors: string[]
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
type ParseFormDataResult<T> =
|
|
291
|
+
| { success: true; data: T; formData: FormData; formId: string | null; issues: undefined }
|
|
292
|
+
| { success: false; data: any; formData: FormData; formId: string | null; issues: readonly StandardSchemaV1.Issue[] }
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Common Patterns
|
|
296
|
+
|
|
297
|
+
### Custom Input with Validation
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
function PhoneInput({ name, form }: { name: string; form: string }) {
|
|
301
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
302
|
+
const [value, setValue] = HeadlessForm.useControlledState<string>(undefined, '')
|
|
303
|
+
|
|
304
|
+
const validation = HeadlessForm.useValidation(
|
|
305
|
+
{
|
|
306
|
+
name,
|
|
307
|
+
form,
|
|
308
|
+
value,
|
|
309
|
+
validate: (v) => (v && !/^\d{10}$/.test(v) ? 'Enter a 10-digit phone number' : null)
|
|
310
|
+
},
|
|
311
|
+
inputRef
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
HeadlessForm.useReset(inputRef, '', setValue)
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
<div>
|
|
318
|
+
<input value={value} onChange={(e) => setValue(e.target.value)} />
|
|
319
|
+
<HeadlessForm.HiddenInput ref={inputRef} name={name} value={value} form={form} />
|
|
320
|
+
{validation.isInvalid && <span>{validation.validationMessage}</span>}
|
|
321
|
+
</div>
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Server-Side Form Processing
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
async function handleProfileUpdate(formData: FormData) {
|
|
330
|
+
const schema = z.object({
|
|
331
|
+
name: z.string().min(1),
|
|
332
|
+
bio: z.string().nullable(),
|
|
333
|
+
age: z.number().min(13),
|
|
334
|
+
avatar: z.string(),
|
|
335
|
+
tags: z.array(z.string())
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
const result = await HeadlessForm.parseFormData(schema, formData, {
|
|
339
|
+
handleFile: async (file) => uploadToStorage(file)
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
if (!result.success) {
|
|
343
|
+
return { errors: result.issues.map((i) => ({ path: i.path?.join('.') ?? '', message: i.message })) }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
await db.updateUser(result.data)
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Custom Select with Hidden Native Element
|
|
351
|
+
|
|
352
|
+
```tsx
|
|
353
|
+
function RoleSelect({ name, form }: { name: string; form: string }) {
|
|
354
|
+
const selectRef = useRef<HTMLSelectElement>(null)
|
|
355
|
+
const [value, setValue] = HeadlessForm.useControlledState<string>(undefined, '')
|
|
356
|
+
|
|
357
|
+
const validation = HeadlessForm.useValidation({ name, form, value, isRequired: true }, selectRef)
|
|
358
|
+
HeadlessForm.useReset(selectRef, '', setValue)
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<div>
|
|
362
|
+
<CustomDropdown value={value} onChange={setValue} options={['admin', 'editor', 'viewer']} />
|
|
363
|
+
<HeadlessForm.HiddenSelect ref={selectRef} name={name} value={value} form={form} isRequired>
|
|
364
|
+
<option value="admin">Admin</option>
|
|
365
|
+
<option value="editor">Editor</option>
|
|
366
|
+
<option value="viewer">Viewer</option>
|
|
367
|
+
</HeadlessForm.HiddenSelect>
|
|
368
|
+
{validation.isInvalid && <span>{validation.validationMessage}</span>}
|
|
369
|
+
</div>
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## Warnings and Gotchas
|
|
375
|
+
|
|
376
|
+
> **Warning:** `GlobalProvider` must wrap your app before any `HeadlessForm` is rendered. Without it, `useServerErrors` throws: `'GlobalContext not found. Please wrap your site with GlobalProvider'`.
|
|
377
|
+
|
|
378
|
+
> **Warning:** Do not use async validation with `parseFormData` / `parseFormDataClientSide` -- they throw `'Async validation not supported'`. All schema validation must be synchronous.
|
|
379
|
+
|
|
380
|
+
> **Note:** Non-GET forms inject a hidden `__formId` field. `parseFormData` strips this automatically. If you parse FormData manually, filter out `__formId`.
|
|
381
|
+
|
|
382
|
+
> **Note:** `parseFormData` requires a `handleFile` option when the form contains file uploads. Without it, it throws. The client-side variant replaces files with placeholder URLs instead.
|
|
383
|
+
|
|
384
|
+
> **Warning:** Unchecked boolean checkboxes are absent from FormData. The parser defaults these to `false` based on the schema. If the boolean field isn't declared in the schema, the absent value won't be coerced.
|
|
385
|
+
|
|
386
|
+
> **Note:** `useControlledState` warns in the console if a component switches between controlled and uncontrolled mode. Always consistently pass either a value or `undefined`.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import * as React$1 from 'react';
|
|
2
|
+
import React__default from 'react';
|
|
3
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
|
+
import { StandardJSONSchemaV1, StandardSchemaV1, StandardTypedV1 } from '@standard-schema/spec';
|
|
5
|
+
|
|
6
|
+
type HeadlessFormProps<As extends React__default.ElementType> = {
|
|
7
|
+
id?: string;
|
|
8
|
+
children?: React__default.ReactNode;
|
|
9
|
+
as: RefType<As> extends HTMLFormElement ? As : never;
|
|
10
|
+
method?: 'get' | 'post' | 'put' | 'patch' | 'delete' | 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
11
|
+
} & React__default.ComponentProps<As>;
|
|
12
|
+
type RefType<T extends React__default.ElementType<any>> = React__default.ComponentPropsWithRef<T>['ref'] extends React__default.Ref<infer I> | undefined ? I : never;
|
|
13
|
+
declare function GlobalProvider({ useServerErrors, children, csrfToken }: GlobalContextValue): react_jsx_runtime.JSX.Element;
|
|
14
|
+
interface GlobalContextValue {
|
|
15
|
+
useServerErrors(formId: string): {
|
|
16
|
+
path: string;
|
|
17
|
+
message: string;
|
|
18
|
+
}[] | null;
|
|
19
|
+
csrfToken?: string;
|
|
20
|
+
children?: React__default.ReactNode;
|
|
21
|
+
}
|
|
22
|
+
declare function useServerErrors(formId: string | undefined): {
|
|
23
|
+
path: string;
|
|
24
|
+
message: string;
|
|
25
|
+
}[];
|
|
26
|
+
|
|
27
|
+
declare function useControlledState<T, C = T>(value: Exclude<T, undefined>, defaultValue: Exclude<T, undefined> | undefined, onChange?: (v: C, ...args: any[]) => void): [T, (value: T) => void];
|
|
28
|
+
declare function useControlledState<T, C = T>(value: Exclude<T, undefined> | undefined, defaultValue: Exclude<T, undefined>, onChange?: (v: C, ...args: any[]) => void): [T, (value: T) => void];
|
|
29
|
+
|
|
30
|
+
declare function useTopLevelFormValidation(formElement: HTMLFormElement | string): {
|
|
31
|
+
revalidate: (field: string | null) => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type ValidatableElement$1 = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
|
35
|
+
declare function useReset<T>(ref: React__default.RefObject<ValidatableElement$1 | null>, initialValue: T, onReset: (value: T) => void): void;
|
|
36
|
+
|
|
37
|
+
interface Validation<T> {
|
|
38
|
+
/** Whether user input is required on the input before form submission. */
|
|
39
|
+
isRequired?: boolean;
|
|
40
|
+
/** Whether the input value is invalid. */
|
|
41
|
+
isInvalid?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* A function that returns an error message if a given value is invalid.
|
|
44
|
+
* Validation errors are displayed to the user when the form is submitted
|
|
45
|
+
* if `validationBehavior="native"`. For realtime validation, use the `isInvalid`
|
|
46
|
+
* prop instead.
|
|
47
|
+
*/
|
|
48
|
+
validate?: ValidationFunction<T>;
|
|
49
|
+
}
|
|
50
|
+
interface ValidationResult {
|
|
51
|
+
isInvalid: boolean;
|
|
52
|
+
validationDetails: ValidityState;
|
|
53
|
+
validationErrors: string[];
|
|
54
|
+
}
|
|
55
|
+
type ValidationFunction<T> = (value: T) => string | string[] | null | undefined;
|
|
56
|
+
|
|
57
|
+
type ValidatableElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
|
58
|
+
interface FormValidationHookOptions<T> extends Validation<T> {
|
|
59
|
+
focus?: () => void;
|
|
60
|
+
name: string | string[] | undefined;
|
|
61
|
+
value: T;
|
|
62
|
+
form: string | undefined;
|
|
63
|
+
}
|
|
64
|
+
declare function useValidation<T>(props: FormValidationHookOptions<T>, ref: React__default.RefObject<ValidatableElement | null> | undefined): {
|
|
65
|
+
validationMessage: string;
|
|
66
|
+
isInvalid: boolean;
|
|
67
|
+
validationDetails: ValidityState;
|
|
68
|
+
validationErrors: string[];
|
|
69
|
+
commitValidation: () => void;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
interface FormValidationProps<T> extends Validation<T> {
|
|
73
|
+
value: T;
|
|
74
|
+
}
|
|
75
|
+
interface FormWarningState {
|
|
76
|
+
/** Realtime validation results, updated as the user edits the value. */
|
|
77
|
+
realtimeValidation: ValidationResult;
|
|
78
|
+
/** Currently displayed validation results, updated when the user commits their changes. */
|
|
79
|
+
displayValidation: ValidationResult;
|
|
80
|
+
/** Updates the current validation result. Not displayed to the user until `commitValidation` is called. */
|
|
81
|
+
updateValidation(result: ValidationResult): void;
|
|
82
|
+
/** Resets the displayed validation state to valid when the user resets the form. */
|
|
83
|
+
resetValidation(): void;
|
|
84
|
+
/** Commits the realtime validation so it is displayed to the user. */
|
|
85
|
+
commitValidation(): void;
|
|
86
|
+
warningMessage: string;
|
|
87
|
+
}
|
|
88
|
+
declare function useWarningState<T>(props: FormValidationProps<T>): FormWarningState;
|
|
89
|
+
|
|
90
|
+
type StandardSchema<Input = unknown, Output = Input> = StandardJSONSchemaV1<Input, Output> & StandardSchemaV1<Input, Output> & StandardTypedV1<Input, Output>;
|
|
91
|
+
type ParseFormDataResult<T> = {
|
|
92
|
+
success: false;
|
|
93
|
+
issues: {
|
|
94
|
+
/** The error message of the issue. */
|
|
95
|
+
message: string;
|
|
96
|
+
/** The path of the issue, if any. */
|
|
97
|
+
path?: Array<PropertyKey | {
|
|
98
|
+
key: string | number | symbol;
|
|
99
|
+
}> | undefined;
|
|
100
|
+
}[];
|
|
101
|
+
data: any;
|
|
102
|
+
formData: FormData;
|
|
103
|
+
formId: string | null;
|
|
104
|
+
} | {
|
|
105
|
+
success: true;
|
|
106
|
+
issues: undefined;
|
|
107
|
+
data: T;
|
|
108
|
+
formData: FormData;
|
|
109
|
+
formId: string | null;
|
|
110
|
+
};
|
|
111
|
+
interface ParseFormDataOptions<T> {
|
|
112
|
+
handleFile?(file: File, path: string, data: T): Promise<string>;
|
|
113
|
+
libraryOptions?: Record<string, unknown> | undefined;
|
|
114
|
+
}
|
|
115
|
+
declare function parseFormDataClientSide<T>(schema: StandardSchema<T>, formData: FormData, options?: ParseFormDataOptions<T>): ParseFormDataResult<T> & {
|
|
116
|
+
fileEntries: FoundFile[];
|
|
117
|
+
};
|
|
118
|
+
declare function parseFormData<T>(schema: StandardSchema<T>, formData: FormData | Promise<FormData>, options?: ParseFormDataOptions<T>): Promise<ParseFormDataResult<T>>;
|
|
119
|
+
interface FoundFile {
|
|
120
|
+
path: (string | number)[];
|
|
121
|
+
value: File;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
declare function composeValidators<T>(...fns: (ValidationFunction<T> | undefined | null)[]): ValidationFunction<T>;
|
|
125
|
+
/**
|
|
126
|
+
* Headless React form primitives with validation, hidden inputs, and FormData parsing.
|
|
127
|
+
* Wrap your app with `HeadlessForm.GlobalProvider`, use `HeadlessForm` as the form root,
|
|
128
|
+
* and build custom inputs with `useValidation`, `useControlledState`, and hidden input components.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```tsx
|
|
132
|
+
* <HeadlessForm.GlobalProvider useServerErrors={useMyServerErrors}>
|
|
133
|
+
* <HeadlessForm as="form" method="post">
|
|
134
|
+
* <PhoneInput name="phone" form={formId} />
|
|
135
|
+
* <button type="submit">Submit</button>
|
|
136
|
+
* </HeadlessForm>
|
|
137
|
+
* </HeadlessForm.GlobalProvider>
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
declare const HeadlessForm: (<As extends React.ElementType = "form">(props: {
|
|
141
|
+
id?: string;
|
|
142
|
+
children?: React.ReactNode;
|
|
143
|
+
as: (React$1.ComponentPropsWithRef<As>["ref"] extends React$1.Ref<infer I> | undefined ? I : never) extends HTMLFormElement ? As : never;
|
|
144
|
+
method?: "get" | "post" | "put" | "patch" | "delete" | "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
145
|
+
} & React$1.ComponentProps<As> & React$1.RefAttributes<HTMLFormElement>) => React.ReactElement | null) & {
|
|
146
|
+
GlobalProvider: typeof GlobalProvider;
|
|
147
|
+
useValidation: typeof useValidation;
|
|
148
|
+
useWarningState: typeof useWarningState;
|
|
149
|
+
useReset: typeof useReset;
|
|
150
|
+
useControlledState: typeof useControlledState;
|
|
151
|
+
HiddenInput: React$1.ForwardRefExoticComponent<{
|
|
152
|
+
value: string | number | readonly string[] | undefined;
|
|
153
|
+
name: string | undefined;
|
|
154
|
+
form: string | undefined;
|
|
155
|
+
isRequired?: boolean;
|
|
156
|
+
min?: number;
|
|
157
|
+
max?: number;
|
|
158
|
+
type?: React$1.HTMLInputTypeAttribute;
|
|
159
|
+
} & React$1.RefAttributes<HTMLInputElement>>;
|
|
160
|
+
HiddenFileInput: React$1.ForwardRefExoticComponent<{
|
|
161
|
+
value: File | File[] | null | undefined;
|
|
162
|
+
name: string | undefined;
|
|
163
|
+
form?: string | undefined;
|
|
164
|
+
isRequired?: boolean;
|
|
165
|
+
} & React$1.RefAttributes<HTMLInputElement>>;
|
|
166
|
+
HiddenSelect: React$1.ForwardRefExoticComponent<{
|
|
167
|
+
value: string | number | readonly string[] | undefined;
|
|
168
|
+
name: string | undefined;
|
|
169
|
+
form: string | undefined;
|
|
170
|
+
isRequired?: boolean;
|
|
171
|
+
children?: React.ReactNode;
|
|
172
|
+
autoComplete?: string;
|
|
173
|
+
onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => unknown;
|
|
174
|
+
label?: React.ReactNode;
|
|
175
|
+
} & React$1.RefAttributes<HTMLSelectElement>>;
|
|
176
|
+
useServerErrors: typeof useServerErrors;
|
|
177
|
+
parseFormDataClientSide: typeof parseFormDataClientSide;
|
|
178
|
+
parseFormData: typeof parseFormData;
|
|
179
|
+
useTopLevelFormValidation: typeof useTopLevelFormValidation;
|
|
180
|
+
composeValidators: typeof composeValidators;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export { HeadlessForm, type HeadlessFormProps, type ValidationFunction, type ValidationResult };
|