attaform 0.0.1 → 0.14.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/LICENSE +21 -0
- package/README.md +142 -2
- package/dist/chunks/devtools.cjs +179 -0
- package/dist/chunks/devtools.cjs.map +1 -0
- package/dist/chunks/devtools.mjs +177 -0
- package/dist/chunks/devtools.mjs.map +1 -0
- package/dist/chunks/indexeddb.cjs +119 -0
- package/dist/chunks/indexeddb.cjs.map +1 -0
- package/dist/chunks/indexeddb.mjs +117 -0
- package/dist/chunks/indexeddb.mjs.map +1 -0
- package/dist/chunks/local-storage.cjs +58 -0
- package/dist/chunks/local-storage.cjs.map +1 -0
- package/dist/chunks/local-storage.mjs +56 -0
- package/dist/chunks/local-storage.mjs.map +1 -0
- package/dist/chunks/session-storage.cjs +58 -0
- package/dist/chunks/session-storage.cjs.map +1 -0
- package/dist/chunks/session-storage.mjs +56 -0
- package/dist/chunks/session-storage.mjs.map +1 -0
- package/dist/index.cjs +173 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +493 -0
- package/dist/index.d.mts +493 -0
- package/dist/index.d.ts +493 -0
- package/dist/index.mjs +141 -0
- package/dist/index.mjs.map +1 -0
- package/dist/nuxt.cjs +97 -0
- package/dist/nuxt.cjs.map +1 -0
- package/dist/nuxt.d.cts +38 -0
- package/dist/nuxt.d.mts +38 -0
- package/dist/nuxt.d.ts +38 -0
- package/dist/nuxt.mjs +94 -0
- package/dist/nuxt.mjs.map +1 -0
- package/dist/runtime/plugins/attaform.cjs +32 -0
- package/dist/runtime/plugins/attaform.cjs.map +1 -0
- package/dist/runtime/plugins/attaform.d.cts +5 -0
- package/dist/runtime/plugins/attaform.d.mts +5 -0
- package/dist/runtime/plugins/attaform.d.ts +5 -0
- package/dist/runtime/plugins/attaform.mjs +30 -0
- package/dist/runtime/plugins/attaform.mjs.map +1 -0
- package/dist/shared/attaform.B5GWYl76.cjs +386 -0
- package/dist/shared/attaform.B5GWYl76.cjs.map +1 -0
- package/dist/shared/attaform.BRTxpA3q.mjs +3283 -0
- package/dist/shared/attaform.BRTxpA3q.mjs.map +1 -0
- package/dist/shared/attaform.BYc9kugA.d.ts +124 -0
- package/dist/shared/attaform.Bubm_slq.cjs +622 -0
- package/dist/shared/attaform.Bubm_slq.cjs.map +1 -0
- package/dist/shared/attaform.BwaYWtMs.d.cts +126 -0
- package/dist/shared/attaform.BwaYWtMs.d.mts +126 -0
- package/dist/shared/attaform.BwaYWtMs.d.ts +126 -0
- package/dist/shared/attaform.CNJO3mME.cjs +3295 -0
- package/dist/shared/attaform.CNJO3mME.cjs.map +1 -0
- package/dist/shared/attaform.CRgix6_n.cjs +796 -0
- package/dist/shared/attaform.CRgix6_n.cjs.map +1 -0
- package/dist/shared/attaform.CXZgUECn.d.cts +124 -0
- package/dist/shared/attaform.CXpzmj38.mjs +617 -0
- package/dist/shared/attaform.CXpzmj38.mjs.map +1 -0
- package/dist/shared/attaform.Cc93zNzD.mjs +83 -0
- package/dist/shared/attaform.Cc93zNzD.mjs.map +1 -0
- package/dist/shared/attaform.DDXrY-1Q.d.cts +2568 -0
- package/dist/shared/attaform.DDXrY-1Q.d.mts +2568 -0
- package/dist/shared/attaform.DDXrY-1Q.d.ts +2568 -0
- package/dist/shared/attaform.DOKOyb3Y.d.mts +124 -0
- package/dist/shared/attaform.DlgKK10S.mjs +789 -0
- package/dist/shared/attaform.DlgKK10S.mjs.map +1 -0
- package/dist/shared/attaform.al_rpt7_.mjs +361 -0
- package/dist/shared/attaform.al_rpt7_.mjs.map +1 -0
- package/dist/shared/attaform.xKWYHMdq.cjs +89 -0
- package/dist/shared/attaform.xKWYHMdq.cjs.map +1 -0
- package/dist/transforms.cjs +11 -0
- package/dist/transforms.cjs.map +1 -0
- package/dist/transforms.d.cts +49 -0
- package/dist/transforms.d.mts +49 -0
- package/dist/transforms.d.ts +49 -0
- package/dist/transforms.mjs +2 -0
- package/dist/transforms.mjs.map +1 -0
- package/dist/vite.cjs +39 -0
- package/dist/vite.cjs.map +1 -0
- package/dist/vite.d.cts +53 -0
- package/dist/vite.d.mts +53 -0
- package/dist/vite.d.ts +53 -0
- package/dist/vite.mjs +37 -0
- package/dist/vite.mjs.map +1 -0
- package/dist/zod-v3.cjs +1511 -0
- package/dist/zod-v3.cjs.map +1 -0
- package/dist/zod-v3.d.cts +164 -0
- package/dist/zod-v3.d.mts +164 -0
- package/dist/zod-v3.d.ts +164 -0
- package/dist/zod-v3.mjs +1504 -0
- package/dist/zod-v3.mjs.map +1 -0
- package/dist/zod.cjs +1548 -0
- package/dist/zod.cjs.map +1 -0
- package/dist/zod.d.cts +67 -0
- package/dist/zod.d.mts +67 -0
- package/dist/zod.d.ts +67 -0
- package/dist/zod.mjs +1541 -0
- package/dist/zod.mjs.map +1 -0
- package/package.json +182 -6
|
@@ -0,0 +1,2568 @@
|
|
|
1
|
+
import { Ref, ObjectDirective, ComputedRef } from 'vue';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Path primitives for advanced integrations. The form library accepts
|
|
5
|
+
* paths in dotted-string form (`'user.email'`) at every public API.
|
|
6
|
+
* These primitives are exposed for adapter authors who need to
|
|
7
|
+
* canonicalise user-provided paths.
|
|
8
|
+
*/
|
|
9
|
+
declare const pathKeyBrand: unique symbol;
|
|
10
|
+
/**
|
|
11
|
+
* Branded string identifier for a canonicalised path. Useful as a
|
|
12
|
+
* `Map` key — two paths that resolve to the same canonical form
|
|
13
|
+
* produce the same `PathKey`. Treat as opaque; don't try to parse.
|
|
14
|
+
*/
|
|
15
|
+
type PathKey = string & {
|
|
16
|
+
readonly [pathKeyBrand]: 'PathKey';
|
|
17
|
+
};
|
|
18
|
+
/** A single path segment — a property name or array index. */
|
|
19
|
+
type Segment = string | number;
|
|
20
|
+
/** A structured path as a read-only sequence of segments. */
|
|
21
|
+
type Path = readonly Segment[];
|
|
22
|
+
/**
|
|
23
|
+
* Parse a dotted-string path into structured segments.
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* parseDottedPath('user.address.line1') // ['user', 'address', 'line1']
|
|
27
|
+
* parseDottedPath('items.0.name') // ['items', 0, 'name']
|
|
28
|
+
* parseDottedPath('') // [] (root)
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* Throws `InvalidPathError` for paths with empty segments
|
|
32
|
+
* (`'a..b'`, leading or trailing dots). For keys containing literal
|
|
33
|
+
* dots, pass an array form (`['user.name']`) instead.
|
|
34
|
+
*/
|
|
35
|
+
declare function parseDottedPath(path: string): Segment[];
|
|
36
|
+
/**
|
|
37
|
+
* Canonicalise a path into structured segments plus a stable string
|
|
38
|
+
* key. Accepts either dotted-string or array form; integer-looking
|
|
39
|
+
* segments normalise to numbers.
|
|
40
|
+
*
|
|
41
|
+
* ```ts
|
|
42
|
+
* canonicalizePath('items.0.name')
|
|
43
|
+
* // { segments: ['items', 0, 'name'], key: '["items",0,"name"]' as PathKey }
|
|
44
|
+
*
|
|
45
|
+
* canonicalizePath(['items', 0, 'name'])
|
|
46
|
+
* // → same result
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* The returned `key` is suitable as a `Map`/`Set` key — equal paths
|
|
50
|
+
* produce equal keys regardless of input form.
|
|
51
|
+
*/
|
|
52
|
+
declare function canonicalizePath(input: string | Path): {
|
|
53
|
+
segments: readonly Segment[];
|
|
54
|
+
key: PathKey;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* The root path — an empty segment tuple. Pass to APIs that accept
|
|
58
|
+
* a `Path` to address the form value as a whole.
|
|
59
|
+
*/
|
|
60
|
+
declare const ROOT_PATH: Path;
|
|
61
|
+
/** Stable string key for the root path. */
|
|
62
|
+
declare const ROOT_PATH_KEY: PathKey;
|
|
63
|
+
|
|
64
|
+
/** Internal brand for the `Unset` type. Never exposed at runtime. */
|
|
65
|
+
declare const _unsetBrand: unique symbol;
|
|
66
|
+
/**
|
|
67
|
+
* Brand-typed sentinel admitted at every primitive leaf of
|
|
68
|
+
* `DefaultValuesShape<T>`, `setValue`, and `reset`. The runtime value
|
|
69
|
+
* is exported as {@link unset} under the same name.
|
|
70
|
+
*/
|
|
71
|
+
type Unset = typeof _unsetBrand;
|
|
72
|
+
/**
|
|
73
|
+
* The `unset` sentinel — pass it as a primitive leaf's value to mark
|
|
74
|
+
* the field **displayed-empty** while storage holds the schema's slim
|
|
75
|
+
* default (`0` / `''` / `false` / `0n`).
|
|
76
|
+
*
|
|
77
|
+
* Use it wherever a primitive leaf value is expected:
|
|
78
|
+
*
|
|
79
|
+
* ```ts
|
|
80
|
+
* const form = useForm({
|
|
81
|
+
* schema: z.object({ income: z.number() }),
|
|
82
|
+
* defaultValues: { income: unset }, // UI starts blank, storage holds 0
|
|
83
|
+
* key: 'housing',
|
|
84
|
+
* })
|
|
85
|
+
*
|
|
86
|
+
* form.setValue('income', unset) // re-blank a field after a write
|
|
87
|
+
* form.reset({ income: unset }) // reset to the blank state
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* Accepted at any `string` / `number` / `boolean` / `bigint` leaf in
|
|
91
|
+
* `defaultValues`, `setValue(path, unset)`, and `form.reset({ … })`.
|
|
92
|
+
*
|
|
93
|
+
* The path joins the form's `blankPaths` set as long as it stays
|
|
94
|
+
* unset. Required schemas (no `.optional()` / `.nullable()` /
|
|
95
|
+
* `.default(N)`) raise `"No value supplied"` on submit while a leaf
|
|
96
|
+
* is in `blankPaths`; optional / nullable / has-default schemas
|
|
97
|
+
* accept the empty case as their wrapper allows.
|
|
98
|
+
*
|
|
99
|
+
* Storage never holds the symbol — the runtime translates it at the
|
|
100
|
+
* API boundary, so reads through `form.values` always see the slim
|
|
101
|
+
* default. Cross-bundle / SSR-safe: backed by `Symbol.for(...)` so
|
|
102
|
+
* every realm gets the same sentinel.
|
|
103
|
+
*
|
|
104
|
+
* @see {@link isUnset} — type guard that narrows a value back to {@link Unset}.
|
|
105
|
+
* @see `docs/blank.md` — the conceptual model behind blank-aware fields.
|
|
106
|
+
*/
|
|
107
|
+
declare const unset: Unset;
|
|
108
|
+
/**
|
|
109
|
+
* Type guard — `true` when `value` is the `unset` sentinel.
|
|
110
|
+
*
|
|
111
|
+
* ```ts
|
|
112
|
+
* if (isUnset(payload.income)) {
|
|
113
|
+
* // payload.income is the sentinel; the field will display empty
|
|
114
|
+
* }
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
declare function isUnset(value: unknown): value is Unset;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The minimum shape any form value satisfies — a plain record. Use
|
|
121
|
+
* as a constraint for composables that work generically across forms
|
|
122
|
+
* (e.g. a custom hook that takes any form's `useForm` return).
|
|
123
|
+
*/
|
|
124
|
+
type GenericForm = Record<string, unknown>;
|
|
125
|
+
/** Internal helper — `true` when `T` is an object or array. */
|
|
126
|
+
type IsObjectOrArray<T> = T extends GenericForm ? true : T extends Array<unknown> ? true : false;
|
|
127
|
+
type PartialFlatPath<Form, Key extends keyof Form = keyof Form> = IsObjectOrArray<Form> extends true ? Key extends string ? Form[Key] extends infer Value ? Value extends Array<infer ArrayItem> ? `${Key}` | `${Key}.${number}` | `${Key}.${number}.${PartialFlatPath<ArrayItem>}` : Value extends GenericForm ? `${Key}` | `${Key}.${PartialFlatPath<Value>}` : `${Key}` : never : Key extends number ? `${Key}` | (Form[Key] extends GenericForm ? `${Key}.${PartialFlatPath<Form[Key]>}` : Form[Key] extends Array<infer ArrayItem> ? IsObjectOrArray<ArrayItem> extends true ? `${Key}.${number}` | `${Key}.${number}.${PartialFlatPath<ArrayItem>}` : `${Key}.${number}` : never) : never : never;
|
|
128
|
+
type CompleteFlatPath<Form, Key extends keyof Form = keyof Form> = IsObjectOrArray<Form> extends true ? Key extends string ? Form[Key] extends infer Value ? Value extends Array<infer ArrayItem> ? `${Key}.${number}.${CompleteFlatPath<ArrayItem>}` : Value extends GenericForm ? `${Key}.${CompleteFlatPath<Value>}` : `${Key}` : never : Key extends number ? `${Key}` | (Form[Key] extends GenericForm ? `${Key}.${CompleteFlatPath<Form[Key]>}` : Form[Key] extends Array<infer ArrayItem> ? IsObjectOrArray<ArrayItem> extends true ? `${Key}.${number}.${CompleteFlatPath<ArrayItem>}` : `${Key}.${number}` : never) : never : never;
|
|
129
|
+
/**
|
|
130
|
+
* Union of dotted-string paths reachable inside `Form`, e.g. for
|
|
131
|
+
* `{ user: { email: string }, items: string[] }`:
|
|
132
|
+
*
|
|
133
|
+
* `'user' | 'user.email' | 'items' | 'items.0' | 'items.1' | …`
|
|
134
|
+
*
|
|
135
|
+
* Used by every path-addressed API (`setValue(path, value)`,
|
|
136
|
+
* `register(path)`, `toRef(path)`, etc.) so paths autocomplete in
|
|
137
|
+
* the IDE and typos compile-error.
|
|
138
|
+
*
|
|
139
|
+
* Set `ForceFullPath` to `true` to restrict to leaf paths only
|
|
140
|
+
* (no intermediate container paths).
|
|
141
|
+
*/
|
|
142
|
+
type FlatPath<Form, Key extends keyof Form = keyof Form, ForceFullPath extends boolean = false> = ForceFullPath extends true ? CompleteFlatPath<Form, Key> : PartialFlatPath<Form, Key>;
|
|
143
|
+
/**
|
|
144
|
+
* Recursive `Partial` — every property at every depth is optional.
|
|
145
|
+
* Used as the parameter type of `defaultValues` and `reset()` so
|
|
146
|
+
* partial overrides at any nesting level are valid.
|
|
147
|
+
*/
|
|
148
|
+
type DeepPartial<T> = T extends Primitive ? T : T extends Array<infer ArrayItem> ? DeepPartial<ArrayItem>[] : T extends object ? {
|
|
149
|
+
[Key in keyof T]?: DeepPartial<T[Key]>;
|
|
150
|
+
} : T;
|
|
151
|
+
/**
|
|
152
|
+
* Resolve the type at a dotted-string path inside `RootValue`. Used
|
|
153
|
+
* by the strict (write-side) APIs to derive the type at a path:
|
|
154
|
+
*
|
|
155
|
+
* `NestedType<{ user: { email: string } }, 'user.email'>` → `string`
|
|
156
|
+
*
|
|
157
|
+
* TypeScript caps conditional-type recursion at around 50 levels;
|
|
158
|
+
* paths deeper than that resolve to `never`. Real form schemas
|
|
159
|
+
* never reach this depth.
|
|
160
|
+
*/
|
|
161
|
+
type NestedType<RootValue, FlattenedPath extends string, FilterOutNullishTypesDuringRecursion extends boolean = true, _RootValue = FilterOutNullishTypesDuringRecursion extends false ? RootValue : NonNullable<RootValue>> = IsObjectOrArray<_RootValue> extends false ? never : FlattenedPath extends `${infer Key}.${infer Rest}` ? Key extends `${number}` ? Key extends keyof _RootValue ? NestedType<_RootValue[Key], Rest, FilterOutNullishTypesDuringRecursion> : Key extends `${infer NumericKey extends number}` ? NumericKey extends keyof _RootValue ? NestedType<_RootValue[NumericKey], Rest, FilterOutNullishTypesDuringRecursion> : never : never : Key extends keyof _RootValue ? NestedType<_RootValue[Key], Rest, FilterOutNullishTypesDuringRecursion> : never : FlattenedPath extends `${number}` ? FlattenedPath extends keyof _RootValue ? _RootValue[FlattenedPath] : FlattenedPath extends `${infer NumericKey extends number}` ? NumericKey extends keyof _RootValue ? _RootValue[NumericKey] : never : never : FlattenedPath extends keyof _RootValue ? _RootValue[FlattenedPath] : never;
|
|
162
|
+
type Primitive = string | number | boolean | symbol | bigint | null | undefined;
|
|
163
|
+
/**
|
|
164
|
+
* Distinguish a tuple from a regular array.
|
|
165
|
+
*
|
|
166
|
+
* `IsTuple<[string, number]>` → `true`
|
|
167
|
+
* `IsTuple<string[]>` → `false`
|
|
168
|
+
*
|
|
169
|
+
* Useful for write-side helpers that need to preserve tuple
|
|
170
|
+
* positions instead of widening to `Array<element>`.
|
|
171
|
+
*/
|
|
172
|
+
type IsTuple<T extends readonly unknown[]> = number extends T['length'] ? false : true;
|
|
173
|
+
/**
|
|
174
|
+
* Path-resolved type for read-side APIs. Like `NestedType`, but once
|
|
175
|
+
* the walk crosses an array index segment the resulting type is
|
|
176
|
+
* tagged `| undefined` (the runtime can return undefined for
|
|
177
|
+
* out-of-bounds reads).
|
|
178
|
+
*
|
|
179
|
+
* Used by `form.values.<path>` reads, `form.toRef(path)`, and
|
|
180
|
+
* `register(path).innerRef` so the compile-time type honours the
|
|
181
|
+
* runtime possibility of a missing array position.
|
|
182
|
+
*/
|
|
183
|
+
type NestedReadType<RootValue, FlattenedPath extends string, _Tainted extends boolean = false, _RootValue = NonNullable<RootValue>> = IsObjectOrArray<_RootValue> extends false ? never : FlattenedPath extends `${infer Key}.${infer Rest}` ? Key extends `${number}` ? Key extends keyof _RootValue ? NestedReadType<_RootValue[Key], Rest, true> : Key extends `${infer NumericKey extends number}` ? NumericKey extends keyof _RootValue ? NestedReadType<_RootValue[NumericKey], Rest, true> : never : never : Key extends keyof _RootValue ? NestedReadType<_RootValue[Key], Rest, _Tainted> : never : FlattenedPath extends `${number}` ? FlattenedPath extends keyof _RootValue ? _RootValue[FlattenedPath] | undefined : FlattenedPath extends `${infer NumericKey extends number}` ? NumericKey extends keyof _RootValue ? _RootValue[NumericKey] | undefined : never : never : FlattenedPath extends keyof _RootValue ? _Tainted extends true ? _RootValue[FlattenedPath] | undefined : _RootValue[FlattenedPath] : never;
|
|
184
|
+
/**
|
|
185
|
+
* Filter FlatPath<Form> down to the subset of paths whose resolved leaf
|
|
186
|
+
* is an array. Used by the typed field-array helpers (append / remove /
|
|
187
|
+
* swap / ...) so those helpers only accept paths that actually address
|
|
188
|
+
* an array — calling `append('email', ...)` on a `{ email: string }`
|
|
189
|
+
* is a compile error.
|
|
190
|
+
*
|
|
191
|
+
* `P extends string` re-triggers distribution over the `FlatPath<Form>`
|
|
192
|
+
* union so the conditional evaluates per member. Without it, the
|
|
193
|
+
* branch would reduce against the union as a whole and collapse to
|
|
194
|
+
* `never` whenever a single member failed the predicate.
|
|
195
|
+
*/
|
|
196
|
+
type ArrayPath<Form, P extends FlatPath<Form> = FlatPath<Form>> = P extends string ? NestedType<Form, P> extends readonly unknown[] ? P : never : never;
|
|
197
|
+
/**
|
|
198
|
+
* Extract the element type of the array addressed by `Path`. Callers
|
|
199
|
+
* constrain `Path extends ArrayPath<Form>` so this is always well-defined.
|
|
200
|
+
*/
|
|
201
|
+
type ArrayItem<Form, Path extends ArrayPath<Form>> = NestedType<Form, Path> extends ReadonlyArray<infer Item> ? Item : never;
|
|
202
|
+
/**
|
|
203
|
+
* Widens primitive-literal leaves to their primitive supertype to
|
|
204
|
+
* match the runtime "slim-primitive write contract."
|
|
205
|
+
*
|
|
206
|
+
* WriteShape<{ color: 'red' | 'green' }>
|
|
207
|
+
* // → { color: string }
|
|
208
|
+
* WriteShape<{ kind: 'on' }>
|
|
209
|
+
* // → { kind: string }
|
|
210
|
+
* WriteShape<{ count: 42 }>
|
|
211
|
+
* // → { count: number }
|
|
212
|
+
*
|
|
213
|
+
* The runtime gate accepts any value at a path whose primitive type
|
|
214
|
+
* matches the schema's slim primitive set at that path. Refinement-
|
|
215
|
+
* level constraints (enum membership, literal equality, format
|
|
216
|
+
* checks, length / range bounds, regex, custom predicates) are NOT
|
|
217
|
+
* enforced at write time — they surface via field-level validation.
|
|
218
|
+
* The type widening here mirrors that runtime behaviour, so
|
|
219
|
+
* `setValue('color', 'magenta')` and `defaultValues: { color: 'teal' }`
|
|
220
|
+
* are not TS errors despite being out-of-enum at the validation
|
|
221
|
+
* layer.
|
|
222
|
+
*
|
|
223
|
+
* Tuple positions preserve their literal types via the homomorphic
|
|
224
|
+
* mapped form (`{ [K in keyof T]: ... }` over a readonly tuple
|
|
225
|
+
* preserves the position labels), so `[string, number]` stays a
|
|
226
|
+
* 2-tuple of widened primitives instead of collapsing to
|
|
227
|
+
* `Array<string | number>`.
|
|
228
|
+
*
|
|
229
|
+
* Date / RegExp / Map / Set / function instances pass through
|
|
230
|
+
* unchanged — those aren't "primitive literals" and the runtime
|
|
231
|
+
* accepts them as their own slim kinds. Tuple-detection runs before
|
|
232
|
+
* the array-recursion branch so positionally-typed array literals
|
|
233
|
+
* survive intact.
|
|
234
|
+
*
|
|
235
|
+
* Read-side types (handleSubmit's `data` argument,
|
|
236
|
+
* validate*() result payloads) intentionally stay STRICT — those
|
|
237
|
+
* payloads have been parsed by the schema, so the widened shape
|
|
238
|
+
* doesn't apply.
|
|
239
|
+
*/
|
|
240
|
+
type WriteShape<T> = T extends string | number | boolean | bigint | symbol | null | undefined ? T extends string ? string : T extends number ? number : T extends boolean ? boolean : T extends bigint ? bigint : T extends symbol ? symbol : T : T extends Date | RegExp | Map<unknown, unknown> | Set<unknown> | ((...args: never) => unknown) ? T : T extends readonly [unknown, ...unknown[]] ? {
|
|
241
|
+
-readonly [K in keyof T]: WriteShape<T[K]>;
|
|
242
|
+
} : T extends ReadonlyArray<infer U> ? IsTuple<T> extends true ? {
|
|
243
|
+
-readonly [K in keyof T]: WriteShape<T[K]>;
|
|
244
|
+
} : Array<WriteShape<U>> : T extends object ? {
|
|
245
|
+
[K in keyof T]: WriteShape<T[K]>;
|
|
246
|
+
} : T;
|
|
247
|
+
/**
|
|
248
|
+
* Like `WriteShape<T>`, but additionally widens every primitive leaf
|
|
249
|
+
* (`string`, `number`, `boolean`, `bigint`) to admit `Unset` — the
|
|
250
|
+
* brand-typed sentinel consumers pass to indicate "this leaf starts
|
|
251
|
+
* displayed-empty" in `defaultValues`, `setValue`, and `reset`.
|
|
252
|
+
*
|
|
253
|
+
* Non-primitive leaves (`Date`, `RegExp`, `Map`, `Set`, functions)
|
|
254
|
+
* stay strict — `defaultValues: { joinedAt: unset }` against a
|
|
255
|
+
* `Date`-typed leaf is a type error.
|
|
256
|
+
*
|
|
257
|
+
* The recursion mirrors `WriteShape<T>` exactly so `defaultValues`
|
|
258
|
+
* stays compatible at every nested position; the only divergence is
|
|
259
|
+
* the leaf widening. Tuple positions, unbounded arrays, and nested
|
|
260
|
+
* records all flow through unchanged.
|
|
261
|
+
*
|
|
262
|
+
* Example:
|
|
263
|
+
*
|
|
264
|
+
* DefaultValuesShape<{ income: number; name: string; age: 21 }>
|
|
265
|
+
* // → { income: number | Unset; name: string | Unset; age: number | Unset }
|
|
266
|
+
*
|
|
267
|
+
* Used by `UseFormConfiguration.defaultValues`, `setValue`'s value
|
|
268
|
+
* parameter, and `reset`'s parameter (commit 7 widens all three).
|
|
269
|
+
*/
|
|
270
|
+
type DefaultValuesShape<T> = T extends string | number | boolean | bigint | symbol | null | undefined ? T extends string ? string | Unset : T extends number ? number | Unset : T extends boolean ? boolean | Unset : T extends bigint ? bigint | Unset : T extends symbol ? symbol : T : T extends Date | RegExp | Map<unknown, unknown> | Set<unknown> | ((...args: never) => unknown) ? T : T extends readonly [unknown, ...unknown[]] ? {
|
|
271
|
+
-readonly [K in keyof T]: DefaultValuesShape<T[K]>;
|
|
272
|
+
} : T extends ReadonlyArray<infer U> ? IsTuple<T> extends true ? {
|
|
273
|
+
-readonly [K in keyof T]: DefaultValuesShape<T[K]>;
|
|
274
|
+
} : Array<DefaultValuesShape<U>> : T extends object ? {
|
|
275
|
+
[K in keyof T]: DefaultValuesShape<T[K]>;
|
|
276
|
+
} : T;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Identifier for a form. A `FormKey` is the string passed via
|
|
280
|
+
* `useForm({ key })`, used to look up a form by name from a distant
|
|
281
|
+
* component, namespace persisted drafts, and label errors and
|
|
282
|
+
* DevTools entries. Anonymous `useForm` calls allocate one
|
|
283
|
+
* automatically; you only need to pick one when the form needs
|
|
284
|
+
* stable identity.
|
|
285
|
+
*/
|
|
286
|
+
type FormKey = string;
|
|
287
|
+
/**
|
|
288
|
+
* One validation failure. `path` points at the offending field as a
|
|
289
|
+
* structured array — `['user', 'address', 0, 'line1']` for a nested
|
|
290
|
+
* field, `[]` for a form-level error. `formKey` identifies which
|
|
291
|
+
* form produced the error so a single error list can be routed to
|
|
292
|
+
* multiple forms.
|
|
293
|
+
*
|
|
294
|
+
* Returned by `validate()` / `validateAsync()` / `handleSubmit`'s
|
|
295
|
+
* `onError` callback, and by `parseApiErrors` for server responses.
|
|
296
|
+
*/
|
|
297
|
+
type ValidationError = {
|
|
298
|
+
/** Human-readable message describing the failure. */
|
|
299
|
+
message: string;
|
|
300
|
+
/** Structured path of the offending field. Empty array means a form-level error. */
|
|
301
|
+
path: (string | number)[];
|
|
302
|
+
/** Identifies which form produced this error. */
|
|
303
|
+
formKey: FormKey;
|
|
304
|
+
/**
|
|
305
|
+
* Stable machine identifier for the failure, scoped by prefix:
|
|
306
|
+
*
|
|
307
|
+
* - `atta:` — library-internal codes (see `AttaformErrorCode`).
|
|
308
|
+
* - adapter prefix (e.g. `zod:`) — forwarded from the underlying
|
|
309
|
+
* schema library's own issue code, when one exists.
|
|
310
|
+
* - consumer-defined — anything else (e.g. `api:duplicate-email`,
|
|
311
|
+
* `auth:expired-token`). Pick a prefix and stay consistent so
|
|
312
|
+
* error renderers and tests can branch on `code` instead of
|
|
313
|
+
* exact-message string matching.
|
|
314
|
+
*/
|
|
315
|
+
code: string;
|
|
316
|
+
};
|
|
317
|
+
/** Settled validation result when the form (or subtree) parsed successfully. */
|
|
318
|
+
type ValidationResponseSuccess<TData> = {
|
|
319
|
+
/** The parsed value at the validated subtree (whole form when `validate()` was called without a path). */
|
|
320
|
+
data: TData;
|
|
321
|
+
errors: undefined;
|
|
322
|
+
success: true;
|
|
323
|
+
formKey: FormKey;
|
|
324
|
+
};
|
|
325
|
+
/** Settled validation result when no data could be produced (e.g. a top-level type mismatch). */
|
|
326
|
+
type ValidationResponseErrorWithoutData = {
|
|
327
|
+
data: undefined;
|
|
328
|
+
/** Non-empty list of failures. */
|
|
329
|
+
errors: ValidationError[];
|
|
330
|
+
success: false;
|
|
331
|
+
formKey: FormKey;
|
|
332
|
+
};
|
|
333
|
+
/** Settled validation result when the parser produced partial data alongside failures. */
|
|
334
|
+
type ValidationResponseErrorWithData<TData> = {
|
|
335
|
+
data: TData;
|
|
336
|
+
errors: ValidationError[];
|
|
337
|
+
success: false;
|
|
338
|
+
formKey: FormKey;
|
|
339
|
+
};
|
|
340
|
+
/**
|
|
341
|
+
* Settled validation result. Discriminate on `success`:
|
|
342
|
+
*
|
|
343
|
+
* ```ts
|
|
344
|
+
* if (result.success) {
|
|
345
|
+
* // result.data is the parsed value, errors is undefined
|
|
346
|
+
* } else {
|
|
347
|
+
* // result.errors is non-empty, data may or may not be set
|
|
348
|
+
* }
|
|
349
|
+
* ```
|
|
350
|
+
*/
|
|
351
|
+
type ValidationResponse<TData> = ValidationResponseSuccess<TData> | ValidationResponseErrorWithData<TData> | ValidationResponseErrorWithoutData;
|
|
352
|
+
/**
|
|
353
|
+
* Result of resolving the form's default values. Always returns at
|
|
354
|
+
* least the shape derived from the schema; `errors` carry any
|
|
355
|
+
* failures from validating those defaults against the schema.
|
|
356
|
+
*/
|
|
357
|
+
type DefaultValuesResponse<TData> = ValidationResponseSuccess<TData> | ValidationResponseErrorWithData<TData>;
|
|
358
|
+
/**
|
|
359
|
+
* Trimmed `ValidationResponse` that omits the `data` payload. Used by
|
|
360
|
+
* `validate()` / `validateAsync()` since consumers usually only need
|
|
361
|
+
* the success flag and error list at those entry points.
|
|
362
|
+
*/
|
|
363
|
+
type ValidationResponseWithoutValue<Form> = Omit<ValidationResponse<Form>, 'data'>;
|
|
364
|
+
/**
|
|
365
|
+
* Sync-or-async return shape for `AbstractSchema.validateAtPath`. The
|
|
366
|
+
* adapter returns the response inline when the schema and the
|
|
367
|
+
* caller's options permit synchronous validation; otherwise a
|
|
368
|
+
* `Promise<T>`. Callers that don't care simply `await` (works for
|
|
369
|
+
* both); callers that DO care (the reshape pre-pass — flicker
|
|
370
|
+
* prevention) branch on `instanceof Promise`.
|
|
371
|
+
*/
|
|
372
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
373
|
+
/**
|
|
374
|
+
* Options accepted by `AbstractSchema.validateAtPath`. Currently a
|
|
375
|
+
* single field; kept as an object for forward-compat with future
|
|
376
|
+
* knobs (e.g. cancellation signals, abort tokens) without breaking
|
|
377
|
+
* the call signature.
|
|
378
|
+
*
|
|
379
|
+
* - `sync`: when `true`, the adapter SHOULD return the response
|
|
380
|
+
* inline if the schema permits synchronous validation. When the
|
|
381
|
+
* schema is structurally async (any verdict that resolves only via
|
|
382
|
+
* a Promise — async refinements, async transforms / pipes — in
|
|
383
|
+
* whichever library the adapter wraps), the adapter falls back to
|
|
384
|
+
* a `Promise<T>` — the flag is a preference, not a guarantee.
|
|
385
|
+
*
|
|
386
|
+
* When omitted or `false`, the adapter is free to use its async
|
|
387
|
+
* path (matches the historical Promise-returning contract; every
|
|
388
|
+
* non-reshape callsite uses this default).
|
|
389
|
+
*/
|
|
390
|
+
type ValidateOptions = {
|
|
391
|
+
sync?: boolean;
|
|
392
|
+
};
|
|
393
|
+
type GetDefaultValuesConfig<Form> = {
|
|
394
|
+
useDefaultSchemaValues: boolean;
|
|
395
|
+
/**
|
|
396
|
+
* Whether to keep schema refinements when deriving slim defaults.
|
|
397
|
+
* `true` (default) — preserve refinements; `false` — strip them so
|
|
398
|
+
* placeholder data lands without immediate construction-time
|
|
399
|
+
* errors. Mirrors `useForm({ strict })`.
|
|
400
|
+
*/
|
|
401
|
+
strict?: boolean;
|
|
402
|
+
constraints?: DeepPartial<WriteShape<Form>> | undefined;
|
|
403
|
+
};
|
|
404
|
+
/**
|
|
405
|
+
* The contract a schema adapter implements so the form runtime can
|
|
406
|
+
* read defaults, validate, and walk paths against any underlying
|
|
407
|
+
* schema library.
|
|
408
|
+
*
|
|
409
|
+
* Most consumers never touch this type directly — the typed entry
|
|
410
|
+
* points (e.g. `attaform/zod`, `attaform/zod-v3`)
|
|
411
|
+
* wire an adapter automatically. Implement this interface only when
|
|
412
|
+
* adding support for a new schema library (Valibot, ArkType, custom).
|
|
413
|
+
*/
|
|
414
|
+
type AbstractSchema<Form, GetValueFormType> = {
|
|
415
|
+
/**
|
|
416
|
+
* Structural fingerprint of the schema. Same shape → same string;
|
|
417
|
+
* different shape → (best-effort) different string.
|
|
418
|
+
*
|
|
419
|
+
* The library uses this to detect schema mismatches at a shared
|
|
420
|
+
* form key: two `useForm({ key: 'x', schema })` calls are allowed
|
|
421
|
+
* to land on the same `FormStore` (the "shared store" semantic),
|
|
422
|
+
* but only when their schemas agree. If the second call's
|
|
423
|
+
* fingerprint differs from the first's, the library emits a
|
|
424
|
+
* dev-mode warning — the first call's schema stays canonical and
|
|
425
|
+
* the second call's schema is silently ignored.
|
|
426
|
+
*
|
|
427
|
+
* Guarantees adapter authors should provide:
|
|
428
|
+
* - **Determinism:** equal shapes at different memory addresses
|
|
429
|
+
* must produce the same fingerprint. Referential equality fails
|
|
430
|
+
* 99% of the time across files, so reference-identity is not a
|
|
431
|
+
* substitute.
|
|
432
|
+
* - **Key-order-insensitivity** for record-like shapes (object,
|
|
433
|
+
* struct) — two shapes with the same keys but different iteration
|
|
434
|
+
* order must match.
|
|
435
|
+
* - **Order-insensitivity for unbounded unions** — `a | b` and
|
|
436
|
+
* `b | a` must match (the set of members is what matters, not
|
|
437
|
+
* their source order).
|
|
438
|
+
*
|
|
439
|
+
* Compromises adapter authors may accept:
|
|
440
|
+
* - Function-valued metadata (refinements, transforms, lazy
|
|
441
|
+
* defaults) is not stably hashable. Represent it as an opaque
|
|
442
|
+
* sentinel; two schemas differing only in refinement logic will
|
|
443
|
+
* look identical. The warning is a footgun catcher, not a
|
|
444
|
+
* soundness guarantee.
|
|
445
|
+
*/
|
|
446
|
+
fingerprint(): string;
|
|
447
|
+
getDefaultValues(config: GetDefaultValuesConfig<Form>): DefaultValuesResponse<Form>;
|
|
448
|
+
/**
|
|
449
|
+
* Return the schema-prescribed default value at the given path. The
|
|
450
|
+
* runtime uses this to fill structural gaps so every `setValue` write
|
|
451
|
+
* leaves the form satisfying the slim schema (objects/arrays/primitives
|
|
452
|
+
* without refinement-level constraints).
|
|
453
|
+
*
|
|
454
|
+
* Semantics:
|
|
455
|
+
* - **Object property path:** the property's schema default.
|
|
456
|
+
* - **Array element path:** the element default (paths past the
|
|
457
|
+
* array's current length still resolve — every position resolves
|
|
458
|
+
* to the same element type).
|
|
459
|
+
* - **Tuple position path:** the position-specific default. Out-of-
|
|
460
|
+
* range positions return `undefined`.
|
|
461
|
+
* - **Optional/Default/Nullable/Readonly/Catch/Pipe wrappers:** the
|
|
462
|
+
* inner default.
|
|
463
|
+
* - **Discriminated union:** the first variant's default (matches
|
|
464
|
+
* `validateAtPath`'s first-success semantic).
|
|
465
|
+
* - **Leaf:** the primitive default (`''`, `0`, `false`, etc., or the
|
|
466
|
+
* wrapper's `.default(x)` value when present).
|
|
467
|
+
* - **Path doesn't exist in schema:** `undefined`.
|
|
468
|
+
*
|
|
469
|
+
* Adapters may return `undefined` when the path can't be resolved;
|
|
470
|
+
* callers treat that as "don't fill" and fall back to existing data.
|
|
471
|
+
*/
|
|
472
|
+
getDefaultAtPath(path: Path): unknown;
|
|
473
|
+
/**
|
|
474
|
+
* Distinguish a tuple (fixed-length, position-typed) from an
|
|
475
|
+
* unbounded array at `path`. The runtime calls this on every
|
|
476
|
+
* `mergeStructural` / `setAtPathWithSchemaFill` write that descends
|
|
477
|
+
* into an array branch — caching the answer at the schema level
|
|
478
|
+
* replaces the per-write 1M-index probe + sequential probe loop
|
|
479
|
+
* (up to 1024 schema lookups) the runtime previously used.
|
|
480
|
+
*
|
|
481
|
+
* Return values:
|
|
482
|
+
* - `number` → tuple of this structural length. The runtime pads
|
|
483
|
+
* the consumer to this length and recurses position-by-position.
|
|
484
|
+
* - `null` → unbounded array. The runtime uses the consumer's
|
|
485
|
+
* length and reuses one element default for every position.
|
|
486
|
+
* - `undefined` → the path doesn't resolve to an array OR the
|
|
487
|
+
* adapter can't determine the shape. The runtime falls back to
|
|
488
|
+
* the legacy probe loop in this case (defensive — every built-in
|
|
489
|
+
* adapter returns `number` or `null`).
|
|
490
|
+
*
|
|
491
|
+
* Wrappers (optional / nullable / default / readonly / catch /
|
|
492
|
+
* pipe / lazy) are peeled transparently before the type check, so
|
|
493
|
+
* `optional(z.tuple([...]))` reports its tuple length.
|
|
494
|
+
*/
|
|
495
|
+
arrayShapeAtPath(path: Path): number | null | undefined;
|
|
496
|
+
/**
|
|
497
|
+
* Return every sub-schema that could resolve at the given structured
|
|
498
|
+
* path. Multiple results are only expected for discriminated / union
|
|
499
|
+
* branches where the adapter can't decide a single winner until the
|
|
500
|
+
* data lands. `path` is the canonical `Segment[]` — adapters walk it
|
|
501
|
+
* segment-by-segment so literal-dot keys (`['user.name']`) don't
|
|
502
|
+
* collide with the sibling-pair form (`['user', 'name']`).
|
|
503
|
+
*/
|
|
504
|
+
getSchemasAtPath(path: Path): AbstractSchema<unknown, GetValueFormType>[];
|
|
505
|
+
/**
|
|
506
|
+
* Validate a subtree (when `path` is provided) or the whole form (when
|
|
507
|
+
* `path` is `undefined`). `path` is the canonical `Segment[]`, not a
|
|
508
|
+
* dotted string — two schemas with otherwise-colliding dotted forms
|
|
509
|
+
* (`['user.name']` vs `['user', 'name']`) stay distinct at the
|
|
510
|
+
* adapter boundary.
|
|
511
|
+
*
|
|
512
|
+
* Return type is `MaybePromise<ValidationResponse>`:
|
|
513
|
+
* - With `options.sync === true` AND a sync-capable schema, the
|
|
514
|
+
* adapter SHOULD return the response inline (`T`). This lets the
|
|
515
|
+
* runtime batch error writes with a coincident form-value
|
|
516
|
+
* mutation in a single Vue reactive flush — preventing the `{}`
|
|
517
|
+
* flicker observable during DU variant reshape.
|
|
518
|
+
* - With `options.sync === true` AND an async-only schema (any
|
|
519
|
+
* verdict that resolves only via a Promise), the adapter MUST
|
|
520
|
+
* fall back to `Promise<T>`. The flag is a preference, not a
|
|
521
|
+
* guarantee; sync isn't always achievable.
|
|
522
|
+
* - With `options.sync` omitted or `false`, the adapter SHOULD
|
|
523
|
+
* return `Promise<T>` (matches the historical contract — every
|
|
524
|
+
* non-reshape callsite uses this default and immediately
|
|
525
|
+
* `await`s the result).
|
|
526
|
+
*
|
|
527
|
+
* Callers that don't care simply `await` (works for both arms);
|
|
528
|
+
* callers that need to detect sync-vs-async branch on
|
|
529
|
+
* `instanceof Promise`. Adapters MUST NOT throw — errors are
|
|
530
|
+
* returned as a `success: false` response with a populated
|
|
531
|
+
* `errors` array.
|
|
532
|
+
*/
|
|
533
|
+
validateAtPath(data: unknown, path: Path | undefined, options?: ValidateOptions): MaybePromise<ValidationResponse<Form>>;
|
|
534
|
+
/**
|
|
535
|
+
* Sync sister to `getSchemasAtPath` / `validateAtPath`. Returns the
|
|
536
|
+
* set of primitive `typeof`-style kinds the path's leaf schema
|
|
537
|
+
* accepts at write time. Wrappers (optional / nullable / default /
|
|
538
|
+
* refinement / transform / pipe / readonly / catch / lazy) are
|
|
539
|
+
* peeled; refinement-level constraints (format checks like email /
|
|
540
|
+
* uuid, min/max length, enum membership, literal equality, regex)
|
|
541
|
+
* are IGNORED — they're a validation-time concern.
|
|
542
|
+
*
|
|
543
|
+
* Used by `setValueAtPath` to gate writes synchronously without
|
|
544
|
+
* round-tripping through async `validateAtPath`. The returned set
|
|
545
|
+
* unions across union branches and intersects across intersection
|
|
546
|
+
* sides.
|
|
547
|
+
*
|
|
548
|
+
* Conventions:
|
|
549
|
+
* - Empty set → no kind admitted. The runtime gate rejects every
|
|
550
|
+
* write to the path. Surfaces for `never`-typed schemas AND for
|
|
551
|
+
* paths that don't resolve in the schema (typo / unknown leaf).
|
|
552
|
+
* - Permissive set (every kind) → "unknown / unconstrained." The
|
|
553
|
+
* gate accepts any value. Surfaces for `any` / `unknown` / `void`
|
|
554
|
+
* and the lazy-peel-failure case where the adapter can't
|
|
555
|
+
* introspect the schema.
|
|
556
|
+
* - For string-valued enums: returns `{'string'}`. For numeric
|
|
557
|
+
* enums: `{'number'}`.
|
|
558
|
+
* - For literal types: returns `{primitiveKindOf(literalValue)}`.
|
|
559
|
+
* - For object / array containers: `{'object'}` / `{'array'}`. The
|
|
560
|
+
* runtime walker recurses into entries / elements at write time.
|
|
561
|
+
* - For nullable / optional wrappers: adds `'null'` / `'undefined'`
|
|
562
|
+
* to the inner's set.
|
|
563
|
+
*/
|
|
564
|
+
getSlimPrimitiveTypesAtPath(path: Path): Set<SlimPrimitiveKind>;
|
|
565
|
+
/**
|
|
566
|
+
* Return `true` iff `path` resolves to a **leaf** in the schema — a
|
|
567
|
+
* path whose slim primitive set contains only primitive kinds (no
|
|
568
|
+
* `object`, `array`, `map`, `set`). The runtime proxies (`form.values`,
|
|
569
|
+
* `form.errors`, `form.fields`) query this at every step to decide
|
|
570
|
+
* between **descend into a sub-proxy** (container) and **terminate
|
|
571
|
+
* with a leaf value** (leaf).
|
|
572
|
+
*
|
|
573
|
+
* The leaf-aware branching is what kills the FIELD_STATE_KEYS
|
|
574
|
+
* shadowing problem: reserved leaf-prop names (`dirty`, `errors`,
|
|
575
|
+
* `isValid`, …) inject only at the FieldStateView terminal, not at
|
|
576
|
+
* every depth. A schema field literally named `dirty` at depth ≥ 2
|
|
577
|
+
* stays reachable as a sub-proxy or leaf in its own right.
|
|
578
|
+
*
|
|
579
|
+
* Semantics:
|
|
580
|
+
* - **Object / Array / Map / Set** at any wrapper layer → `false`
|
|
581
|
+
* (container; descend further).
|
|
582
|
+
* - **Primitive** (string/number/boolean/bigint/symbol/null/undefined/
|
|
583
|
+
* date/function) → `true`. `'date'` counts as a leaf (don't drill
|
|
584
|
+
* into `Date`). `'function'` is a leaf for the same reason — opaque
|
|
585
|
+
* value.
|
|
586
|
+
* - **Optional / Nullable / Default / Catch** wrappers transparent —
|
|
587
|
+
* adds `'null'` / `'undefined'` to the inner kind set without
|
|
588
|
+
* changing the leaf classification.
|
|
589
|
+
* - **Discriminated union root** → `false` (variants are objects;
|
|
590
|
+
* the kind set contains `'object'`).
|
|
591
|
+
* - **DU discriminator key** → `true` (the literal type resolves to
|
|
592
|
+
* `{'string'}` / `{'number'}`).
|
|
593
|
+
* - **DU variant-only key** → `true` if it resolves to a primitive
|
|
594
|
+
* in any variant; schema-static (does NOT query live storage to
|
|
595
|
+
* decide which variant is active).
|
|
596
|
+
* - **Empty path (root)** → `false` (root is the form-as-object).
|
|
597
|
+
* - **Path doesn't exist in schema** → `false`. The proxy descends
|
|
598
|
+
* permissively; reads of leaf props at the unknown path return
|
|
599
|
+
* `undefined` from the underlying store. Treating unknown paths
|
|
600
|
+
* as containers preserves the schema's authority and avoids
|
|
601
|
+
* re-introducing shadowing on typos.
|
|
602
|
+
*
|
|
603
|
+
* Adapters MAY cache results per-path — `isLeafAtPath` will be
|
|
604
|
+
* called on every proxy `get` trap hit. The reference implementation
|
|
605
|
+
* memoises a `Map<PathKey, boolean>` keyed by `canonicalizePath(path).key`,
|
|
606
|
+
* lifetime tied to the adapter (one per `useForm()` call).
|
|
607
|
+
*/
|
|
608
|
+
isLeafAtPath(path: Path): boolean;
|
|
609
|
+
/**
|
|
610
|
+
* Return `true` if the leaf at `path` is required — i.e. the schema
|
|
611
|
+
* does NOT admit "empty" via `.optional()`, `.nullable()`,
|
|
612
|
+
* `.default(N)`, or `.catch(N)` at the leaf or any wrapper.
|
|
613
|
+
*
|
|
614
|
+
* Used by the submit / validate path to surface a "No value supplied" error
|
|
615
|
+
* when a field is in the form's `blankPaths` set (the user
|
|
616
|
+
* cleared it or never answered) AND the schema treats the field as
|
|
617
|
+
* required. Without this, a strict numeric leaf would silently
|
|
618
|
+
* accept the slim default (`0`) for an unanswered field — the
|
|
619
|
+
* "public-housing" footgun where `$0 income` passes validation.
|
|
620
|
+
*
|
|
621
|
+
* Semantics:
|
|
622
|
+
* - **Optional / Nullable / Default / Catch** at any wrapper layer
|
|
623
|
+
* (root or nested) → `false`. The schema author opted into
|
|
624
|
+
* accepting empty.
|
|
625
|
+
* - **Readonly / Pipe / Lazy** wrappers are transparent — peel and
|
|
626
|
+
* re-check the inner schema.
|
|
627
|
+
* - **Union / Discriminated union** → `false` if ANY branch admits
|
|
628
|
+
* empty (the union accepts what the most permissive branch
|
|
629
|
+
* accepts). This matches the parse-time "first success wins"
|
|
630
|
+
* semantic of `validateAtPath`.
|
|
631
|
+
* - **Intersection** → `true` if EITHER side requires the path
|
|
632
|
+
* (intersection requires both sides to accept; if one rejects
|
|
633
|
+
* empty, the intersection rejects empty).
|
|
634
|
+
* - **Path doesn't exist in schema** → `false` (can't enforce
|
|
635
|
+
* what we don't know about).
|
|
636
|
+
* - **Empty path (root)** → `true` (the root form is always
|
|
637
|
+
* required as an object).
|
|
638
|
+
*
|
|
639
|
+
* Refinement-level constraints (length / format / custom predicates)
|
|
640
|
+
* are NOT consulted here — those run at parse time inside
|
|
641
|
+
* `validateAtPath` and surface as schema errors regardless.
|
|
642
|
+
* `isRequiredAtPath` only answers the "is this leaf at all
|
|
643
|
+
* required?" question; the refinements layer on top.
|
|
644
|
+
*/
|
|
645
|
+
isRequiredAtPath(path: Path): boolean;
|
|
646
|
+
/**
|
|
647
|
+
* If the schema at `path` is (or wraps) a discriminated union,
|
|
648
|
+
* return its discriminator key plus a `getVariantDefault(value)`
|
|
649
|
+
* lookup — otherwise `undefined`. Wrappers (optional, default,
|
|
650
|
+
* nullable, readonly, pipe, lazy, catch) are peeled transparently.
|
|
651
|
+
*
|
|
652
|
+
* The runtime uses this for two related reshapes that share the
|
|
653
|
+
* same lookup:
|
|
654
|
+
*
|
|
655
|
+
* 1. **Discriminator-key write** — the runtime calls this with
|
|
656
|
+
* the parent path. If the returned `discriminatorKey` matches
|
|
657
|
+
* the path's last segment, the write changes which variant is
|
|
658
|
+
* active; the parent storage is replaced with the matching
|
|
659
|
+
* variant's slim default so the OLD variant's keys (e.g.
|
|
660
|
+
* `address` after switching to `sms`) don't leak.
|
|
661
|
+
*
|
|
662
|
+
* 2. **Whole-union write** — the runtime calls this with the
|
|
663
|
+
* path itself. If the returned info exists and the consumer's
|
|
664
|
+
* value carries the discriminator key, the merge uses the
|
|
665
|
+
* matching variant's default instead of the first-variant
|
|
666
|
+
* fallback that `getDefaultAtPath` returns for unions.
|
|
667
|
+
*
|
|
668
|
+
* Adapters that don't model discriminated unions can return
|
|
669
|
+
* `undefined` unconditionally; the runtime reshape is a no-op
|
|
670
|
+
* without this hook.
|
|
671
|
+
*/
|
|
672
|
+
getUnionDiscriminatorAtPath(path: Path): UnionDiscriminatorContext | undefined;
|
|
673
|
+
/**
|
|
674
|
+
* Return `true` if `validateAtPath` MAY have to run asynchronously
|
|
675
|
+
* to surface every error this schema can produce. The runtime uses
|
|
676
|
+
* this at construction to decide whether to schedule a one-shot
|
|
677
|
+
* full-form async validation: when `false` (or omitted), the
|
|
678
|
+
* construction-time sync seed is the authoritative result and no
|
|
679
|
+
* extra microtask is spent; when `true`, an async pass is queued
|
|
680
|
+
* so any async-only verdicts (refinements / transforms / pipes
|
|
681
|
+
* that resolve only via a Promise) surface without waiting for a
|
|
682
|
+
* user mutation.
|
|
683
|
+
*
|
|
684
|
+
* Optional. The runtime treats a missing implementation as
|
|
685
|
+
* `() => false`, so adapters that don't model async work — or
|
|
686
|
+
* don't yet support detection — can omit it; async-only errors
|
|
687
|
+
* then fall back to firing on first user mutation, matching the
|
|
688
|
+
* pre-detection behavior. Detection is best-effort.
|
|
689
|
+
*/
|
|
690
|
+
needsAsyncValidation?(): boolean;
|
|
691
|
+
};
|
|
692
|
+
/**
|
|
693
|
+
* Adapter-returned info for a discriminated union — its discriminator
|
|
694
|
+
* key plus a function that maps a discriminator literal to the slim
|
|
695
|
+
* default of the matching variant. Returned by
|
|
696
|
+
* `AbstractSchema.getUnionDiscriminatorAtPath`.
|
|
697
|
+
*/
|
|
698
|
+
type UnionDiscriminatorContext = {
|
|
699
|
+
/**
|
|
700
|
+
* The union's discriminator key — the property name whose literal
|
|
701
|
+
* value selects the variant (e.g. `'channel'` for a union split on
|
|
702
|
+
* `{ channel: 'sms' | 'email' }`).
|
|
703
|
+
*/
|
|
704
|
+
readonly discriminatorKey: string;
|
|
705
|
+
/**
|
|
706
|
+
* Slim default for the variant whose discriminator literal equals
|
|
707
|
+
* `value`. Returns `undefined` if no variant matches — the runtime
|
|
708
|
+
* skips the reshape and falls back to a plain write.
|
|
709
|
+
*/
|
|
710
|
+
getVariantDefault(value: unknown): unknown;
|
|
711
|
+
};
|
|
712
|
+
/**
|
|
713
|
+
* The set of primitive "kinds" the slim-primitive write contract
|
|
714
|
+
* recognises. Drawn from `typeof` plus a few well-known reference
|
|
715
|
+
* shapes (`Date`, `Array`, `Map`, `Set`, plain `object`, `null`).
|
|
716
|
+
*
|
|
717
|
+
* The runtime gate's `slimKindOf(value)` returns one of these for a
|
|
718
|
+
* value; the adapter's `getSlimPrimitiveTypesAtPath(path)` returns
|
|
719
|
+
* the set of kinds the path's leaf schema accepts. A write is gated
|
|
720
|
+
* by `accepted.has(slimKindOf(value))`.
|
|
721
|
+
*/
|
|
722
|
+
type SlimPrimitiveKind = 'string' | 'number' | 'boolean' | 'bigint' | 'date' | 'null' | 'undefined' | 'object' | 'array' | 'symbol' | 'function' | 'map' | 'set';
|
|
723
|
+
/**
|
|
724
|
+
* The "no result yet" status returned by the reactive `validate()` ref
|
|
725
|
+
* while a validation run is in flight.
|
|
726
|
+
*
|
|
727
|
+
* Narrow against `pending` to access the settled fields:
|
|
728
|
+
*
|
|
729
|
+
* ```ts
|
|
730
|
+
* const status = form.validate()
|
|
731
|
+
* watchEffect(() => {
|
|
732
|
+
* if (status.value.pending) return
|
|
733
|
+
* // status.value.success / status.value.errors are now safe to read
|
|
734
|
+
* })
|
|
735
|
+
* ```
|
|
736
|
+
*/
|
|
737
|
+
type PendingValidationStatus = {
|
|
738
|
+
readonly pending: true;
|
|
739
|
+
readonly errors: undefined;
|
|
740
|
+
readonly success: false;
|
|
741
|
+
readonly formKey: FormKey;
|
|
742
|
+
};
|
|
743
|
+
/** Settled status of a reactive `validate()` call. Mirrors the latest result. */
|
|
744
|
+
type SettledValidationStatus<Form> = {
|
|
745
|
+
readonly pending: false;
|
|
746
|
+
} & ValidationResponseWithoutValue<Form>;
|
|
747
|
+
/**
|
|
748
|
+
* The value type of the ref returned by `validate()`. Discriminate on
|
|
749
|
+
* `pending` to switch between in-flight and settled states.
|
|
750
|
+
*/
|
|
751
|
+
type ReactiveValidationStatus<Form> = PendingValidationStatus | SettledValidationStatus<Form>;
|
|
752
|
+
/**
|
|
753
|
+
* What to do when a submit attempt fails validation. The library can
|
|
754
|
+
* focus and/or scroll the first errored field into view without
|
|
755
|
+
* wiring an `onError` callback yourself. Off by default.
|
|
756
|
+
*
|
|
757
|
+
* - `'none'` (default): no automatic UI nudge.
|
|
758
|
+
* - `'focus-first-error'`: focus the first errored field's first
|
|
759
|
+
* visible element (with `preventScroll: true` so it doesn't fight
|
|
760
|
+
* any `'scroll-to-first-error'` choice you make).
|
|
761
|
+
* - `'scroll-to-first-error'`: scroll that element into view.
|
|
762
|
+
* - `'both'`: scroll first, then focus.
|
|
763
|
+
*
|
|
764
|
+
* If no errored field has a currently mounted, visible element, the
|
|
765
|
+
* policy silently no-ops.
|
|
766
|
+
*/
|
|
767
|
+
type OnInvalidSubmitPolicy = 'none' | 'focus-first-error' | 'scroll-to-first-error' | 'both';
|
|
768
|
+
/**
|
|
769
|
+
* When per-field VALIDATION runs. Only validation timing varies per
|
|
770
|
+
* mode; storage commit timing is the directive's concern (the
|
|
771
|
+
* default `<input v-register>` commits per keystroke; `.lazy` defers
|
|
772
|
+
* to blur).
|
|
773
|
+
*
|
|
774
|
+
* - `'change'` (default): every committed write schedules a
|
|
775
|
+
* validation for the affected path. With `debounceMs: 0` (also the
|
|
776
|
+
* default) the run is synchronous in the write handler;
|
|
777
|
+
* positive `debounceMs` coalesces rapid bursts.
|
|
778
|
+
* - `'blur'`: validate immediately when the user tabs away from a
|
|
779
|
+
* registered field. No debounce — `debounceMs` is rejected by the
|
|
780
|
+
* type.
|
|
781
|
+
* - `'submit'`: no live validation. `handleSubmit` and explicit
|
|
782
|
+
* `validate()` / `validateAsync()` calls are the only validation
|
|
783
|
+
* surfaces. `debounceMs` is rejected by the type.
|
|
784
|
+
*/
|
|
785
|
+
type ValidateOn = 'change' | 'blur' | 'submit';
|
|
786
|
+
/**
|
|
787
|
+
* Validation timing config — `validateOn` is the trigger, `debounceMs`
|
|
788
|
+
* the wait (after the last committed write) before the next
|
|
789
|
+
* validation run fires. `debounceMs` ONLY governs validation;
|
|
790
|
+
* `setValueWithInternalPath` commits to `form.values` immediately
|
|
791
|
+
* regardless of debounce. (How OFTEN the directive forwards writes
|
|
792
|
+
* to storage is the directive's concern — default `<input
|
|
793
|
+
* v-register>` commits per keystroke; `<input v-register.lazy>`
|
|
794
|
+
* defers to the blur `change` event.)
|
|
795
|
+
*
|
|
796
|
+
* `debounceMs` is only meaningful with `validateOn: 'change'` (the
|
|
797
|
+
* default); `'blur'` and `'submit'` ignore the wait entirely (blur
|
|
798
|
+
* fires validation immediately on focus-out; submit is its own
|
|
799
|
+
* trigger). The discriminated union below makes pairing `debounceMs`
|
|
800
|
+
* with `'blur'` / `'submit'` a TS error instead of a silent runtime
|
|
801
|
+
* drop.
|
|
802
|
+
*
|
|
803
|
+
* Pass `debounceMs: 0` (the default) to disable validation
|
|
804
|
+
* debouncing — every committed write triggers a validation pass with
|
|
805
|
+
* no `setTimeout` indirection. Schema work itself still rides
|
|
806
|
+
* `Promise.resolve().then(validateAtPath)` — async but microtask, so
|
|
807
|
+
* errors land on the next tick. Set `debounceMs` to a positive
|
|
808
|
+
* number to coalesce rapid bursts (useful for slow async adapters or
|
|
809
|
+
* for smoothing inline feedback under heavy typing).
|
|
810
|
+
*/
|
|
811
|
+
type ValidateOnConfig = {
|
|
812
|
+
/** Validation trigger. Default `'change'`. */
|
|
813
|
+
validateOn?: 'change';
|
|
814
|
+
/**
|
|
815
|
+
* Milliseconds to wait after the last committed write before
|
|
816
|
+
* running validation. Default `0` (validation runs synchronously
|
|
817
|
+
* after the write; no `setTimeout`). Set to a positive number
|
|
818
|
+
* to coalesce rapid bursts into a single validation pass.
|
|
819
|
+
*
|
|
820
|
+
* Note: this is purely the validation debounce. Storage commits
|
|
821
|
+
* happen at the directive's listener (per-keystroke for
|
|
822
|
+
* `<input v-register>`, per-blur for `<input v-register.lazy>`)
|
|
823
|
+
* — `debounceMs` doesn't change either.
|
|
824
|
+
*/
|
|
825
|
+
debounceMs?: number;
|
|
826
|
+
} | {
|
|
827
|
+
/** Validation trigger. */
|
|
828
|
+
validateOn: 'blur' | 'submit';
|
|
829
|
+
/** `debounceMs` is not allowed with `'blur'` or `'submit'`. */
|
|
830
|
+
debounceMs?: never;
|
|
831
|
+
};
|
|
832
|
+
/**
|
|
833
|
+
* Built-in storage backends:
|
|
834
|
+
*
|
|
835
|
+
* - `'local'` — browser `localStorage` (persists across tabs and reloads).
|
|
836
|
+
* - `'session'` — browser `sessionStorage` (cleared when the tab closes).
|
|
837
|
+
* - `'indexeddb'` — IndexedDB via a zero-dependency wrapper (handles
|
|
838
|
+
* structured-cloneable data; suitable for larger drafts).
|
|
839
|
+
*
|
|
840
|
+
* For anything else (encrypted storage, a native bridge, a cookie
|
|
841
|
+
* store) pass a custom `FormStorage` object instead.
|
|
842
|
+
*/
|
|
843
|
+
type FormStorageKind = 'local' | 'session' | 'indexeddb';
|
|
844
|
+
/**
|
|
845
|
+
* Custom persistence backend. Implement this when none of the built-in
|
|
846
|
+
* `'local'` / `'session'` / `'indexeddb'` backends fit (e.g. encrypted
|
|
847
|
+
* storage, a cross-window broadcast layer, or a native mobile bridge).
|
|
848
|
+
*
|
|
849
|
+
* All methods are async. Pass values through unchanged — `getItem`
|
|
850
|
+
* should return whatever `setItem` was given, including non-string
|
|
851
|
+
* values. The library handles serialization for the built-in
|
|
852
|
+
* `'local'` / `'session'` backends; custom adapters can store the
|
|
853
|
+
* value directly if their backing store accepts structured data.
|
|
854
|
+
*
|
|
855
|
+
* `listKeys(prefix)` returns every key starting with `prefix`. The
|
|
856
|
+
* library uses it on mount to clean up entries left over from older
|
|
857
|
+
* schema versions (each persisted entry carries a schema fingerprint
|
|
858
|
+
* suffix; mismatched entries are dropped automatically).
|
|
859
|
+
*/
|
|
860
|
+
type FormStorage = {
|
|
861
|
+
/** Fetch the value previously stored under `key`. Resolve to `null`/`undefined` for misses. */
|
|
862
|
+
getItem(key: string): Promise<unknown>;
|
|
863
|
+
/** Persist `value` under `key`. */
|
|
864
|
+
setItem(key: string, value: unknown): Promise<void>;
|
|
865
|
+
/** Remove the entry at `key`. No-op if not present. */
|
|
866
|
+
removeItem(key: string): Promise<void>;
|
|
867
|
+
/** Return every key in this backend whose name starts with `prefix`. */
|
|
868
|
+
listKeys(prefix: string): Promise<string[]>;
|
|
869
|
+
};
|
|
870
|
+
/**
|
|
871
|
+
* What to include when persisting:
|
|
872
|
+
*
|
|
873
|
+
* - `'form'` (default) — only the form value. Errors get repopulated
|
|
874
|
+
* by validation on reload anyway.
|
|
875
|
+
* - `'form+errors'` — also persist the current error list. Useful when
|
|
876
|
+
* the error context is expensive to recompute (e.g. cross-field
|
|
877
|
+
* refinements that depend on server data).
|
|
878
|
+
*/
|
|
879
|
+
type PersistIncludeMode = 'form' | 'form+errors';
|
|
880
|
+
/**
|
|
881
|
+
* Per-write metadata. Used internally to flag which writes should
|
|
882
|
+
* reach the persistence layer (e.g. only writes from elements opted
|
|
883
|
+
* into persistence via `register(path, { persist: true })`).
|
|
884
|
+
*
|
|
885
|
+
* Custom directive integrations may set `persist: true` to forward
|
|
886
|
+
* a write to the configured storage adapter; otherwise leave unset.
|
|
887
|
+
*/
|
|
888
|
+
type WriteMeta = {
|
|
889
|
+
/** When `true`, this write is forwarded to the configured persistence backend. */
|
|
890
|
+
readonly persist?: boolean;
|
|
891
|
+
/**
|
|
892
|
+
* When `true`, the path being written is added to the FormStore's
|
|
893
|
+
* `blankPaths` set — meaning storage holds a real, schema-
|
|
894
|
+
* conformant value (the slim default) but the UI should display the
|
|
895
|
+
* field as empty. The next write to that path WITHOUT this flag
|
|
896
|
+
* implicitly removes the path from the set (the user typed something
|
|
897
|
+
* real). Internal — set by `markBlank()` on the register
|
|
898
|
+
* binding and by the `unset` translation in `setValue` / `reset` /
|
|
899
|
+
* `useAbstractForm` construction. Don't set from consumer code.
|
|
900
|
+
*/
|
|
901
|
+
readonly blank?: boolean;
|
|
902
|
+
/**
|
|
903
|
+
* When `true`, the discriminator-aware variant reshape inside
|
|
904
|
+
* `setValueAtPath` is skipped for this write. Internal — set by
|
|
905
|
+
* the reshape itself when re-entering with the new variant default
|
|
906
|
+
* so the literal discriminator inside the default doesn't trigger
|
|
907
|
+
* an infinite loop. Don't set from consumer code.
|
|
908
|
+
*/
|
|
909
|
+
readonly skipDiscriminatorReshape?: boolean;
|
|
910
|
+
};
|
|
911
|
+
/**
|
|
912
|
+
* Undo/redo configuration passed via `useForm({ history })`.
|
|
913
|
+
*
|
|
914
|
+
* - `true` — enable with the default snapshot cap (`max: 50`).
|
|
915
|
+
* - `{ max }` — enable and tune the bounded snapshot stack size.
|
|
916
|
+
*
|
|
917
|
+
* When enabled, every mutation pushes a snapshot; `undo()` /
|
|
918
|
+
* `redo()` walk the stacks. `reset()` clears history.
|
|
919
|
+
*/
|
|
920
|
+
type HistoryConfig = true | {
|
|
921
|
+
max?: number;
|
|
922
|
+
};
|
|
923
|
+
/**
|
|
924
|
+
* Full options bag for `useForm({ persist })`. Use this when you need
|
|
925
|
+
* to override defaults beyond picking the backend.
|
|
926
|
+
*
|
|
927
|
+
* For backend-only setup, the shorthand forms are equivalent:
|
|
928
|
+
*
|
|
929
|
+
* ```ts
|
|
930
|
+
* useForm({ persist: 'local' })
|
|
931
|
+
* // same as
|
|
932
|
+
* useForm({ persist: { storage: 'local' } })
|
|
933
|
+
* ```
|
|
934
|
+
*/
|
|
935
|
+
type PersistConfigOptions = {
|
|
936
|
+
/**
|
|
937
|
+
* Where to persist. Pass `'local'` / `'session'` / `'indexeddb'` to
|
|
938
|
+
* use a built-in backend, or a custom `FormStorage` object for
|
|
939
|
+
* anything else. The built-in backends are loaded on demand, so
|
|
940
|
+
* picking `'local'` doesn't pull in IndexedDB code.
|
|
941
|
+
*/
|
|
942
|
+
storage: FormStorageKind | FormStorage;
|
|
943
|
+
/**
|
|
944
|
+
* Storage key namespace. Defaults to `attaform:${formKey}`.
|
|
945
|
+
* Override when you need a custom prefix (e.g. multi-tenant apps
|
|
946
|
+
* where the same form key may exist per-tenant).
|
|
947
|
+
*/
|
|
948
|
+
key?: string;
|
|
949
|
+
/**
|
|
950
|
+
* How long to wait after the last mutation before writing. Default
|
|
951
|
+
* `300` ms.
|
|
952
|
+
*
|
|
953
|
+
* Pass `0` to disable debouncing — every form change writes to the
|
|
954
|
+
* storage adapter immediately, no `setTimeout` indirection. Almost
|
|
955
|
+
* never the right choice for production (the storage adapter sees
|
|
956
|
+
* every keystroke), but useful for tests or for diagnosing perceived
|
|
957
|
+
* lag.
|
|
958
|
+
*/
|
|
959
|
+
debounceMs?: number;
|
|
960
|
+
/**
|
|
961
|
+
* What to persist. `'form'` (default) is sufficient for most cases —
|
|
962
|
+
* fresh validation on reload repopulates errors. Pick `'form+errors'`
|
|
963
|
+
* when the error state is expensive to recompute (e.g. server-side
|
|
964
|
+
* cross-field validation).
|
|
965
|
+
*/
|
|
966
|
+
include?: PersistIncludeMode;
|
|
967
|
+
/**
|
|
968
|
+
* When `true` (default), the persisted entry is wiped after
|
|
969
|
+
* `handleSubmit`'s submit callback resolves successfully. Set to
|
|
970
|
+
* `false` if you need the draft to survive across submissions.
|
|
971
|
+
*/
|
|
972
|
+
clearOnSubmitSuccess?: boolean;
|
|
973
|
+
};
|
|
974
|
+
/**
|
|
975
|
+
* Persistence configuration for `useForm({ persist })`. Off by default —
|
|
976
|
+
* with no config, the form does no reads, no writes, and pulls in no
|
|
977
|
+
* storage code.
|
|
978
|
+
*
|
|
979
|
+
* Three input forms; pick the one that reads best at the call site:
|
|
980
|
+
*
|
|
981
|
+
* ```ts
|
|
982
|
+
* // shorthand: built-in backend
|
|
983
|
+
* useForm({ persist: 'local' })
|
|
984
|
+
*
|
|
985
|
+
* // shorthand: custom adapter
|
|
986
|
+
* useForm({ persist: encryptedStorage })
|
|
987
|
+
*
|
|
988
|
+
* // full options bag
|
|
989
|
+
* useForm({ persist: { storage: 'local', debounceMs: 500 } })
|
|
990
|
+
* ```
|
|
991
|
+
*
|
|
992
|
+
* Per-field opt-in: setting `persist` is necessary but not sufficient.
|
|
993
|
+
* Each field that should actually persist also needs
|
|
994
|
+
* `register('foo', { persist: true })` — sensitive fields must opt in
|
|
995
|
+
* explicitly so they don't accidentally land in client-side storage.
|
|
996
|
+
*/
|
|
997
|
+
type PersistConfig = FormStorageKind | FormStorage | PersistConfigOptions;
|
|
998
|
+
/**
|
|
999
|
+
* Configuration object passed to `useForm`. All fields except `schema`
|
|
1000
|
+
* are optional.
|
|
1001
|
+
*
|
|
1002
|
+
* ```ts
|
|
1003
|
+
* const form = useForm({
|
|
1004
|
+
* schema: signupSchema,
|
|
1005
|
+
* defaultValues: { email: '' },
|
|
1006
|
+
* validateOn: 'change',
|
|
1007
|
+
* debounceMs: 200,
|
|
1008
|
+
* persist: 'local',
|
|
1009
|
+
* })
|
|
1010
|
+
* ```
|
|
1011
|
+
*/
|
|
1012
|
+
type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema extends AbstractSchema<Form, GetValueFormType>, DefaultValues extends DeepPartial<DefaultValuesShape<Form>>> = {
|
|
1013
|
+
/**
|
|
1014
|
+
* The schema describing the form's shape and validation rules.
|
|
1015
|
+
* Typed entry points like `attaform/zod` accept the
|
|
1016
|
+
* underlying library's schema directly and wrap an adapter; the
|
|
1017
|
+
* abstract entry point accepts any object implementing
|
|
1018
|
+
* `AbstractSchema`.
|
|
1019
|
+
*
|
|
1020
|
+
* For schemas that depend on the form's identity, pass a factory
|
|
1021
|
+
* `(key) => schema` instead — the library calls it once per form.
|
|
1022
|
+
*/
|
|
1023
|
+
schema: Schema | ((key: FormKey) => Schema);
|
|
1024
|
+
/**
|
|
1025
|
+
* Optional identifier for this form. Omit for one-off forms; the
|
|
1026
|
+
* library allocates a unique key automatically (SSR-safe, stable
|
|
1027
|
+
* across server→client hydration).
|
|
1028
|
+
*
|
|
1029
|
+
* Pass a string key when the form needs identity:
|
|
1030
|
+
* - to look it up from a distant component via `injectForm(key)`;
|
|
1031
|
+
* - to share state across components (multiple `useForm({ key })`
|
|
1032
|
+
* calls with the same key resolve to the same form);
|
|
1033
|
+
* - to give DevTools and validation errors a recognisable label;
|
|
1034
|
+
* - to namespace persisted drafts.
|
|
1035
|
+
*
|
|
1036
|
+
* Keys starting with `__atta:` are reserved for internal use and
|
|
1037
|
+
* throw `ReservedFormKeyError` if passed.
|
|
1038
|
+
*/
|
|
1039
|
+
key?: FormKey;
|
|
1040
|
+
/**
|
|
1041
|
+
* Initial values applied over the schema's defaults. Each field
|
|
1042
|
+
* falls back to the schema default (or the primitive default for
|
|
1043
|
+
* the slot's type) when not provided here.
|
|
1044
|
+
*
|
|
1045
|
+
* Values must satisfy the slim primitive type at each path
|
|
1046
|
+
* (string / number / boolean / Date / etc.) but do NOT have to
|
|
1047
|
+
* satisfy refinement-level constraints (format checks, enum
|
|
1048
|
+
* membership, length / range bounds). Refinement-invalid defaults
|
|
1049
|
+
* pass through and surface as field errors — this lets you
|
|
1050
|
+
* rehydrate stale saved data without losing the user's input.
|
|
1051
|
+
*/
|
|
1052
|
+
defaultValues?: DefaultValues;
|
|
1053
|
+
/**
|
|
1054
|
+
* Whether to validate default values at construction. Default
|
|
1055
|
+
* `true`.
|
|
1056
|
+
*
|
|
1057
|
+
* - `true` (default): the schema is run against the derived
|
|
1058
|
+
* defaults immediately; any failures populate `form.errors` from
|
|
1059
|
+
* the first frame. The UI decides when to *show* errors — gate
|
|
1060
|
+
* on `form.fields.<path>.touched`, `form.meta.submitCount`, etc.
|
|
1061
|
+
* - `false`: refinements are stripped during defaults derivation
|
|
1062
|
+
* and construction-time validation is skipped. Useful for
|
|
1063
|
+
* multi-step wizards, field arrays seeded with placeholder
|
|
1064
|
+
* rows, or any form intentionally mounting with incomplete data.
|
|
1065
|
+
*
|
|
1066
|
+
* Runtime validation (per-field on edit, full-form on submit) is
|
|
1067
|
+
* identical regardless of this flag.
|
|
1068
|
+
*/
|
|
1069
|
+
strict?: boolean;
|
|
1070
|
+
/**
|
|
1071
|
+
* Automatic UI nudge on submit-validation failure. Fires after
|
|
1072
|
+
* errors are populated and before your `onError` callback runs.
|
|
1073
|
+
* Default `'none'`.
|
|
1074
|
+
*
|
|
1075
|
+
* - `'focus-first-error'`: focus the first errored field's first
|
|
1076
|
+
* visible element (without scrolling).
|
|
1077
|
+
* - `'scroll-to-first-error'`: scroll it into view.
|
|
1078
|
+
* - `'both'`: scroll, then focus.
|
|
1079
|
+
*
|
|
1080
|
+
* If no errored field has a currently-mounted, visible element,
|
|
1081
|
+
* the policy silently no-ops.
|
|
1082
|
+
*/
|
|
1083
|
+
onInvalidSubmit?: OnInvalidSubmitPolicy;
|
|
1084
|
+
/**
|
|
1085
|
+
* When per-field VALIDATION runs (the directive's listener controls
|
|
1086
|
+
* how often storage commits — per keystroke by default, per blur
|
|
1087
|
+
* with `.lazy`). Default `'change'`. See `ValidateOn` for mode
|
|
1088
|
+
* semantics.
|
|
1089
|
+
*
|
|
1090
|
+
* The strict public `useForm` signature wraps this type in an
|
|
1091
|
+
* intersection with `ValidateOnConfig`, which enforces that
|
|
1092
|
+
* `debounceMs` is only allowed under `'change'`. Internal callers
|
|
1093
|
+
* (adapters, hydration paths) work with the loose form below.
|
|
1094
|
+
*/
|
|
1095
|
+
validateOn?: ValidateOn;
|
|
1096
|
+
/**
|
|
1097
|
+
* Milliseconds to wait after the last committed write before
|
|
1098
|
+
* running validation. Default `0` (validation fires synchronously
|
|
1099
|
+
* after the write; no `setTimeout`). Set to a positive number to
|
|
1100
|
+
* coalesce rapid bursts. Ignored under `validateOn: 'blur'` and
|
|
1101
|
+
* `'submit'`.
|
|
1102
|
+
*
|
|
1103
|
+
* This is purely a VALIDATION debounce — storage commits are the
|
|
1104
|
+
* directive's concern (per keystroke for `<input v-register>`,
|
|
1105
|
+
* per blur for `<input v-register.lazy>`).
|
|
1106
|
+
*/
|
|
1107
|
+
debounceMs?: number;
|
|
1108
|
+
/**
|
|
1109
|
+
* Opt-in persistence of the form's draft state. Off by default —
|
|
1110
|
+
* with no config, no reads, no writes, no storage code is loaded.
|
|
1111
|
+
*
|
|
1112
|
+
* Three input forms; pick the one that reads best:
|
|
1113
|
+
*
|
|
1114
|
+
* ```ts
|
|
1115
|
+
* useForm({ persist: 'local' }) // built-in backend
|
|
1116
|
+
* useForm({ persist: encryptedStorage }) // custom backend
|
|
1117
|
+
* useForm({ persist: { storage: 'local', debounceMs: 500 } })
|
|
1118
|
+
* ```
|
|
1119
|
+
*
|
|
1120
|
+
* Per-field opt-in is required: every field that should actually
|
|
1121
|
+
* persist needs `register(path, { persist: true })`. Without any
|
|
1122
|
+
* opt-ins, the form mounts but never writes to storage — and a
|
|
1123
|
+
* dev-mode warning surfaces the misconfiguration. This guard
|
|
1124
|
+
* prevents sensitive fields from accidentally leaking to
|
|
1125
|
+
* client-side storage.
|
|
1126
|
+
*
|
|
1127
|
+
* Switching backends across reloads (e.g. `'local'` → `'session'`)
|
|
1128
|
+
* automatically clears the previous backend's entry so old drafts
|
|
1129
|
+
* don't orphan.
|
|
1130
|
+
*/
|
|
1131
|
+
persist?: PersistConfig;
|
|
1132
|
+
/**
|
|
1133
|
+
* Opt-in undo/redo. Off by default. `true` enables with a 50-snapshot
|
|
1134
|
+
* cap; `{ max: N }` tunes the cap.
|
|
1135
|
+
*
|
|
1136
|
+
* Every mutation pushes a snapshot. `undo()` pops one; `redo()`
|
|
1137
|
+
* replays it. `reset()` clears history. Reactive flags
|
|
1138
|
+
* `state.canUndo` / `state.canRedo` / `state.historySize` reflect
|
|
1139
|
+
* the current stack.
|
|
1140
|
+
*/
|
|
1141
|
+
history?: HistoryConfig;
|
|
1142
|
+
/**
|
|
1143
|
+
* Whether to remember the typed state of each discriminated-union
|
|
1144
|
+
* variant across switches. Default `true`.
|
|
1145
|
+
*
|
|
1146
|
+
* When `true`, switching `notify.channel` from `email` (with
|
|
1147
|
+
* `address: 'foo@bar.com'`) to `sms` and back lands on
|
|
1148
|
+
* `address: 'foo@bar.com'` again — the runtime snapshots the
|
|
1149
|
+
* outgoing variant's subtree on switch-out and restores the
|
|
1150
|
+
* incoming variant's prior subtree on switch-in. Each
|
|
1151
|
+
* discriminated union at every nesting depth is independently
|
|
1152
|
+
* memorized.
|
|
1153
|
+
*
|
|
1154
|
+
* Set to `false` to drop the outgoing variant's typed state on
|
|
1155
|
+
* every switch (the data is gone). The new variant initializes
|
|
1156
|
+
* from its slim default.
|
|
1157
|
+
*
|
|
1158
|
+
* Memory is in-memory only and does not survive reload. Persisted
|
|
1159
|
+
* state restores values into form storage on hydration, but
|
|
1160
|
+
* variant memory starts empty — the first discriminator switch
|
|
1161
|
+
* after reload loses any persisted typing in the outgoing variant.
|
|
1162
|
+
* Consumers needing cross-session continuity must persist beyond
|
|
1163
|
+
* the variant boundary themselves.
|
|
1164
|
+
*
|
|
1165
|
+
* `reset()` clears variant memory. `resetField(path)` clears any
|
|
1166
|
+
* memory entry whose union path equals or sits under `path`.
|
|
1167
|
+
*/
|
|
1168
|
+
rememberVariants?: boolean;
|
|
1169
|
+
/**
|
|
1170
|
+
* Schema-driven coercion of user-typed DOM values at the v-register
|
|
1171
|
+
* directive layer. Per-form override of the plugin-level
|
|
1172
|
+
* `AttaformDefaults.coerce`.
|
|
1173
|
+
*
|
|
1174
|
+
* - `true` / `undefined` — runs the built-in `defaultCoercionRules`.
|
|
1175
|
+
* - `false` — disables coercion; the slim gate rejects mismatches.
|
|
1176
|
+
* - `CoercionRegistry` — a custom array of entries (REPLACES, not
|
|
1177
|
+
* merges, the plugin defaults). Spread `defaultCoercionRules` to
|
|
1178
|
+
* extend.
|
|
1179
|
+
*
|
|
1180
|
+
* Coercion applies ONLY to user-typed DOM values. Programmatic
|
|
1181
|
+
* writes (`form.setValue`, `setValueWithInternalPath`) are NEVER
|
|
1182
|
+
* coerced.
|
|
1183
|
+
*/
|
|
1184
|
+
coerce?: boolean | CoercionRegistry;
|
|
1185
|
+
};
|
|
1186
|
+
/**
|
|
1187
|
+
* App-level defaults applied to every `useForm` call. Set these once
|
|
1188
|
+
* per app via `createAttaform({ defaults })` (bare Vue) or
|
|
1189
|
+
* `attaform.defaults` (Nuxt module).
|
|
1190
|
+
*
|
|
1191
|
+
* Resolution order (per-form wins):
|
|
1192
|
+
*
|
|
1193
|
+
* useForm({ ... }) > createAttaform({ defaults }) > library default
|
|
1194
|
+
*
|
|
1195
|
+
* `validateOn` and `debounceMs` resolve per-field — set the debounce
|
|
1196
|
+
* globally while still overriding the trigger per form:
|
|
1197
|
+
*
|
|
1198
|
+
* ```ts
|
|
1199
|
+
* createAttaform({
|
|
1200
|
+
* defaults: { debounceMs: 100 },
|
|
1201
|
+
* })
|
|
1202
|
+
* // later
|
|
1203
|
+
* useForm({ schema, validateOn: 'blur' })
|
|
1204
|
+
* // → { validateOn: 'blur', debounceMs: <ignored under blur> }
|
|
1205
|
+
* ```
|
|
1206
|
+
*
|
|
1207
|
+
* Note: per the discriminated union, `debounceMs` only takes effect
|
|
1208
|
+
* when `validateOn` is `'change'` (or omitted). Setting it as an
|
|
1209
|
+
* app-level default is fine — forms that switch to `'blur'` /
|
|
1210
|
+
* `'submit'` simply ignore the inherited `debounceMs`.
|
|
1211
|
+
*
|
|
1212
|
+
* `schema`, `key`, `defaultValues`, and `persist` are not configurable
|
|
1213
|
+
* here — they belong on the per-form call.
|
|
1214
|
+
*/
|
|
1215
|
+
type AttaformDefaults = {
|
|
1216
|
+
/** Default for `useForm({ strict })`. Default `true`. */
|
|
1217
|
+
strict?: boolean;
|
|
1218
|
+
/** Default for `useForm({ onInvalidSubmit })`. */
|
|
1219
|
+
onInvalidSubmit?: OnInvalidSubmitPolicy;
|
|
1220
|
+
/** Default for `useForm({ validateOn })` — when validation runs. */
|
|
1221
|
+
validateOn?: ValidateOn;
|
|
1222
|
+
/**
|
|
1223
|
+
* Default for `useForm({ debounceMs })` — ms to wait after the last
|
|
1224
|
+
* input event before re-running validation. Only meaningful when
|
|
1225
|
+
* `validateOn` resolves to `'change'`. Default `0` (synchronous).
|
|
1226
|
+
*/
|
|
1227
|
+
debounceMs?: number;
|
|
1228
|
+
/** Default for `useForm({ history })`. */
|
|
1229
|
+
history?: HistoryConfig;
|
|
1230
|
+
/** Default for `useForm({ rememberVariants })`. */
|
|
1231
|
+
rememberVariants?: boolean;
|
|
1232
|
+
/**
|
|
1233
|
+
* Default for `useForm({ coerce })`. Schema-driven coercion of
|
|
1234
|
+
* user-typed DOM values at the v-register directive layer.
|
|
1235
|
+
*
|
|
1236
|
+
* - `true` (default) — runs the built-in `defaultCoercionRules`
|
|
1237
|
+
* (`string→number`, `string→boolean`).
|
|
1238
|
+
* - `false` — disables coercion globally; the slim-primitive gate
|
|
1239
|
+
* rejects type mismatches with its existing dev-warn instead.
|
|
1240
|
+
* - `CoercionRegistry` — a custom array of `CoercionEntry` records.
|
|
1241
|
+
* Spread `defaultCoercionRules` to extend rather than replace:
|
|
1242
|
+
* `[...defaultCoercionRules, defineCoercion({ ... })]`.
|
|
1243
|
+
*
|
|
1244
|
+
* Coercion applies ONLY to user-typed DOM values flowing through
|
|
1245
|
+
* the directive's assigner. Programmatic writes (`form.setValue`,
|
|
1246
|
+
* `setValueWithInternalPath`) are NEVER coerced — they're
|
|
1247
|
+
* authoritative writes whose strict typing is on the caller.
|
|
1248
|
+
*/
|
|
1249
|
+
coerce?: boolean | CoercionRegistry;
|
|
1250
|
+
};
|
|
1251
|
+
/**
|
|
1252
|
+
* Callback invoked by `handleSubmit` after the form parses successfully.
|
|
1253
|
+
* Receives the strictly-typed parsed value — refinements have run, so
|
|
1254
|
+
* enum / literal / format constraints are honoured.
|
|
1255
|
+
*/
|
|
1256
|
+
type OnSubmit<Form extends GenericForm> = (form: Form) => void | Promise<void>;
|
|
1257
|
+
/**
|
|
1258
|
+
* Callback invoked by `handleSubmit` when validation fails. Receives
|
|
1259
|
+
* the full list of errors. Bind this when you want to react to
|
|
1260
|
+
* submit failures explicitly (alongside or instead of the
|
|
1261
|
+
* automatic `onInvalidSubmit` UI nudge).
|
|
1262
|
+
*/
|
|
1263
|
+
type OnError = (error: ValidationError[]) => void | Promise<void>;
|
|
1264
|
+
/**
|
|
1265
|
+
* Submit handler returned by `handleSubmit(onSubmit, onError)`. Bind
|
|
1266
|
+
* it to a `<form>`:
|
|
1267
|
+
*
|
|
1268
|
+
* ```vue
|
|
1269
|
+
* <form @submit.prevent="onSubmit">…</form>
|
|
1270
|
+
* ```
|
|
1271
|
+
*
|
|
1272
|
+
* It optionally accepts the originating `Event` so it can sit on
|
|
1273
|
+
* `@submit` directly (without `.prevent` if you want to call
|
|
1274
|
+
* `event.preventDefault()` yourself).
|
|
1275
|
+
*/
|
|
1276
|
+
type SubmitHandler = (event?: Event) => Promise<void>;
|
|
1277
|
+
/**
|
|
1278
|
+
* Type of `form.handleSubmit`. Pass an `onSubmit` callback for the
|
|
1279
|
+
* happy path and (optionally) an `onError` callback that receives
|
|
1280
|
+
* the validation errors when parsing fails.
|
|
1281
|
+
*
|
|
1282
|
+
* ```ts
|
|
1283
|
+
* const onSubmit = form.handleSubmit(
|
|
1284
|
+
* (data) => api.signup(data),
|
|
1285
|
+
* (errors) => console.log(errors),
|
|
1286
|
+
* )
|
|
1287
|
+
* ```
|
|
1288
|
+
*/
|
|
1289
|
+
type HandleSubmit<Form extends GenericForm> = (onSubmit: OnSubmit<Form>, onError?: OnError) => SubmitHandler;
|
|
1290
|
+
/**
|
|
1291
|
+
* Per-leaf metadata tracked alongside a field's value. Read from
|
|
1292
|
+
* `FieldState.meta` when type-narrowing through that surface.
|
|
1293
|
+
*
|
|
1294
|
+
* - `updatedAt` — ISO timestamp of the most recent write at this path,
|
|
1295
|
+
* or `null` if the field has never been written.
|
|
1296
|
+
* - `rawValue` — the value as it arrived (before any transform);
|
|
1297
|
+
* useful for distinguishing parse-coerced reads from raw user input.
|
|
1298
|
+
* - `isConnected` — whether at least one DOM element bound to this
|
|
1299
|
+
* path is currently mounted. Flips to `false` when every binding
|
|
1300
|
+
* unmounts.
|
|
1301
|
+
* - `formKey` — identifier of the form this metadata belongs to.
|
|
1302
|
+
* - `path` — dotted-string path to this leaf, or `null` when not applicable.
|
|
1303
|
+
*/
|
|
1304
|
+
type MetaTrackerValue = {
|
|
1305
|
+
/** ISO timestamp of the most recent write at this path. `null` if never written. */
|
|
1306
|
+
updatedAt: string | null;
|
|
1307
|
+
/** Value as it arrived, before any transforms. */
|
|
1308
|
+
rawValue: unknown;
|
|
1309
|
+
/** `true` while at least one binding to this path is currently mounted. */
|
|
1310
|
+
isConnected: boolean;
|
|
1311
|
+
/** Form this metadata belongs to. */
|
|
1312
|
+
formKey: FormKey;
|
|
1313
|
+
/** Dotted-string path to this leaf. */
|
|
1314
|
+
path: string | null;
|
|
1315
|
+
/**
|
|
1316
|
+
* `true` when this field is **blank** — the runtime has recorded
|
|
1317
|
+
* that storage and the visible display diverge here. Reserved for
|
|
1318
|
+
* the case the schema can't see on its own: storage forces a
|
|
1319
|
+
* value (e.g. `0` for a numeric leaf, `0n` for a bigint leaf)
|
|
1320
|
+
* while the DOM input shows `''`, and the runtime needs a side-
|
|
1321
|
+
* channel to tell "user typed 0" from "user supplied nothing."
|
|
1322
|
+
*
|
|
1323
|
+
* Set automatically for numeric leaves (the directive's input
|
|
1324
|
+
* listener on clear; the construction-time pass when the consumer
|
|
1325
|
+
* didn't supply a value). Set explicitly for any primitive leaf
|
|
1326
|
+
* via `setValue(path, unset)` / `defaultValues: { x: unset }` /
|
|
1327
|
+
* `reset({ x: unset })` — that's the documented opt-in signal for
|
|
1328
|
+
* strings, booleans, and other types that don't otherwise diverge.
|
|
1329
|
+
* Cleared on the first non-`unset` write.
|
|
1330
|
+
*
|
|
1331
|
+
* `errors = f(schema, state)` is reactive end-to-end: any required
|
|
1332
|
+
* path with `blank: true` produces a "No value supplied" entry in
|
|
1333
|
+
* `form.errors` immediately, no `validate()` / `handleSubmit` call
|
|
1334
|
+
* required. Most consumers don't need this flag directly — gate UI
|
|
1335
|
+
* on `errors[path]` and `touched`. Read `blank` itself when you
|
|
1336
|
+
* want pre-error introspection ("the user hasn't decided yet"
|
|
1337
|
+
* indicator, "review unanswered fields" hint).
|
|
1338
|
+
*
|
|
1339
|
+
* See `docs/blank.md` for the full conceptual model.
|
|
1340
|
+
*/
|
|
1341
|
+
blank: boolean;
|
|
1342
|
+
};
|
|
1343
|
+
type RegisterFlatPath<Form, Key extends keyof Form = keyof Form> = IsObjectOrArray<Form> extends true ? Key extends string ? Form[Key] extends infer Value ? Value extends Array<infer ArrayItem> ? IsObjectOrArray<ArrayItem> extends true ? `${Key}.${number}.${RegisterFlatPath<ArrayItem>}` : `${Key}` | `${Key}.${number}` : Value extends GenericForm ? `${Key}.${RegisterFlatPath<Value>}` : `${Key}` : never : Key extends number ? `${Key}` | (Form[Key] extends GenericForm ? `${Key}.${RegisterFlatPath<Form[Key]>}` : Form[Key] extends Array<infer ArrayItem> ? IsObjectOrArray<ArrayItem> extends true ? `${Key}.${number}.${RegisterFlatPath<ArrayItem>}` : `${Key}` | `${Key}.${number}` : never) : never : never;
|
|
1344
|
+
/**
|
|
1345
|
+
* Sync transformation applied to a field's value as user input flows
|
|
1346
|
+
* from DOM through the directive's assigner. Composes left-to-right
|
|
1347
|
+
* via the `transforms: [...]` array on `register()`.
|
|
1348
|
+
*
|
|
1349
|
+
* The shape is intentionally generic-erased (`(unknown) => unknown`)
|
|
1350
|
+
* rather than per-path-typed: a personal library of transforms
|
|
1351
|
+
* (`trim`, `lowercase`, `slugify`, `clamp`, …) should plug into any
|
|
1352
|
+
* `register()` slot regardless of the path's value type. Library
|
|
1353
|
+
* authors write defensive bodies that no-op on type mismatch:
|
|
1354
|
+
*
|
|
1355
|
+
* ```ts
|
|
1356
|
+
* export const trim: RegisterTransform = (v) =>
|
|
1357
|
+
* typeof v === 'string' ? v.trim() : v
|
|
1358
|
+
* ```
|
|
1359
|
+
*
|
|
1360
|
+
* Type-safety at the call site is delegated to attaform's slim-primitive
|
|
1361
|
+
* gate — a transform that produces a value the path's storage
|
|
1362
|
+
* doesn't accept gets rejected at write time with a standard
|
|
1363
|
+
* diagnostic.
|
|
1364
|
+
*
|
|
1365
|
+
* Transforms must be sync. A `Promise` return is treated as a
|
|
1366
|
+
* pipeline failure: the write is aborted and a console.error is
|
|
1367
|
+
* logged. Use async field validation for canonicalize-before-write
|
|
1368
|
+
* patterns; use sync transforms for fire-and-forget side effects
|
|
1369
|
+
* (`void doIt(value); return value`).
|
|
1370
|
+
*
|
|
1371
|
+
* Throws are caught and aborted: attaform wraps each transform call in
|
|
1372
|
+
* try/catch so a buggy or defensive-throw transform doesn't crash
|
|
1373
|
+
* the host app. On throw the pipeline aborts (subsequent transforms
|
|
1374
|
+
* don't run), nothing is written to form state, and the assigner
|
|
1375
|
+
* returns `false`.
|
|
1376
|
+
*/
|
|
1377
|
+
type RegisterTransform = (value: unknown) => unknown;
|
|
1378
|
+
/**
|
|
1379
|
+
* Runtime type for a slim primitive kind. Used to narrow the
|
|
1380
|
+
* `transform` parameter and return value on a `CoercionEntry` so
|
|
1381
|
+
* authors writing rules don't have to cast `unknown`.
|
|
1382
|
+
*
|
|
1383
|
+
* Exhaustive over `SlimPrimitiveKind` — adding a new kind to that
|
|
1384
|
+
* union must add a corresponding branch here.
|
|
1385
|
+
*/
|
|
1386
|
+
type SlimRuntimeOf<K extends SlimPrimitiveKind> = K extends 'string' ? string : K extends 'number' ? number : K extends 'boolean' ? boolean : K extends 'bigint' ? bigint : K extends 'date' ? Date : K extends 'null' ? null : K extends 'undefined' ? undefined : K extends 'array' ? readonly unknown[] : K extends 'set' ? ReadonlySet<unknown> : K extends 'map' ? ReadonlyMap<unknown, unknown> : K extends 'object' ? Record<string, unknown> : K extends 'symbol' ? symbol : K extends 'function' ? (...args: never[]) => unknown : never;
|
|
1387
|
+
/**
|
|
1388
|
+
* Outcome of a coercion attempt.
|
|
1389
|
+
*
|
|
1390
|
+
* - `coerced: true` — the rule produced `value`, which the directive
|
|
1391
|
+
* forwards to the slim gate (the gate may still reject if the
|
|
1392
|
+
* value doesn't satisfy the path's accept set).
|
|
1393
|
+
* - `coerced: false` — the rule decided it can't coerce this input.
|
|
1394
|
+
* The directive passes the original value through; the slim gate
|
|
1395
|
+
* decides downstream.
|
|
1396
|
+
*
|
|
1397
|
+
* Discriminated rather than `O | undefined` so rules with
|
|
1398
|
+
* `output: 'undefined'` or `output: 'null'` don't conflict with the
|
|
1399
|
+
* "skip" signal.
|
|
1400
|
+
*/
|
|
1401
|
+
type CoercionResult<O> = {
|
|
1402
|
+
coerced: true;
|
|
1403
|
+
value: O;
|
|
1404
|
+
} | {
|
|
1405
|
+
coerced: false;
|
|
1406
|
+
};
|
|
1407
|
+
/**
|
|
1408
|
+
* A single coercion rule. `input` and `output` are
|
|
1409
|
+
* `SlimPrimitiveKind` literals; `transform` receives a value already
|
|
1410
|
+
* narrowed to `SlimRuntimeOf<input>` and returns
|
|
1411
|
+
* `CoercionResult<SlimRuntimeOf<output>>`.
|
|
1412
|
+
*
|
|
1413
|
+
* Rules MUST be sync. They SHOULD NOT throw — wrap internal
|
|
1414
|
+
* try/catch when the conversion can fail (e.g. `BigInt(s)` throws
|
|
1415
|
+
* for non-numeric strings). The library wraps each invocation in
|
|
1416
|
+
* try/catch as defense in depth; throws are caught, logged once per
|
|
1417
|
+
* `(input, output)`, and the original value passes through.
|
|
1418
|
+
*/
|
|
1419
|
+
type CoercionEntry<I extends SlimPrimitiveKind = SlimPrimitiveKind, O extends SlimPrimitiveKind = SlimPrimitiveKind> = {
|
|
1420
|
+
readonly input: I;
|
|
1421
|
+
readonly output: O;
|
|
1422
|
+
readonly transform: (value: SlimRuntimeOf<I>) => CoercionResult<SlimRuntimeOf<O>>;
|
|
1423
|
+
};
|
|
1424
|
+
/**
|
|
1425
|
+
* A registry is an ordered array of `CoercionEntry` records.
|
|
1426
|
+
* Consumers compose by spreading `defaultCoercionRules` and
|
|
1427
|
+
* appending their own entries. Order is observable only when two
|
|
1428
|
+
* entries share the same `(input, output)` pair — the library emits
|
|
1429
|
+
* a one-shot dev-warn and the LATER entry wins.
|
|
1430
|
+
*/
|
|
1431
|
+
type CoercionRegistry = readonly CoercionEntry[];
|
|
1432
|
+
/**
|
|
1433
|
+
* Options for `register(path, options)`. Per-field rather than
|
|
1434
|
+
* per-form so each persisted path is opted in at its own call site —
|
|
1435
|
+
* adding a new field can't accidentally leak into the persistence
|
|
1436
|
+
* pipeline unless the field's `register` call says so explicitly.
|
|
1437
|
+
*/
|
|
1438
|
+
type RegisterOptions = {
|
|
1439
|
+
/**
|
|
1440
|
+
* Opt this field into the form's persistence pipeline. The form
|
|
1441
|
+
* also needs `useForm({ persist })` configured for any storage
|
|
1442
|
+
* activity to happen.
|
|
1443
|
+
*
|
|
1444
|
+
* Persistence follows the field's lifecycle: writes flow on
|
|
1445
|
+
* mount, the field is dropped from the persisted draft on unmount.
|
|
1446
|
+
* If multiple inputs bind to the same path, the path keeps
|
|
1447
|
+
* persisting as long as any opted-in input is mounted.
|
|
1448
|
+
*
|
|
1449
|
+
* Throws `SensitivePersistFieldError` when the path looks
|
|
1450
|
+
* sensitive (password / cvv / ssn / token / etc.) unless
|
|
1451
|
+
* `acknowledgeSensitive: true` is also set.
|
|
1452
|
+
*/
|
|
1453
|
+
persist?: boolean;
|
|
1454
|
+
/**
|
|
1455
|
+
* Suppress the sensitive-name guard. Required to persist any path
|
|
1456
|
+
* whose name matches the heuristic (password, cvv, ssn, etc.).
|
|
1457
|
+
* Treat this as a code-review checkpoint: setting it should be a
|
|
1458
|
+
* deliberate decision that the path's data is safe to land in
|
|
1459
|
+
* client-side storage for this user's session.
|
|
1460
|
+
*/
|
|
1461
|
+
acknowledgeSensitive?: boolean;
|
|
1462
|
+
/**
|
|
1463
|
+
* Sync transformation pipeline applied to user-typed values before
|
|
1464
|
+
* they reach form state. Composes left-to-right: each transform
|
|
1465
|
+
* receives the previous transform's output (or the directive-
|
|
1466
|
+
* extracted DOM value for the first transform).
|
|
1467
|
+
*
|
|
1468
|
+
* Pipeline order:
|
|
1469
|
+
* `DOM event → modifier cast (.lazy/.trim/.number) → transforms[0] → … → transforms[n] → assigner`
|
|
1470
|
+
*
|
|
1471
|
+
* Applies to user input only. Programmatic writes
|
|
1472
|
+
* (`form.setValue(...)`, `rv.setValueWithInternalPath(...)`),
|
|
1473
|
+
* `form.reset()`, hydration, SSR replay, and `markBlank()` all
|
|
1474
|
+
* bypass transforms — those write canonical state, not normalized
|
|
1475
|
+
* user input. If you want the same normalization on a programmatic
|
|
1476
|
+
* write, compose the transforms yourself at the call site:
|
|
1477
|
+
*
|
|
1478
|
+
* ```ts
|
|
1479
|
+
* form.setValue('email', slugify(lowercase(rawValue)))
|
|
1480
|
+
* ```
|
|
1481
|
+
*
|
|
1482
|
+
* Transforms must be sync. Throws and Promise returns abort the
|
|
1483
|
+
* write and log to `console.error` (see `RegisterTransform` for
|
|
1484
|
+
* the failure-mode contract).
|
|
1485
|
+
*
|
|
1486
|
+
* For patterns that need to inspect the `RegisterValue` itself
|
|
1487
|
+
* (rejection-with-side-effect, redirection to other fields, custom
|
|
1488
|
+
* DOM mutation), use `@update:registerValue` on the bound element
|
|
1489
|
+
* instead — see the "Custom assigners" section in the API docs.
|
|
1490
|
+
*/
|
|
1491
|
+
transforms?: ReadonlyArray<RegisterTransform>;
|
|
1492
|
+
};
|
|
1493
|
+
/**
|
|
1494
|
+
* The object returned by `form.register(path)`. Pass it to a native
|
|
1495
|
+
* input via `v-register`:
|
|
1496
|
+
*
|
|
1497
|
+
* ```vue
|
|
1498
|
+
* <input v-register="form.register('email')" />
|
|
1499
|
+
* ```
|
|
1500
|
+
*
|
|
1501
|
+
* Or read `innerRef` directly when integrating with custom components.
|
|
1502
|
+
*
|
|
1503
|
+
* The remaining fields support advanced bindings (custom assigners,
|
|
1504
|
+
* SSR optimistic marking, persistence opt-ins). Most consumers only
|
|
1505
|
+
* touch `innerRef`.
|
|
1506
|
+
*/
|
|
1507
|
+
type RegisterValue<Value = unknown> = {
|
|
1508
|
+
/**
|
|
1509
|
+
* Live, read-only reactive value at this path. Watch it to drive
|
|
1510
|
+
* UI that depends on the field's current value.
|
|
1511
|
+
*/
|
|
1512
|
+
innerRef: Readonly<Ref<Value>>;
|
|
1513
|
+
/**
|
|
1514
|
+
* Attach an HTML element to this binding. Called by `v-register`
|
|
1515
|
+
* automatically; expose it to custom integrations that need to
|
|
1516
|
+
* register an element manually.
|
|
1517
|
+
*/
|
|
1518
|
+
registerElement: (el: HTMLElement) => void;
|
|
1519
|
+
/**
|
|
1520
|
+
* Detach an HTML element from this binding. Pair with
|
|
1521
|
+
* `registerElement` for custom integrations.
|
|
1522
|
+
*/
|
|
1523
|
+
deregisterElement: (el: HTMLElement) => void;
|
|
1524
|
+
/**
|
|
1525
|
+
* Write the field's value programmatically. Returns `true` when the
|
|
1526
|
+
* write was accepted, `false` when it was rejected (e.g. wrong
|
|
1527
|
+
* primitive type for the path). The optional `meta` lets custom
|
|
1528
|
+
* directives signal whether the write should be persisted.
|
|
1529
|
+
*/
|
|
1530
|
+
setValueWithInternalPath: (value: unknown, meta?: WriteMeta) => boolean;
|
|
1531
|
+
/**
|
|
1532
|
+
* Read-only, string-form view of the field's current value — what
|
|
1533
|
+
* the compile-time `:value` injection reads on every input /
|
|
1534
|
+
* textarea / select bound by `v-register`.
|
|
1535
|
+
*
|
|
1536
|
+
* Returns `''` when the path is in the form's `blankPaths`
|
|
1537
|
+
* set OR storage is `null` / `undefined`; otherwise stringifies
|
|
1538
|
+
* the storage value via `String(...)`. The blank branch
|
|
1539
|
+
* lets the user clear a numeric field without the next Vue render
|
|
1540
|
+
* patching `el.value` back to `'0'` (the slim default).
|
|
1541
|
+
*/
|
|
1542
|
+
displayValue: Readonly<Ref<string>>;
|
|
1543
|
+
};
|
|
1544
|
+
/**
|
|
1545
|
+
* Custom assigner installed on an element via the directive's
|
|
1546
|
+
* `[assignKey]` slot OR an `@update:registerValue` listener. Called
|
|
1547
|
+
* by the directive when a DOM event (input / change / etc.) fires
|
|
1548
|
+
* on the bound element.
|
|
1549
|
+
*
|
|
1550
|
+
* The directive passes the extracted value plus the `RegisterValue`
|
|
1551
|
+
* the directive is currently bound to. The second arg lets a
|
|
1552
|
+
* top-level handler write back to form state without having to
|
|
1553
|
+
* capture the RV via closure:
|
|
1554
|
+
*
|
|
1555
|
+
* ```ts
|
|
1556
|
+
* function upperCaseAssigner(value: unknown, rv: RegisterValue): void {
|
|
1557
|
+
* rv.setValueWithInternalPath(String(value ?? '').toUpperCase())
|
|
1558
|
+
* }
|
|
1559
|
+
* ```
|
|
1560
|
+
*
|
|
1561
|
+
* `registerValue` is omitted only for assigners installed directly
|
|
1562
|
+
* via `el[assignKey] = fn` — those callers already have the RV in
|
|
1563
|
+
* scope at install time.
|
|
1564
|
+
*
|
|
1565
|
+
* Return `true` when the write was accepted, `false` when it was
|
|
1566
|
+
* rejected (e.g. the value didn't match the path's expected type).
|
|
1567
|
+
* `undefined` is treated as "succeeded" so simple assigners can
|
|
1568
|
+
* just return `void`.
|
|
1569
|
+
*/
|
|
1570
|
+
type CustomDirectiveRegisterAssignerFn = (value: unknown, registerValue?: RegisterValue) => boolean | undefined;
|
|
1571
|
+
/**
|
|
1572
|
+
* Generic shape of a v-register directive variant. Used by the
|
|
1573
|
+
* library's text / checkbox / radio / select directive types and
|
|
1574
|
+
* available for custom integrations that need to drop in their own
|
|
1575
|
+
* variant.
|
|
1576
|
+
*
|
|
1577
|
+
* The value generic admits `undefined` because `useRegister()` may
|
|
1578
|
+
* return `undefined` (a wrapper component rendered without a parent
|
|
1579
|
+
* `registerValue`); binding that value to `v-register` is supported
|
|
1580
|
+
* and installs a no-op assigner at runtime.
|
|
1581
|
+
*/
|
|
1582
|
+
type CustomRegisterDirective<T, Modifiers extends string = string> = ObjectDirective<T & {
|
|
1583
|
+
_assigning?: boolean;
|
|
1584
|
+
/**
|
|
1585
|
+
* Snapshot of the last `value.innerRef.value` reference the
|
|
1586
|
+
* directive's DOM-sync (setSelected / setChecked / radio
|
|
1587
|
+
* `el.checked = …`) was applied for. Used by every input
|
|
1588
|
+
* directive's `updated` / `beforeUpdate` to skip the per-render
|
|
1589
|
+
* DOM sync when the model is identity-unchanged — preventing
|
|
1590
|
+
* parent re-renders (a typed character in a sibling, an async-
|
|
1591
|
+
* validation tick, any reactive read) from clobbering an in-
|
|
1592
|
+
* progress user interaction. Identity comparison is sound:
|
|
1593
|
+
* every form write produces a fresh value at the path (scalars
|
|
1594
|
+
* are new primitives; arrays/Sets get fresh references along the
|
|
1595
|
+
* spine via diff-apply), so reference equality on
|
|
1596
|
+
* `innerRef.value` tracks "did the model move" exactly. The
|
|
1597
|
+
* `_assigning` gate stays alongside — it short-circuits the
|
|
1598
|
+
* immediate post-write render where the DOM is already in sync
|
|
1599
|
+
* from the user's input.
|
|
1600
|
+
*/
|
|
1601
|
+
_lastAppliedModel?: unknown;
|
|
1602
|
+
[S: symbol]: CustomDirectiveRegisterAssignerFn;
|
|
1603
|
+
}, RegisterValue | undefined, Modifiers, string>;
|
|
1604
|
+
/**
|
|
1605
|
+
* Modifier names supported by `v-register` on `<input type="text">`,
|
|
1606
|
+
* `<input type="number">`, and `<textarea>`. Mirrors Vue's
|
|
1607
|
+
* `v-model` modifier semantics on the same elements; combine freely
|
|
1608
|
+
* (`<input v-register.lazy.trim.number="..." />`).
|
|
1609
|
+
*/
|
|
1610
|
+
type RegisterTextModifier =
|
|
1611
|
+
/**
|
|
1612
|
+
* Write on `change` (blur) instead of `input`. The reactive
|
|
1613
|
+
* model only updates after the user tabs/clicks out of the
|
|
1614
|
+
* field. IME composition handlers are skipped under `.lazy` —
|
|
1615
|
+
* composition events do not gate writes.
|
|
1616
|
+
*/
|
|
1617
|
+
'lazy'
|
|
1618
|
+
/**
|
|
1619
|
+
* Strip leading and trailing whitespace on blur. The form holds
|
|
1620
|
+
* the user's raw input (whitespace included) while they're
|
|
1621
|
+
* typing; on `change` (blur / commit) the value is trimmed
|
|
1622
|
+
* once and written back to both the model and the visible DOM.
|
|
1623
|
+
* Combine with `.lazy` to skip the mid-typing writes entirely.
|
|
1624
|
+
*/
|
|
1625
|
+
| 'trim'
|
|
1626
|
+
/**
|
|
1627
|
+
* Cast the value via `parseFloat` before writing. Values that
|
|
1628
|
+
* can't be parsed as a number (e.g. `'abc'`) pass through
|
|
1629
|
+
* unchanged — the slim-primitive gate then sees a string
|
|
1630
|
+
* heading to a numeric slot and rejects the write. Auto-applied
|
|
1631
|
+
* for `<input type="number">`; explicit `.number` is redundant
|
|
1632
|
+
* there.
|
|
1633
|
+
*/
|
|
1634
|
+
| 'number';
|
|
1635
|
+
/**
|
|
1636
|
+
* v-register directive variant for `<input type="text">`,
|
|
1637
|
+
* `<input type="number">`, and `<textarea>`. Supports the
|
|
1638
|
+
* `.lazy`, `.trim`, and `.number` modifiers — see
|
|
1639
|
+
* `RegisterTextModifier` for per-modifier semantics.
|
|
1640
|
+
*/
|
|
1641
|
+
type RegisterTextCustomDirective = CustomRegisterDirective<HTMLInputElement | HTMLTextAreaElement, RegisterTextModifier>;
|
|
1642
|
+
/** v-register directive variant for checkboxes. No modifiers. */
|
|
1643
|
+
type RegisterCheckboxCustomDirective = CustomRegisterDirective<HTMLInputElement>;
|
|
1644
|
+
/** v-register directive variant for radio inputs. No modifiers. */
|
|
1645
|
+
type RegisterRadioCustomDirective = CustomRegisterDirective<HTMLInputElement>;
|
|
1646
|
+
/**
|
|
1647
|
+
* Modifier name supported by `v-register` on `<select>`. Mirrors
|
|
1648
|
+
* Vue's `v-model` `.number` on the same element.
|
|
1649
|
+
*/
|
|
1650
|
+
type RegisterSelectModifier =
|
|
1651
|
+
/**
|
|
1652
|
+
* Cast each selected option's `value` via `parseFloat` before
|
|
1653
|
+
* writing. The form state holds numbers, not numeric strings —
|
|
1654
|
+
* useful when option values are written as strings in the
|
|
1655
|
+
* markup but the schema expects numbers.
|
|
1656
|
+
*/
|
|
1657
|
+
'number';
|
|
1658
|
+
/**
|
|
1659
|
+
* v-register directive variant for `<select>`. Supports `.number`
|
|
1660
|
+
* — see `RegisterSelectModifier` for semantics.
|
|
1661
|
+
*/
|
|
1662
|
+
type RegisterSelectCustomDirective = CustomRegisterDirective<HTMLSelectElement, RegisterSelectModifier>;
|
|
1663
|
+
/** v-register directive variant for the dynamic input/select/textarea bridge. */
|
|
1664
|
+
type RegisterModelDynamicCustomDirective = ObjectDirective<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement, RegisterValue | undefined, string>;
|
|
1665
|
+
/**
|
|
1666
|
+
* The `v-register` directive. Binds a form field to a native
|
|
1667
|
+
* input, select, textarea, checkbox, or radio:
|
|
1668
|
+
*
|
|
1669
|
+
* ```vue
|
|
1670
|
+
* <input v-register="form.register('email')" />
|
|
1671
|
+
* <select v-register="form.register('country')">
|
|
1672
|
+
* <option value="us">US</option>
|
|
1673
|
+
* <option value="uk">UK</option>
|
|
1674
|
+
* </select>
|
|
1675
|
+
* ```
|
|
1676
|
+
*
|
|
1677
|
+
* Also works on custom components whose root is NOT a native
|
|
1678
|
+
* input — call `useRegister()` in the child's setup to read the
|
|
1679
|
+
* parent's binding, then re-bind `v-register` onto an inner native
|
|
1680
|
+
* element. (When the wrapper's root IS the input itself, attribute
|
|
1681
|
+
* fallthrough handles it; `useRegister` is unnecessary.)
|
|
1682
|
+
*
|
|
1683
|
+
* ```vue
|
|
1684
|
+
* <!-- Parent -->
|
|
1685
|
+
* <MyField label="Email" v-register="form.register('email')" />
|
|
1686
|
+
*
|
|
1687
|
+
* <!-- MyField.vue (root is <label>, not <input>) -->
|
|
1688
|
+
* <script setup>
|
|
1689
|
+
* import { useRegister } from 'attaform'
|
|
1690
|
+
* defineProps<{ label: string }>()
|
|
1691
|
+
* const register = useRegister()
|
|
1692
|
+
* </script>
|
|
1693
|
+
* <template>
|
|
1694
|
+
* <label>
|
|
1695
|
+
* <span>{{ label }}</span>
|
|
1696
|
+
* <input v-register="register" />
|
|
1697
|
+
* </label>
|
|
1698
|
+
* </template>
|
|
1699
|
+
* ```
|
|
1700
|
+
*
|
|
1701
|
+
* Modifier support varies by element:
|
|
1702
|
+
* - text / number / textarea: `.lazy`, `.trim`, `.number`
|
|
1703
|
+
* - select: `.number`
|
|
1704
|
+
* - checkbox / radio: none
|
|
1705
|
+
*
|
|
1706
|
+
* See `RegisterTextModifier` / `RegisterSelectModifier` for
|
|
1707
|
+
* per-modifier semantics.
|
|
1708
|
+
*
|
|
1709
|
+
* Registered globally by `createAttaform()` (and by the
|
|
1710
|
+
* `attaform/nuxt` module). Most consumers don't import the
|
|
1711
|
+
* directive itself — it's exposed for integrations that install
|
|
1712
|
+
* directives manually.
|
|
1713
|
+
*/
|
|
1714
|
+
type RegisterDirective = RegisterTextCustomDirective | RegisterCheckboxCustomDirective | RegisterSelectCustomDirective | RegisterRadioCustomDirective | RegisterModelDynamicCustomDirective;
|
|
1715
|
+
/**
|
|
1716
|
+
* Callback form of `setValue`'s value argument. Receives the previous
|
|
1717
|
+
* value at the path and returns the next value:
|
|
1718
|
+
*
|
|
1719
|
+
* ```ts
|
|
1720
|
+
* form.setValue('count', (prev) => prev + 1)
|
|
1721
|
+
* form.setValue((prev) => ({ ...prev, name: 'Ada' }))
|
|
1722
|
+
* ```
|
|
1723
|
+
*
|
|
1724
|
+
* The library fills any missing structural slots (e.g. nested
|
|
1725
|
+
* objects) against the schema's defaults after the callback returns,
|
|
1726
|
+
* so partial returns are safe.
|
|
1727
|
+
*/
|
|
1728
|
+
type SetValueCallback<Read, Write = Read> = (prev: Read) => Read | Write;
|
|
1729
|
+
/**
|
|
1730
|
+
* The value argument of `form.setValue`. Either the next value
|
|
1731
|
+
* directly, or a callback that derives it from the previous value.
|
|
1732
|
+
*
|
|
1733
|
+
* Type parameters:
|
|
1734
|
+
* - `Write` — what the direct value form accepts (the storable shape
|
|
1735
|
+
* at the path).
|
|
1736
|
+
* - `Read` — what the callback's `prev` argument exposes (defaults
|
|
1737
|
+
* to `Write`). For whole-form callbacks the read shape tags
|
|
1738
|
+
* array elements as possibly-undefined to reflect runtime reality.
|
|
1739
|
+
*/
|
|
1740
|
+
type SetValuePayload<Write, Read = Write> = Write | SetValueCallback<Read, Write>;
|
|
1741
|
+
type DeepFlatten<T> = T extends object ? {
|
|
1742
|
+
[K in keyof T]: DeepFlatten<T[K]>;
|
|
1743
|
+
} : T;
|
|
1744
|
+
/**
|
|
1745
|
+
* Focus / blur / touched flags for a registered field.
|
|
1746
|
+
*
|
|
1747
|
+
* - `focused` — `true` while the user is interacting with the field;
|
|
1748
|
+
* `false` after blur. `null` until the field has ever been focused.
|
|
1749
|
+
* - `blurred` — `true` after the field has lost focus at least once.
|
|
1750
|
+
* `null` before any blur event.
|
|
1751
|
+
* - `touched` — flips to `true` on the first blur after a focus and
|
|
1752
|
+
* stays `true` thereafter. Useful for "show errors only after the
|
|
1753
|
+
* user has interacted" UX.
|
|
1754
|
+
*/
|
|
1755
|
+
type DOMFieldState = {
|
|
1756
|
+
/** `true` while focused; `false` after blur; `null` before first focus. */
|
|
1757
|
+
focused: boolean | null;
|
|
1758
|
+
/** `true` once the field has lost focus at least once; `null` before. */
|
|
1759
|
+
blurred: boolean | null;
|
|
1760
|
+
/** Flips to `true` on the first blur after a focus and stays there. */
|
|
1761
|
+
touched: boolean | null;
|
|
1762
|
+
};
|
|
1763
|
+
/**
|
|
1764
|
+
* Richer per-field type kept for type-level utility code (e.g.
|
|
1765
|
+
* higher-order helpers that pass field state around). Carries
|
|
1766
|
+
* `currentValue` / `originalValue` / `previousValue` (typed `Value`),
|
|
1767
|
+
* the same flag set as `FieldStateLeaf`, plus `meta`
|
|
1768
|
+
* (`MetaTrackerValue`).
|
|
1769
|
+
*
|
|
1770
|
+
* `form.fields.<path>` returns the slim `FieldStateLeaf` shape;
|
|
1771
|
+
* pick `FieldState<Value>` for code that needs `meta` or the historical
|
|
1772
|
+
* `previousValue` slot.
|
|
1773
|
+
*/
|
|
1774
|
+
type FieldState<Value = unknown> = DeepFlatten<DOMFieldState & {
|
|
1775
|
+
/** Per-field metadata (timestamps, raw value, connection state). */
|
|
1776
|
+
meta: MetaTrackerValue;
|
|
1777
|
+
/**
|
|
1778
|
+
* Validation errors for this path. Populated automatically by
|
|
1779
|
+
* `handleSubmit` and per-field validation; also writable via
|
|
1780
|
+
* `setFieldErrors` / `addFieldErrors`. Empty when valid — safe
|
|
1781
|
+
* to read without a null check.
|
|
1782
|
+
*/
|
|
1783
|
+
errors: ValidationError[];
|
|
1784
|
+
/** The value the field was initialised with. */
|
|
1785
|
+
originalValue: Value;
|
|
1786
|
+
/** The value before the most recent write. */
|
|
1787
|
+
previousValue: Value;
|
|
1788
|
+
/** The current value at this path. */
|
|
1789
|
+
currentValue: Value;
|
|
1790
|
+
/** `true` when `currentValue` matches `originalValue`. */
|
|
1791
|
+
pristine: boolean;
|
|
1792
|
+
/** `true` when `currentValue` differs from `originalValue`. */
|
|
1793
|
+
dirty: boolean;
|
|
1794
|
+
/**
|
|
1795
|
+
* `true` when this field is **blank** — the side-channel for
|
|
1796
|
+
* storage / display divergence (numeric leaves where storage
|
|
1797
|
+
* holds `0` / `0n` but the DOM shows `''`, plus any primitive
|
|
1798
|
+
* leaf the consumer explicitly opted in via `unset`). Surfaces
|
|
1799
|
+
* both as a top-level field here AND via `meta.blank` (the meta
|
|
1800
|
+
* projection mirrors the same value). See `docs/blank.md`.
|
|
1801
|
+
*/
|
|
1802
|
+
blank: boolean;
|
|
1803
|
+
}>;
|
|
1804
|
+
/**
|
|
1805
|
+
* Per-field reactive shape returned by `form.fields.<leaf-path>`.
|
|
1806
|
+
* Slim, readonly across the board. Leaf-aware: this shape only
|
|
1807
|
+
* appears at LEAF paths (primitives, dates). At container paths
|
|
1808
|
+
* the proxy descends without injecting these keys, so a schema
|
|
1809
|
+
* field literally named `dirty` at depth 2+ stays reachable as a
|
|
1810
|
+
* descent target — no shadowing.
|
|
1811
|
+
*/
|
|
1812
|
+
type FieldStateLeaf<Value = unknown> = {
|
|
1813
|
+
readonly value: Value;
|
|
1814
|
+
readonly original: Value;
|
|
1815
|
+
readonly pristine: boolean;
|
|
1816
|
+
readonly dirty: boolean;
|
|
1817
|
+
readonly focused: boolean | null;
|
|
1818
|
+
readonly blurred: boolean | null;
|
|
1819
|
+
readonly touched: boolean | null;
|
|
1820
|
+
readonly isConnected: boolean;
|
|
1821
|
+
readonly updatedAt: string | null;
|
|
1822
|
+
readonly errors: readonly ValidationError[];
|
|
1823
|
+
readonly path: ReadonlyArray<string | number>;
|
|
1824
|
+
readonly blank: boolean;
|
|
1825
|
+
};
|
|
1826
|
+
/**
|
|
1827
|
+
* Recursive type behind `form.fields`. Leaf-aware branching: at
|
|
1828
|
+
* primitive paths (string, number, boolean, bigint, Date, …) the
|
|
1829
|
+
* proxy returns a `FieldStateLeaf`; at container paths (object,
|
|
1830
|
+
* array, …) the proxy descends without injecting leaf-keys.
|
|
1831
|
+
*
|
|
1832
|
+
* Field-name collisions at depth 2+ resolve unambiguously: a schema
|
|
1833
|
+
* field literally named `dirty` at depth 2 is reachable as a
|
|
1834
|
+
* descent target (`form.fields.address.dirty` returns the
|
|
1835
|
+
* FieldStateView for `address.dirty`). Reading `dirty` AT the
|
|
1836
|
+
* leaf-view (`form.fields.address.dirty.dirty`) reads the leaf's
|
|
1837
|
+
* own dirty boolean — path-segment and leaf-prop occupy different
|
|
1838
|
+
* proxy depths.
|
|
1839
|
+
*
|
|
1840
|
+
* The runtime implementation queries `schema.isLeafAtPath(segments)`
|
|
1841
|
+
* at every step; this type approximates that decision using
|
|
1842
|
+
* "T extends primitive". The two stay in sync for typical schemas;
|
|
1843
|
+
* exotic adapter-defined leaf kinds (custom `Date`-like) may need
|
|
1844
|
+
* a runtime check (the runtime is authoritative).
|
|
1845
|
+
*/
|
|
1846
|
+
type FieldStateMapEntry<T> = T extends string | number | boolean | bigint | symbol | null | undefined | Date ? FieldStateLeaf<T> : T extends ReadonlyArray<infer U> ? {
|
|
1847
|
+
readonly [K: number]: FieldStateMapEntry<U>;
|
|
1848
|
+
} : T extends object ? {
|
|
1849
|
+
readonly [K in keyof T]: FieldStateMapEntry<T[K]>;
|
|
1850
|
+
} : FieldStateLeaf<T>;
|
|
1851
|
+
/**
|
|
1852
|
+
* Type of `form.fields` — leaf-aware drillable callable Proxy. At
|
|
1853
|
+
* a leaf path the proxy resolves to a `FieldStateLeaf<Value>`; at
|
|
1854
|
+
* a container path it returns a sub-proxy you can keep drilling.
|
|
1855
|
+
*
|
|
1856
|
+
* Augmented with the callable signatures so dot-access and function-
|
|
1857
|
+
* call coexist on the same identifier:
|
|
1858
|
+
*
|
|
1859
|
+
* ```ts
|
|
1860
|
+
* form.fields.email.value // string (leaf-prop on FieldStateView)
|
|
1861
|
+
* form.fields('email').value // function-call (dynamic / programmatic)
|
|
1862
|
+
* form.fields(['users', 0, 'name']) // path-array form
|
|
1863
|
+
* form.fields() // root proxy
|
|
1864
|
+
* ```
|
|
1865
|
+
*
|
|
1866
|
+
* Single-bracket dotted access (`form.fields['address.city']`) is
|
|
1867
|
+
* intentionally NOT supported — JS object semantics treat the dotted
|
|
1868
|
+
* string as a single key. Use chained dot/bracket or the callable
|
|
1869
|
+
* form.
|
|
1870
|
+
*/
|
|
1871
|
+
type FieldStateMap<Form extends GenericForm> = {
|
|
1872
|
+
readonly [K in keyof Form]: FieldStateMapEntry<Form[K]>;
|
|
1873
|
+
} & {
|
|
1874
|
+
(path: string): unknown;
|
|
1875
|
+
(path: ReadonlyArray<string | number>): unknown;
|
|
1876
|
+
(): FieldStateMap<Form>;
|
|
1877
|
+
};
|
|
1878
|
+
/**
|
|
1879
|
+
* Untyped error map keyed by dotted-string path. The same data
|
|
1880
|
+
* exposed by `form.errors`, but as a plain record — useful when
|
|
1881
|
+
* routing API errors that may land on paths the form's TypeScript
|
|
1882
|
+
* type doesn't know about.
|
|
1883
|
+
*/
|
|
1884
|
+
type FormErrorRecord = Record<string, ValidationError[]>;
|
|
1885
|
+
/**
|
|
1886
|
+
* Type of `form.errors`. Leaf-aware drillable callable Proxy. At a
|
|
1887
|
+
* leaf path the proxy resolves to `ValidationError[] | undefined`;
|
|
1888
|
+
* at a container path it returns a sub-proxy you can keep drilling.
|
|
1889
|
+
*
|
|
1890
|
+
* Dot/bracket access mirrors the schema shape:
|
|
1891
|
+
*
|
|
1892
|
+
* ```ts
|
|
1893
|
+
* form.errors.email // ValidationError[] | undefined (leaf)
|
|
1894
|
+
* form.errors.user.profile.email // ValidationError[] | undefined (chained leaves)
|
|
1895
|
+
* form.errors.address // sub-proxy (container — descend further)
|
|
1896
|
+
* ```
|
|
1897
|
+
*
|
|
1898
|
+
* Callable form for dynamic / programmatic paths:
|
|
1899
|
+
*
|
|
1900
|
+
* ```ts
|
|
1901
|
+
* form.errors('user.profile.email') // dotted-string
|
|
1902
|
+
* form.errors(['user', 'profile', 'email']) // path-array
|
|
1903
|
+
* form.errors() // root proxy
|
|
1904
|
+
* ```
|
|
1905
|
+
*
|
|
1906
|
+
* Single-bracket dotted access (`form.errors['user.profile.email']`)
|
|
1907
|
+
* is intentionally NOT supported — JS object semantics treat the
|
|
1908
|
+
* dotted string as a single key, which would land on a non-existent
|
|
1909
|
+
* path. Use chained dot/bracket access or the callable form.
|
|
1910
|
+
*/
|
|
1911
|
+
/**
|
|
1912
|
+
* Recursive shape of the `form.errors` proxy. Mirrors the schema:
|
|
1913
|
+
* primitive leaves expose `ValidationError[] | undefined` directly;
|
|
1914
|
+
* containers expose a sub-shape you can keep drilling. Arrays expose
|
|
1915
|
+
* numeric-indexed sub-shapes.
|
|
1916
|
+
*
|
|
1917
|
+
* Augmented with the callable signatures so dot-access and function-
|
|
1918
|
+
* call coexist on the same identifier.
|
|
1919
|
+
*/
|
|
1920
|
+
type FormErrorsSurface<Form> = ErrorsProxyShape<Form> & {
|
|
1921
|
+
(path: string): readonly ValidationError[] | undefined;
|
|
1922
|
+
(path: ReadonlyArray<string | number>): readonly ValidationError[] | undefined;
|
|
1923
|
+
(): FormErrorsSurface<Form>;
|
|
1924
|
+
};
|
|
1925
|
+
type ErrorsProxyShape<T> = T extends string | number | boolean | bigint | symbol | null | undefined | Date ? readonly ValidationError[] | undefined : T extends ReadonlyArray<infer U> ? {
|
|
1926
|
+
readonly [K: number]: ErrorsProxyShape<U>;
|
|
1927
|
+
} : T extends object ? {
|
|
1928
|
+
readonly [K in keyof T]: ErrorsProxyShape<T[K]>;
|
|
1929
|
+
} : readonly ValidationError[] | undefined;
|
|
1930
|
+
/**
|
|
1931
|
+
* Type of `form.values`. Drillable readonly callable proxy. Unlike
|
|
1932
|
+
* `form.errors` and `form.fields`, containers are USEFUL terminals:
|
|
1933
|
+
* `form.values.address` returns the actual `{ city, … }` subtree
|
|
1934
|
+
* (and keeps drilling). Asymmetry justified by density — every
|
|
1935
|
+
* container in `values` carries meaningful data; in errors / fields
|
|
1936
|
+
* containers are derivations.
|
|
1937
|
+
*
|
|
1938
|
+
* ```ts
|
|
1939
|
+
* form.values.email // string (the value)
|
|
1940
|
+
* form.values.address // { city, … } — object (drillable)
|
|
1941
|
+
* form.values.address.city // string (chained descent)
|
|
1942
|
+
* form.values('address.city') // function-call (dynamic / programmatic)
|
|
1943
|
+
* form.values(['address', 'city']) // path-array form
|
|
1944
|
+
* form.values() // the whole form value (root)
|
|
1945
|
+
* ```
|
|
1946
|
+
*
|
|
1947
|
+
* Single-bracket dotted access (`form.values['address.city']`) is
|
|
1948
|
+
* intentionally NOT supported — JS object semantics treat the dotted
|
|
1949
|
+
* string as a single key. Use chained dot/bracket or the callable
|
|
1950
|
+
* form.
|
|
1951
|
+
*/
|
|
1952
|
+
type ValuesSurface<F> = Readonly<F> & {
|
|
1953
|
+
(path: string): unknown;
|
|
1954
|
+
(path: ReadonlyArray<string | number>): unknown;
|
|
1955
|
+
(): Readonly<F>;
|
|
1956
|
+
};
|
|
1957
|
+
/**
|
|
1958
|
+
* A single server-side error entry. Carries both the human-readable
|
|
1959
|
+
* `message` and a stable `code` identifier — both fields are required.
|
|
1960
|
+
* The `code` is stamped verbatim onto the produced `ValidationError`,
|
|
1961
|
+
* so consumers can branch on it without string-matching on `message`.
|
|
1962
|
+
*
|
|
1963
|
+
* Pick a prefix for your codes (`api:`, `auth:`, etc.) and stay
|
|
1964
|
+
* consistent so error-rendering UIs can switch on the code.
|
|
1965
|
+
*/
|
|
1966
|
+
type ApiErrorEntry = {
|
|
1967
|
+
/** Human-readable failure description. */
|
|
1968
|
+
message: string;
|
|
1969
|
+
/**
|
|
1970
|
+
* Stable machine identifier for the failure (e.g. `'api:duplicate-email'`).
|
|
1971
|
+
* Forwarded verbatim onto the produced `ValidationError`.
|
|
1972
|
+
*/
|
|
1973
|
+
code: string;
|
|
1974
|
+
};
|
|
1975
|
+
/**
|
|
1976
|
+
* Shape of a server-side error details record. Keys are dotted field
|
|
1977
|
+
* paths; values are either a single entry, an array of entries, or a
|
|
1978
|
+
* mix of structured and bare-string entries. Each entry is one of:
|
|
1979
|
+
*
|
|
1980
|
+
* - **Structured** — `{ message: string, code: string }`. The `code`
|
|
1981
|
+
* forwards verbatim onto the produced `ValidationError`.
|
|
1982
|
+
* - **Bare string** — a plain string. The Rails / Django REST
|
|
1983
|
+
* Framework / Laravel default JSON shape (`{ field: ["msg"] }`).
|
|
1984
|
+
* Synthesized into `{ message: <string>, code: <defaultCode> }` at
|
|
1985
|
+
* parse time, where `defaultCode` defaults to `'api:unknown'` and
|
|
1986
|
+
* is configurable via `parseApiErrors`'s options bag.
|
|
1987
|
+
*
|
|
1988
|
+
* Multiple entries at the same path produce multiple
|
|
1989
|
+
* `ValidationError`s — useful for a single field that fails multiple
|
|
1990
|
+
* checks (e.g. `password` is too short *and* missing a digit).
|
|
1991
|
+
*/
|
|
1992
|
+
type ApiErrorDetails = Record<string, ApiErrorValue>;
|
|
1993
|
+
/**
|
|
1994
|
+
* One entry inside an {@link ApiErrorDetails} value — either the
|
|
1995
|
+
* strict `{ message, code }` object, or a bare string (synthesised
|
|
1996
|
+
* with the parser's `defaultCode`).
|
|
1997
|
+
*/
|
|
1998
|
+
type ApiErrorValue = string | ApiErrorEntry | ReadonlyArray<string | ApiErrorEntry>;
|
|
1999
|
+
/**
|
|
2000
|
+
* Outer envelope `parseApiErrors` accepts. Both the wrapped form
|
|
2001
|
+
* (`{ error: { details } }`) and the unwrapped form (`{ details }`)
|
|
2002
|
+
* are recognised; raw detail records (`{ email: { message, code } }`)
|
|
2003
|
+
* are also accepted directly.
|
|
2004
|
+
*/
|
|
2005
|
+
type ApiErrorEnvelope = {
|
|
2006
|
+
/** Wrapped error envelope — `parseApiErrors` reads `details` from inside. */
|
|
2007
|
+
error?: {
|
|
2008
|
+
details?: ApiErrorDetails;
|
|
2009
|
+
[k: string]: unknown;
|
|
2010
|
+
};
|
|
2011
|
+
/** Unwrapped error envelope. */
|
|
2012
|
+
details?: ApiErrorDetails;
|
|
2013
|
+
};
|
|
2014
|
+
/**
|
|
2015
|
+
* Reactive form-level flags, counters, and aggregates returned as
|
|
2016
|
+
* `form.meta`. "Meta" because every other surface (`form.values`,
|
|
2017
|
+
* `form.errors`, `form.fields`) is data-shaped — `form.meta` holds
|
|
2018
|
+
* facts derived ABOUT the form.
|
|
2019
|
+
*
|
|
2020
|
+
* Read fields directly with no `.value` — they auto-unwrap inside
|
|
2021
|
+
* the reactive object:
|
|
2022
|
+
*
|
|
2023
|
+
* ```vue
|
|
2024
|
+
* <button :disabled="form.meta.isSubmitting">Save</button>
|
|
2025
|
+
* ```
|
|
2026
|
+
*
|
|
2027
|
+
* Watch a single field via the getter form:
|
|
2028
|
+
*
|
|
2029
|
+
* ```ts
|
|
2030
|
+
* watch(() => form.meta.isSubmitting, (value) => …)
|
|
2031
|
+
* ```
|
|
2032
|
+
*
|
|
2033
|
+
* Per-field state (touched, dirty, errors) lives behind
|
|
2034
|
+
* `form.fields.<path>`; this is the aggregate view across the
|
|
2035
|
+
* whole form.
|
|
2036
|
+
*
|
|
2037
|
+
* Read-only at runtime — assignments throw. Destructuring snapshots
|
|
2038
|
+
* the current values; use `toRefs()` if you need reactive handles
|
|
2039
|
+
* to individual fields.
|
|
2040
|
+
*/
|
|
2041
|
+
interface FormMeta {
|
|
2042
|
+
/**
|
|
2043
|
+
* `true` when any field's current value differs from its initial
|
|
2044
|
+
* value. `false` for a pristine form and for one where every change
|
|
2045
|
+
* has been undone. Restore the pristine baseline via `reset()`.
|
|
2046
|
+
*
|
|
2047
|
+
* Note: object/array leaves are compared by reference, so replacing
|
|
2048
|
+
* an array with an equal copy still reads as dirty.
|
|
2049
|
+
*/
|
|
2050
|
+
readonly isDirty: boolean;
|
|
2051
|
+
/**
|
|
2052
|
+
* `true` when the form currently has no validation errors. Flips
|
|
2053
|
+
* with every `validate()` / `handleSubmit` outcome.
|
|
2054
|
+
*/
|
|
2055
|
+
readonly isValid: boolean;
|
|
2056
|
+
/**
|
|
2057
|
+
* `true` while a `handleSubmit`-produced submit handler is running.
|
|
2058
|
+
* Covers both the validation phase and your async submit callback.
|
|
2059
|
+
* Useful for disabling the submit button.
|
|
2060
|
+
*/
|
|
2061
|
+
readonly isSubmitting: boolean;
|
|
2062
|
+
/**
|
|
2063
|
+
* `true` while any validation run is in flight (the reactive
|
|
2064
|
+
* `validate()` re-run, an imperative `validateAsync()`, or the
|
|
2065
|
+
* pre-submit validation inside `handleSubmit`).
|
|
2066
|
+
*/
|
|
2067
|
+
readonly isValidating: boolean;
|
|
2068
|
+
/**
|
|
2069
|
+
* How many times the submit handler has been invoked, regardless of
|
|
2070
|
+
* outcome (validation failure, callback success, callback throw).
|
|
2071
|
+
* Useful for "show errors after first submit attempt" UX.
|
|
2072
|
+
*/
|
|
2073
|
+
readonly submitCount: number;
|
|
2074
|
+
/**
|
|
2075
|
+
* The error thrown or rejected by the most recent submit callback
|
|
2076
|
+
* (or its `onError` handler). Cleared to `null` at the start of
|
|
2077
|
+
* each new submission attempt; stays `null` on success.
|
|
2078
|
+
*
|
|
2079
|
+
* The submit handler still throws normally — this is the reactive
|
|
2080
|
+
* mirror for templates. Imperative callers can use
|
|
2081
|
+
* `try { await onSubmit() }` instead.
|
|
2082
|
+
*/
|
|
2083
|
+
readonly submitError: unknown;
|
|
2084
|
+
/** `true` when there is at least one undo step available. Always present (false when history is disabled). */
|
|
2085
|
+
readonly canUndo: boolean;
|
|
2086
|
+
/** `true` when `undo()` has been called and a `redo()` would replay. Always present (false when history is disabled). */
|
|
2087
|
+
readonly canRedo: boolean;
|
|
2088
|
+
/**
|
|
2089
|
+
* Total snapshots across the undo and redo stacks. Useful for
|
|
2090
|
+
* debug overlays; UI driving undo/redo buttons should gate on
|
|
2091
|
+
* `canUndo` / `canRedo` instead.
|
|
2092
|
+
*/
|
|
2093
|
+
readonly historySize: number;
|
|
2094
|
+
/**
|
|
2095
|
+
* Flat aggregate of EVERY validation error in the form — schema-
|
|
2096
|
+
* keyed entries, form-level errors (path: []), unmapped server
|
|
2097
|
+
* errors (paths not in `FlatPath`), and cross-field-refine errors
|
|
2098
|
+
* (paths at containers). Reads as English: "the form's errors."
|
|
2099
|
+
*
|
|
2100
|
+
* Unlike `form.errors.<path>` (per-leaf, active-path-filtered),
|
|
2101
|
+
* `form.meta.errors` is unfiltered — inactive-variant errors stay
|
|
2102
|
+
* in the array. Consumers who want only addressable errors filter
|
|
2103
|
+
* the array themselves (`form.meta.errors.filter(e => …)`).
|
|
2104
|
+
*
|
|
2105
|
+
* Common patterns:
|
|
2106
|
+
*
|
|
2107
|
+
* ```vue
|
|
2108
|
+
* <p v-if="form.meta.errors.length">{{ form.meta.errors.length }} issue(s)</p>
|
|
2109
|
+
* <ul>
|
|
2110
|
+
* <li v-for="err in form.meta.errors" :key="err.path.join('.')">
|
|
2111
|
+
* {{ err.path.join('.') || 'form' }}: {{ err.message }}
|
|
2112
|
+
* </li>
|
|
2113
|
+
* </ul>
|
|
2114
|
+
* ```
|
|
2115
|
+
*
|
|
2116
|
+
* The array re-allocates on any underlying store change (schema /
|
|
2117
|
+
* derived-blank / user); reactivity propagates through the standard
|
|
2118
|
+
* Vue computed graph.
|
|
2119
|
+
*/
|
|
2120
|
+
readonly errors: readonly ValidationError[];
|
|
2121
|
+
/**
|
|
2122
|
+
* Per-`useForm()`-call identity. Stable for the lifetime of one
|
|
2123
|
+
* `useForm()` call; new on every fresh mount. Orthogonal to
|
|
2124
|
+
* `form.key`: the key identifies a SHARED FormStore (so two
|
|
2125
|
+
* `useForm({ key: 'signup' })` calls return the same store and the
|
|
2126
|
+
* same key), while `instanceId` identifies THIS specific callsite —
|
|
2127
|
+
* useful when two forms share a key (sidebar + main rendering the
|
|
2128
|
+
* same form) and you need to disambiguate which caller is which.
|
|
2129
|
+
*
|
|
2130
|
+
* Format is opaque (Vue 3.5+ `useId()`-derived). Treat as identity,
|
|
2131
|
+
* not state — don't parse, don't compare ordinally, don't persist.
|
|
2132
|
+
*
|
|
2133
|
+
* Common patterns:
|
|
2134
|
+
*
|
|
2135
|
+
* - **Devtools panels** disambiguating shared-key form mounts.
|
|
2136
|
+
* - **Telemetry / logging hooks** tagging events with which mount
|
|
2137
|
+
* triggered them.
|
|
2138
|
+
* - **E2E test selectors** stamping `data-form-id={form.meta.instanceId}`
|
|
2139
|
+
* onto a wrapper to assert which form was focused.
|
|
2140
|
+
* - **Vue `:key`** for keyed lists of dynamically-rendered forms
|
|
2141
|
+
* (drag-reorder, etc.) — stable identity per useForm() call.
|
|
2142
|
+
*/
|
|
2143
|
+
readonly instanceId: string;
|
|
2144
|
+
}
|
|
2145
|
+
/**
|
|
2146
|
+
* The object returned by `useForm`. Holds every reactive ref, write
|
|
2147
|
+
* helper, and lifecycle method bound to one form.
|
|
2148
|
+
*
|
|
2149
|
+
* ```ts
|
|
2150
|
+
* const form = useForm({ schema })
|
|
2151
|
+
* form.register('email') // bind to <input v-register>
|
|
2152
|
+
* form.values.email // current value (proxy, no .value)
|
|
2153
|
+
* form.fields.email.dirty // per-field flags
|
|
2154
|
+
* form.errors.email // ValidationError[] | undefined
|
|
2155
|
+
* form.setValue('email', 'a@b.c')
|
|
2156
|
+
* form.handleSubmit(onSubmit) // returns a submit handler
|
|
2157
|
+
* form.meta.isSubmitting // form-level reactive flag
|
|
2158
|
+
* ```
|
|
2159
|
+
*/
|
|
2160
|
+
type UseFormReturnType<Form extends GenericForm, GetValueFormType extends GenericForm = Form> = {
|
|
2161
|
+
/**
|
|
2162
|
+
* Wraps your submit logic with validation and error routing.
|
|
2163
|
+
*
|
|
2164
|
+
* ```ts
|
|
2165
|
+
* <form @submit.prevent="form.handleSubmit(
|
|
2166
|
+
* (data) => api.signup(data),
|
|
2167
|
+
* (errors) => console.log(errors),
|
|
2168
|
+
* )">
|
|
2169
|
+
* ```
|
|
2170
|
+
*
|
|
2171
|
+
* `data` is the strictly-typed parsed value — refinements have
|
|
2172
|
+
* fired, so every leaf is guaranteed to satisfy its schema-level
|
|
2173
|
+
* format / range / membership constraints.
|
|
2174
|
+
*/
|
|
2175
|
+
handleSubmit: HandleSubmit<Form>;
|
|
2176
|
+
/**
|
|
2177
|
+
* Reactive readonly proxy over the form's storage value. Read
|
|
2178
|
+
* identically in script and template — no `.value`, no auto-unwrap
|
|
2179
|
+
* rules. Pinia setup-store pattern.
|
|
2180
|
+
*
|
|
2181
|
+
* ```vue
|
|
2182
|
+
* <script setup>
|
|
2183
|
+
* const form = useForm({ schema, key: 'login' })
|
|
2184
|
+
* </script>
|
|
2185
|
+
*
|
|
2186
|
+
* <template>
|
|
2187
|
+
* <p>{{ form.values.email }}</p>
|
|
2188
|
+
* <p>{{ form.values.address.city }}</p>
|
|
2189
|
+
* </template>
|
|
2190
|
+
* ```
|
|
2191
|
+
*
|
|
2192
|
+
* Writes are blocked at the proxy boundary — go through `setValue`,
|
|
2193
|
+
* the directive, or one of the field-array helpers. The
|
|
2194
|
+
* slim-primitive write gate stays the only path into storage.
|
|
2195
|
+
*
|
|
2196
|
+
* Reads reflect what's storable: enum-typed slots widen to their
|
|
2197
|
+
* primitive supertype (`string`), so refinement-invalid but
|
|
2198
|
+
* structurally-valid values are visible. Use `handleSubmit` /
|
|
2199
|
+
* `validateAsync()` when you need the post-validation strict type.
|
|
2200
|
+
*/
|
|
2201
|
+
values: ValuesSurface<WriteShape<GetValueFormType>>;
|
|
2202
|
+
/**
|
|
2203
|
+
* Reactive per-field state proxy. Pinia-style nested object — read
|
|
2204
|
+
* leaf properties (`value`, `dirty`, `touched`, `errors`, `blurred`,
|
|
2205
|
+
* `focused`, `blank`, …) directly off the field's path:
|
|
2206
|
+
*
|
|
2207
|
+
* ```vue
|
|
2208
|
+
* <p v-if="form.fields.email.touched && form.fields.email.errors.length">
|
|
2209
|
+
* {{ form.fields.email.errors[0].message }}
|
|
2210
|
+
* </p>
|
|
2211
|
+
* <p>City dirty? {{ form.fields.address.city.dirty }}</p>
|
|
2212
|
+
* ```
|
|
2213
|
+
*
|
|
2214
|
+
* The same proxy supports descent at every level — `address` reads
|
|
2215
|
+
* the FieldStateLeaf for the address object, and `address.city`
|
|
2216
|
+
* descends into the nested leaf.
|
|
2217
|
+
*
|
|
2218
|
+
* Leaf values follow the slim WriteShape contract: enum-typed leaves
|
|
2219
|
+
* widen to their primitive supertype. The errors array, dirty flag,
|
|
2220
|
+
* focus state, etc. are unaffected.
|
|
2221
|
+
*
|
|
2222
|
+
* Shadowing: at depth 2+, FieldStateLeaf keys (`dirty`, `touched`,
|
|
2223
|
+
* `errors`, `blank`, `focused`, `blurred`, `value`,
|
|
2224
|
+
* `original`, `pristine`, `isConnected`, `updatedAt`, `path`) win
|
|
2225
|
+
* over schema field names. Top-level fields are NOT shadowed.
|
|
2226
|
+
* Document edge case; rename the offending schema field if the
|
|
2227
|
+
* collision matters.
|
|
2228
|
+
*/
|
|
2229
|
+
fields: FieldStateMap<WriteShape<GetValueFormType>>;
|
|
2230
|
+
/**
|
|
2231
|
+
* Write to the form programmatically. Two forms:
|
|
2232
|
+
*
|
|
2233
|
+
* - `setValue(value)` — replace the whole form.
|
|
2234
|
+
* - `setValue(path, value)` — write at a specific path.
|
|
2235
|
+
*
|
|
2236
|
+
* Either takes a callback in place of `value` to derive the next
|
|
2237
|
+
* value from the previous one:
|
|
2238
|
+
*
|
|
2239
|
+
* ```ts
|
|
2240
|
+
* form.setValue('count', (prev) => prev + 1)
|
|
2241
|
+
* form.setValue((prev) => ({ ...prev, name: 'Ada' }))
|
|
2242
|
+
* ```
|
|
2243
|
+
*
|
|
2244
|
+
* Returns `true` when the write is accepted. A `false` return
|
|
2245
|
+
* means the value didn't match the slot's expected type
|
|
2246
|
+
* (e.g. writing a number to a string field) — the form state
|
|
2247
|
+
* stays unchanged. Refinement-level mismatches (out-of-enum
|
|
2248
|
+
* values, failing format checks, etc.) DO succeed and surface as
|
|
2249
|
+
* field errors instead.
|
|
2250
|
+
*/
|
|
2251
|
+
setValue: {
|
|
2252
|
+
/**
|
|
2253
|
+
* Replace the whole form. Pass a value or a callback receiving
|
|
2254
|
+
* the previous form.
|
|
2255
|
+
*
|
|
2256
|
+
* ```ts
|
|
2257
|
+
* form.setValue({ name: 'Ada', email: 'a@b.c' })
|
|
2258
|
+
* form.setValue((prev) => ({ ...prev, name: 'Ada' }))
|
|
2259
|
+
* ```
|
|
2260
|
+
*
|
|
2261
|
+
* Returns `true` when the write was accepted, `false` when the
|
|
2262
|
+
* value didn't match the expected shape (e.g. wrong primitive
|
|
2263
|
+
* type at a leaf). Refinement-level mismatches (out-of-enum
|
|
2264
|
+
* values, failing format checks, etc.) succeed and surface as
|
|
2265
|
+
* field errors instead.
|
|
2266
|
+
*/
|
|
2267
|
+
<Value extends SetValuePayload<DefaultValuesShape<Form>, WriteShape<Form>>>(value: Value): boolean;
|
|
2268
|
+
/**
|
|
2269
|
+
* Write at a specific path. Pass a value or a callback receiving
|
|
2270
|
+
* the previous value at that path.
|
|
2271
|
+
*
|
|
2272
|
+
* ```ts
|
|
2273
|
+
* form.setValue('email', 'a@b.c')
|
|
2274
|
+
* form.setValue('count', (prev) => prev + 1)
|
|
2275
|
+
* form.setValue('income', unset) // numeric leaf marked displayed-empty
|
|
2276
|
+
* ```
|
|
2277
|
+
*
|
|
2278
|
+
* Returns `true` when the write was accepted, `false` when the
|
|
2279
|
+
* value didn't match the slot's expected primitive type.
|
|
2280
|
+
* Refinement-level mismatches succeed and surface as field
|
|
2281
|
+
* errors. Pass the `unset` symbol at any primitive leaf to mark
|
|
2282
|
+
* it blank (storage holds the slim default; UI displays
|
|
2283
|
+
* empty; submit raises "No value supplied" for required schemas).
|
|
2284
|
+
*/
|
|
2285
|
+
<Path extends FlatPath<Form>, Value extends SetValuePayload<DefaultValuesShape<NestedType<Form, Path>>, NonNullable<WriteShape<NestedType<Form, Path>>>>>(path: Path, value: Value): boolean;
|
|
2286
|
+
};
|
|
2287
|
+
/**
|
|
2288
|
+
* Reactive validation status. Re-runs whenever the form (or the
|
|
2289
|
+
* subtree at `path`) mutates. The returned ref carries a `pending`
|
|
2290
|
+
* flag — gate on `!status.value.pending` before reading
|
|
2291
|
+
* `success` / `errors`.
|
|
2292
|
+
*
|
|
2293
|
+
* ```ts
|
|
2294
|
+
* const status = form.validate()
|
|
2295
|
+
* watchEffect(() => {
|
|
2296
|
+
* if (status.value.pending) return
|
|
2297
|
+
* if (!status.value.success) console.log(status.value.errors)
|
|
2298
|
+
* })
|
|
2299
|
+
* ```
|
|
2300
|
+
*
|
|
2301
|
+
* Stale in-flight runs are dropped automatically — the ref only
|
|
2302
|
+
* settles to results from the most recent call.
|
|
2303
|
+
*/
|
|
2304
|
+
validate: (path?: FlatPath<Form>) => Readonly<Ref<ReactiveValidationStatus<Form>>>;
|
|
2305
|
+
/**
|
|
2306
|
+
* Run validation once and return the result. Unlike `validate()`,
|
|
2307
|
+
* this does not subscribe to form reactivity.
|
|
2308
|
+
*
|
|
2309
|
+
* ```ts
|
|
2310
|
+
* const result = await form.validateAsync()
|
|
2311
|
+
* if (!result.success) showErrors(result.errors)
|
|
2312
|
+
* ```
|
|
2313
|
+
*
|
|
2314
|
+
* Pass a path to validate a subtree. `state.isValidating` flips
|
|
2315
|
+
* `true` while the promise is in flight.
|
|
2316
|
+
*/
|
|
2317
|
+
validateAsync: (path?: FlatPath<Form>) => Promise<ValidationResponseWithoutValue<Form>>;
|
|
2318
|
+
/**
|
|
2319
|
+
* Bind a path to a native input via `v-register`. Returns a
|
|
2320
|
+
* `RegisterValue` carrying the live ref and event handlers the
|
|
2321
|
+
* directive needs.
|
|
2322
|
+
*
|
|
2323
|
+
* ```vue
|
|
2324
|
+
* <input v-register="form.register('email')" />
|
|
2325
|
+
* <input
|
|
2326
|
+
* type="password"
|
|
2327
|
+
* v-register="form.register('password', { persist: true, acknowledgeSensitive: true })"
|
|
2328
|
+
* />
|
|
2329
|
+
* ```
|
|
2330
|
+
*
|
|
2331
|
+
* Pass `options.persist` to opt into the form's persistence
|
|
2332
|
+
* pipeline. Persistence requires `useForm({ persist })` configured
|
|
2333
|
+
* for storage activity to actually happen.
|
|
2334
|
+
*/
|
|
2335
|
+
register: <Path extends RegisterFlatPath<Form, keyof Form>>(path: Path, options?: RegisterOptions) => RegisterValue<NestedReadType<WriteShape<Form>, Path>>;
|
|
2336
|
+
/**
|
|
2337
|
+
* The form's identifier — either the explicit `key` passed to
|
|
2338
|
+
* `useForm` or an auto-generated unique id when `key` was omitted.
|
|
2339
|
+
* Use it when feeding API errors through `parseApiErrors`:
|
|
2340
|
+
*
|
|
2341
|
+
* ```ts
|
|
2342
|
+
* const result = parseApiErrors(serverPayload, { formKey: form.key })
|
|
2343
|
+
* if (result.ok) form.setFieldErrors(result.errors)
|
|
2344
|
+
* ```
|
|
2345
|
+
*/
|
|
2346
|
+
key: FormKey;
|
|
2347
|
+
/**
|
|
2348
|
+
* Reactive map of field errors, keyed by dotted path. Populated
|
|
2349
|
+
* automatically by `handleSubmit` and per-field validation; cleared
|
|
2350
|
+
* on validation success.
|
|
2351
|
+
*
|
|
2352
|
+
* Read in templates with no `.value`:
|
|
2353
|
+
*
|
|
2354
|
+
* ```vue
|
|
2355
|
+
* <p v-if="form.errors.email">{{ form.errors.email[0].message }}</p>
|
|
2356
|
+
* ```
|
|
2357
|
+
*
|
|
2358
|
+
* Watch from script via the getter form:
|
|
2359
|
+
*
|
|
2360
|
+
* ```ts
|
|
2361
|
+
* watch(() => form.errors.email, (errors) => …)
|
|
2362
|
+
* ```
|
|
2363
|
+
*
|
|
2364
|
+
* Use bracket access for nested dotted keys
|
|
2365
|
+
* (`form.errors['user.profile.email']`) — JS dot notation splits
|
|
2366
|
+
* on literal dots.
|
|
2367
|
+
*
|
|
2368
|
+
* Read-only — populate via `setFieldErrors`, `addFieldErrors`, and
|
|
2369
|
+
* `clearFieldErrors`. Server-side errors flow through
|
|
2370
|
+
* `parseApiErrors` first.
|
|
2371
|
+
*/
|
|
2372
|
+
errors: FormErrorsSurface<Form>;
|
|
2373
|
+
/**
|
|
2374
|
+
* Escape hatch for the rare case a consumer needs a `Ref<T>` —
|
|
2375
|
+
* e.g. handing the value to an external composable that expects a
|
|
2376
|
+
* Vue ref, or watching a single path with `watch(formRef, ...)`.
|
|
2377
|
+
*
|
|
2378
|
+
* ```ts
|
|
2379
|
+
* const emailRef = form.toRef('email') // Readonly<Ref<string>>
|
|
2380
|
+
* watch(emailRef, (next) => console.log(next))
|
|
2381
|
+
* ```
|
|
2382
|
+
*
|
|
2383
|
+
* Returns `Readonly<Ref<...>>` — writes go through `setValue`,
|
|
2384
|
+
* `register()`, or the field-array helpers, never via the ref.
|
|
2385
|
+
* Prefer `form.values.email` for direct reads in templates +
|
|
2386
|
+
* scripts; `toRef` is for ref-shaped interop only.
|
|
2387
|
+
*/
|
|
2388
|
+
toRef: <Path extends FlatPath<Form>>(path: Path) => Readonly<Ref<NestedReadType<WriteShape<GetValueFormType>, Path>>>;
|
|
2389
|
+
/**
|
|
2390
|
+
* Replace every field error for this form with the provided list.
|
|
2391
|
+
* Useful after `parseApiErrors` produces a fresh batch from a
|
|
2392
|
+
* server response.
|
|
2393
|
+
*
|
|
2394
|
+
* ```ts
|
|
2395
|
+
* const result = parseApiErrors(payload, { formKey: form.key })
|
|
2396
|
+
* if (result.ok) form.setFieldErrors(result.errors)
|
|
2397
|
+
* ```
|
|
2398
|
+
*/
|
|
2399
|
+
setFieldErrors: (errors: ValidationError[]) => void;
|
|
2400
|
+
/**
|
|
2401
|
+
* Append errors to the existing set without clearing prior entries.
|
|
2402
|
+
* Use when reporting an additional issue alongside existing errors
|
|
2403
|
+
* (e.g. a partial server response).
|
|
2404
|
+
*/
|
|
2405
|
+
addFieldErrors: (errors: ValidationError[]) => void;
|
|
2406
|
+
/**
|
|
2407
|
+
* Clear errors. Pass a path to clear errors for a single field;
|
|
2408
|
+
* call with no arguments to clear every error on the form.
|
|
2409
|
+
*
|
|
2410
|
+
* ```ts
|
|
2411
|
+
* form.clearFieldErrors('email') // clear one field
|
|
2412
|
+
* form.clearFieldErrors() // clear all
|
|
2413
|
+
* ```
|
|
2414
|
+
*/
|
|
2415
|
+
clearFieldErrors: (path?: string | (string | number)[]) => void;
|
|
2416
|
+
/**
|
|
2417
|
+
* Form-level reactive flags, counters, and aggregates (`isDirty`,
|
|
2418
|
+
* `isValid`, `isSubmitting`, `submitCount`, `canUndo`,
|
|
2419
|
+
* `historySize`, and the flat `errors` array). See `FormMeta` for
|
|
2420
|
+
* the full shape. Read leaves directly with no `.value`.
|
|
2421
|
+
*
|
|
2422
|
+
* For per-field state (touched, focused, blurred, errors at one
|
|
2423
|
+
* path), use `form.fields.<path>` instead.
|
|
2424
|
+
*/
|
|
2425
|
+
meta: FormMeta;
|
|
2426
|
+
/**
|
|
2427
|
+
* Restore the form to its initial state. Without arguments,
|
|
2428
|
+
* re-applies the schema defaults (and any `defaultValues` passed
|
|
2429
|
+
* to `useForm`). Pass `nextDefaultValues` to seed the reset with
|
|
2430
|
+
* a fresh set of overrides.
|
|
2431
|
+
*
|
|
2432
|
+
* Resets:
|
|
2433
|
+
* - the form value back to defaults;
|
|
2434
|
+
* - the dirty baseline (so the next edit flips `isDirty` correctly);
|
|
2435
|
+
* - field errors;
|
|
2436
|
+
* - touched / focused / blurred per-field flags;
|
|
2437
|
+
* - submission state (`isSubmitting` / `submitCount` / `submitError`);
|
|
2438
|
+
* - the persisted draft, if persistence is configured.
|
|
2439
|
+
*
|
|
2440
|
+
* The next edit on a still-mounted opted-in input will start
|
|
2441
|
+
* persisting again automatically.
|
|
2442
|
+
*/
|
|
2443
|
+
reset: (nextDefaultValues?: DeepPartial<DefaultValuesShape<Form>>) => void;
|
|
2444
|
+
/**
|
|
2445
|
+
* Restore a single field (or a sub-tree like `'user'`) to its
|
|
2446
|
+
* initial value. Clears errors and touched flags for the field
|
|
2447
|
+
* and its descendants; leaves siblings and submission state alone.
|
|
2448
|
+
*
|
|
2449
|
+
* No-op when the path doesn't exist on the form (e.g. a typo'd
|
|
2450
|
+
* dynamic key).
|
|
2451
|
+
*
|
|
2452
|
+
* If persistence is configured, the matching subpath is removed
|
|
2453
|
+
* from the persisted draft too.
|
|
2454
|
+
*/
|
|
2455
|
+
resetField: (path: FlatPath<Form>) => void;
|
|
2456
|
+
/**
|
|
2457
|
+
* Write the current value at `path` to storage immediately. Useful
|
|
2458
|
+
* for explicit "Save draft" buttons, `beforeunload` handlers, or
|
|
2459
|
+
* multi-step checkpoints where the user shouldn't wait for the
|
|
2460
|
+
* debounce window.
|
|
2461
|
+
*
|
|
2462
|
+
* Bypasses both the per-field opt-in and the debouncer. Existing
|
|
2463
|
+
* paths in the persisted draft are preserved (this is a merge,
|
|
2464
|
+
* not a replace).
|
|
2465
|
+
*
|
|
2466
|
+
* Throws `SensitivePersistFieldError` for sensitive-looking paths
|
|
2467
|
+
* unless you pass `{ acknowledgeSensitive: true }`. No-op when
|
|
2468
|
+
* `useForm({ persist })` wasn't configured.
|
|
2469
|
+
*/
|
|
2470
|
+
persist: (path: FlatPath<Form>, options?: {
|
|
2471
|
+
acknowledgeSensitive?: boolean;
|
|
2472
|
+
}) => Promise<void>;
|
|
2473
|
+
/**
|
|
2474
|
+
* Remove data from the persisted draft. Without arguments, wipes
|
|
2475
|
+
* the entire entry. With a path, removes just that subpath.
|
|
2476
|
+
*
|
|
2477
|
+
* Does not change the in-memory form state — pair with `reset()`
|
|
2478
|
+
* / `resetField()` if you need both. Future edits to still-mounted
|
|
2479
|
+
* opted-in fields will re-populate the entry. No-op when
|
|
2480
|
+
* persistence isn't configured.
|
|
2481
|
+
*/
|
|
2482
|
+
clearPersistedDraft: (path?: FlatPath<Form>) => Promise<void>;
|
|
2483
|
+
/**
|
|
2484
|
+
* Revert the form to the previous snapshot. Returns `true` when a
|
|
2485
|
+
* snapshot was restored, `false` when there's nothing to undo.
|
|
2486
|
+
* No-op (returns `false`) when `useForm({ history })` wasn't configured.
|
|
2487
|
+
*/
|
|
2488
|
+
undo: () => boolean;
|
|
2489
|
+
/**
|
|
2490
|
+
* Replay a previously-undone snapshot. Returns `true` on success,
|
|
2491
|
+
* `false` when the redo stack is empty. The redo stack clears as
|
|
2492
|
+
* soon as a new mutation lands.
|
|
2493
|
+
*/
|
|
2494
|
+
redo: () => boolean;
|
|
2495
|
+
/**
|
|
2496
|
+
* Focus the first errored field's first visible element. Returns
|
|
2497
|
+
* `true` when an element was focused, `false` when no candidate
|
|
2498
|
+
* element exists (no errors, or every errored field is unmounted
|
|
2499
|
+
* or hidden).
|
|
2500
|
+
*
|
|
2501
|
+
* Pass `{ preventScroll: true }` if you're scrolling separately
|
|
2502
|
+
* (e.g. via `scrollToFirstError`) and don't want the browser to
|
|
2503
|
+
* fight the explicit scroll.
|
|
2504
|
+
*/
|
|
2505
|
+
focusFirstError: (options?: {
|
|
2506
|
+
preventScroll?: boolean;
|
|
2507
|
+
}) => boolean;
|
|
2508
|
+
/**
|
|
2509
|
+
* Scroll the first errored field's first visible element into
|
|
2510
|
+
* view. Returns `true` when the call ran, `false` when no
|
|
2511
|
+
* candidate element exists.
|
|
2512
|
+
*
|
|
2513
|
+
* `options` is forwarded to `Element.scrollIntoView` unchanged.
|
|
2514
|
+
*/
|
|
2515
|
+
scrollToFirstError: (options?: ScrollIntoViewOptions) => boolean;
|
|
2516
|
+
/**
|
|
2517
|
+
* Append `value` to the array at `path`.
|
|
2518
|
+
*
|
|
2519
|
+
* ```ts
|
|
2520
|
+
* form.append('items', { name: 'New' })
|
|
2521
|
+
* ```
|
|
2522
|
+
*/
|
|
2523
|
+
append: <Path extends ArrayPath<Form>>(path: Path, value: ArrayItem<Form, Path>) => void;
|
|
2524
|
+
/** Prepend `value` to the array at `path`. */
|
|
2525
|
+
prepend: <Path extends ArrayPath<Form>>(path: Path, value: ArrayItem<Form, Path>) => void;
|
|
2526
|
+
/**
|
|
2527
|
+
* Insert `value` into the array at `path` at the given `index`.
|
|
2528
|
+
* Behaves like `Array.prototype.splice`: `index` is clamped into
|
|
2529
|
+
* `[0, length]`, and negative indices count from the end.
|
|
2530
|
+
*/
|
|
2531
|
+
insert: <Path extends ArrayPath<Form>>(path: Path, index: number, value: ArrayItem<Form, Path>) => void;
|
|
2532
|
+
/** Remove the element at `index` from the array at `path`. No-op when out of range. */
|
|
2533
|
+
remove: <Path extends ArrayPath<Form>>(path: Path, index: number) => void;
|
|
2534
|
+
/** Swap the elements at indices `a` and `b`. No-op when either is out of range. */
|
|
2535
|
+
swap: <Path extends ArrayPath<Form>>(path: Path, a: number, b: number) => void;
|
|
2536
|
+
/**
|
|
2537
|
+
* Move the element at `from` to `to`. Useful for drag-and-drop
|
|
2538
|
+
* reordering. No-op when either index is out of range.
|
|
2539
|
+
*/
|
|
2540
|
+
move: <Path extends ArrayPath<Form>>(path: Path, from: number, to: number) => void;
|
|
2541
|
+
/** Replace the element at `index` with `value`. No-op when out of range. */
|
|
2542
|
+
replace: <Path extends ArrayPath<Form>>(path: Path, index: number, value: ArrayItem<Form, Path>) => void;
|
|
2543
|
+
/**
|
|
2544
|
+
* Read-only view of the form's blank path set. Each entry
|
|
2545
|
+
* is a canonical `PathKey` (the `JSON.stringify(segments)` form
|
|
2546
|
+
* `canonicalizePath` produces). The set is reactive — Vue 3.5
|
|
2547
|
+
* tracks `.has()` / `for..of` / size accesses, so consumers can
|
|
2548
|
+
* drive conditional UI off it directly:
|
|
2549
|
+
*
|
|
2550
|
+
* ```ts
|
|
2551
|
+
* watchEffect(() => {
|
|
2552
|
+
* if (form.blankPaths.value.size > 0) {
|
|
2553
|
+
* console.warn('unanswered fields:', [...form.blankPaths.value])
|
|
2554
|
+
* }
|
|
2555
|
+
* })
|
|
2556
|
+
* ```
|
|
2557
|
+
*
|
|
2558
|
+
* For per-path access, use `form.fields.<path>.blank`.
|
|
2559
|
+
* Writes happen through `setValue(path, unset)`,
|
|
2560
|
+
* `markBlank()` on a register binding, and the directive's
|
|
2561
|
+
* input listener on numeric clear. Mutating the snapshot returned
|
|
2562
|
+
* here does nothing — it's `Object.freeze`-d.
|
|
2563
|
+
*/
|
|
2564
|
+
blankPaths: ComputedRef<ReadonlySet<string>>;
|
|
2565
|
+
};
|
|
2566
|
+
|
|
2567
|
+
export { ROOT_PATH as L, ROOT_PATH_KEY as Q, canonicalizePath as ac, isUnset as ad, parseDottedPath as ae, unset as af };
|
|
2568
|
+
export type { RegisterTransform as $, AttaformDefaults as A, PendingValidationStatus as B, CoercionRegistry as C, DeepPartial as D, PersistConfig as E, FormKey as F, GenericForm as G, HandleSubmit as H, IsTuple as I, PersistConfigOptions as J, PersistIncludeMode as K, MetaTrackerValue as M, NestedReadType as N, OnError as O, Path as P, RegisterValue as R, SlimPrimitiveKind as S, ReactiveValidationStatus as T, UseFormConfiguration as U, ValidationError as V, RegisterDirective as W, RegisterFlatPath as X, RegisterOptions as Y, RegisterSelectModifier as Z, RegisterTextModifier as _, CoercionEntry as a, Segment as a0, SetValueCallback as a1, SetValuePayload as a2, SettledValidationStatus as a3, SlimRuntimeOf as a4, SubmitHandler as a5, Unset as a6, ValidateOn as a7, ValidateOnConfig as a8, ValidationResponse as a9, ValidationResponseWithoutValue as aa, WriteMeta as ab, AbstractSchema as b, DefaultValuesShape as c, UseFormReturnType as d, RegisterModelDynamicCustomDirective as e, ApiErrorEnvelope as f, ApiErrorDetails as g, ApiErrorEntry as h, CoercionResult as i, CustomDirectiveRegisterAssignerFn as j, DefaultValuesResponse as k, FieldState as l, FieldStateLeaf as m, FieldStateMap as n, FieldStateMapEntry as o, FlatPath as p, FormErrorRecord as q, FormErrorsSurface as r, FormMeta as s, FormStorage as t, FormStorageKind as u, HistoryConfig as v, NestedType as w, OnInvalidSubmitPolicy as x, OnSubmit as y, PathKey as z };
|