attaform 0.30.0 → 0.31.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/dist/abstract.d.mts +15 -21
- package/dist/abstract.mjs +3 -3
- package/dist/dev/abstract.mjs +4 -4
- package/dist/dev/chunks/devtools.mjs +4 -4
- package/dist/dev/directive.mjs +1 -1
- package/dist/dev/history.mjs +2 -2
- package/dist/dev/index.mjs +5 -5
- package/dist/dev/runtime/plugins/attaform.mjs +1 -1
- package/dist/dev/shared/{attaform-dev.DUZckyV8.mjs → attaform-dev.B1q-x9R9.mjs} +4 -4
- package/dist/dev/shared/{attaform-dev.BcYT6tFh.mjs → attaform-dev.CZz4GYZ6.mjs} +21 -9
- package/dist/dev/shared/{attaform-dev.BisFstxh.mjs → attaform-dev.Caibt7sb.mjs} +16 -15
- package/dist/dev/shared/{attaform-dev.DTrHonpc.mjs → attaform-dev.CtyqGia7.mjs} +176 -260
- package/dist/dev/shared/{attaform-dev.BK6Ge2qR.mjs → attaform-dev.DJ3nsWw6.mjs} +86 -122
- package/dist/dev/shared/{attaform-dev.BcNtyOmf.mjs → attaform-dev.DlaT4xGd.mjs} +4 -4
- package/dist/dev/shared/{attaform-dev.D6CLvaQT.mjs → attaform-dev.k5X47GMD.mjs} +520 -395
- package/dist/dev/shared/{attaform-dev.CQ2rJiAX.mjs → attaform-dev.y9x9Vp9F.mjs} +324 -234
- package/dist/dev/shared/{attaform-dev.BQc5QV3Y.mjs → attaform-dev.zlvDr0WB.mjs} +2 -2
- package/dist/dev/zod-v3.mjs +6 -6
- package/dist/dev/zod-v4.mjs +6 -6
- package/dist/dev/zod.mjs +5 -5
- package/dist/directive.d.mts +4 -4
- package/dist/directive.mjs +1 -1
- package/dist/esbuild.d.mts +11 -0
- package/dist/esbuild.mjs +1 -1
- package/dist/history.d.mts +3 -32
- package/dist/history.mjs +2 -2
- package/dist/index.d.mts +67 -70
- package/dist/index.mjs +4 -4
- package/dist/nuxt.d.mts +4 -3
- package/dist/nuxt.mjs +3 -6
- package/dist/rollup.d.mts +11 -0
- package/dist/rollup.mjs +1 -1
- package/dist/rspack.d.mts +19 -11
- package/dist/rspack.mjs +1 -1
- package/dist/runtime/components/AttaformDevtoolsPanel.vue +15 -15
- package/dist/runtime/components/DevtoolsValueTree.vue +2 -2
- package/dist/runtime/plugins/attaform.mjs +1 -1
- package/dist/shared/{attaform.BRDDQeHV.mjs → attaform.8mfKZCwH.mjs} +2 -2
- package/dist/shared/{attaform.DRUT703x.d.mts → attaform.B-Tb62qV.d.mts} +12 -12
- package/dist/shared/{attaform.v0fQtWyA.mjs → attaform.B1rH9ht6.mjs} +1 -1
- package/dist/shared/{attaform.DhnPb6nK.mjs → attaform.BAf3UNn4.mjs} +1 -1
- package/dist/shared/{attaform.BXneBxEj.mjs → attaform.BWUn8Kjy.mjs} +85 -121
- package/dist/shared/{attaform.WiGdIqgx.mjs → attaform.C4shmwyJ.mjs} +2 -2
- package/dist/shared/{attaform.Rc9u4Vfw.d.mts → attaform.CRVe8s82.d.mts} +83 -95
- package/dist/shared/attaform.D9RqPgGu.d.mts +42 -0
- package/dist/shared/attaform.DAo0QBmS.d.mts +2173 -0
- package/dist/shared/{attaform.BK0cgu2K.mjs → attaform.DBCbxlyk.mjs} +62 -4
- package/dist/shared/{attaform.CUfrMK7t.mjs → attaform.DL2nl_QR.mjs} +15 -14
- package/dist/shared/{attaform.DNJOWjQw.mjs → attaform.DNPJPOO7.mjs} +299 -218
- package/dist/shared/attaform.DZnrDrNb.d.mts +4657 -0
- package/dist/shared/{attaform.B3resMQ1.mjs → attaform.DxKQsqFZ.mjs} +496 -371
- package/dist/shared/{attaform.ByWP6kSx.mjs → attaform.L011tHgJ.mjs} +3 -3
- package/dist/shared/{attaform.CJ8DpRGj.mjs → attaform.LUPC-2Qg.mjs} +175 -257
- package/dist/transforms.d.mts +153 -26
- package/dist/transforms.mjs +1 -1
- package/dist/vite.d.mts +29 -52
- package/dist/vite.mjs +3 -3
- package/dist/webpack.d.mts +20 -12
- package/dist/webpack.mjs +1 -1
- package/dist/zod-v3.d.mts +90 -96
- package/dist/zod-v3.mjs +5 -5
- package/dist/zod-v4.d.mts +120 -118
- package/dist/zod-v4.mjs +5 -5
- package/dist/zod.d.mts +5 -5
- package/dist/zod.mjs +4 -4
- package/package.json +7 -6
- package/skills/attaform/SKILL.md +1 -1
- package/skills/attaform/references/custom-components.md +4 -0
- package/dist/shared/attaform.Clz0ArIi.d.mts +0 -44
- package/dist/shared/attaform.UGyOK83T.d.mts +0 -2366
- package/dist/shared/attaform.bKLzMU0o.d.mts +0 -4761
|
@@ -0,0 +1,4657 @@
|
|
|
1
|
+
import { ObjectDirective, Ref, ComputedRef, MaybeRefOrGetter } from 'vue';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Schema-attached field metadata: the shared types used by both Zod
|
|
5
|
+
* adapters and the unified `attaform/zod` entry so a consumer's data
|
|
6
|
+
* flow reads the same shape regardless of which path runs at lookup.
|
|
7
|
+
*
|
|
8
|
+
* Storage lives in the cross-adapter `field-meta-store` core: a pair
|
|
9
|
+
* of WeakMaps (single-payload for last-write-wins reads, list-of-
|
|
10
|
+
* payloads for shared-schema disambiguation). Every entry's
|
|
11
|
+
* `fieldMeta` re-exports the same registry-shaped object, so
|
|
12
|
+
* `withMeta`/`fieldMeta.add` writes from one entry surface at lookup
|
|
13
|
+
* through any other.
|
|
14
|
+
*
|
|
15
|
+
* `withMeta(schema, payload)` clones the schema before registering,
|
|
16
|
+
* so each call gets fresh identity (the WeakMap keys on reference).
|
|
17
|
+
* The cloning strategy depends on the major: Zod 4 schemas use the
|
|
18
|
+
* native `.clone()`, Zod 3 schemas reconstruct via constructor +
|
|
19
|
+
* `_def`. The unified entry's `withMeta` runtime-branches on which
|
|
20
|
+
* one is in play.
|
|
21
|
+
*
|
|
22
|
+
* Reads are unified through `AbstractSchema.getFieldMetaAtPath(path)`,
|
|
23
|
+
* which returns a fully-resolved `ResolvedFieldMeta` (label /
|
|
24
|
+
* description / placeholder / meta) so the per-leaf and per-container
|
|
25
|
+
* `FieldState` producers in core never see the version split.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* The metadata a consumer attaches to a schema node: a short label
|
|
29
|
+
* (presentational), longer description (helper text), placeholder
|
|
30
|
+
* (input affordance). Declared as `interface` (not `type`) so
|
|
31
|
+
* downstream apps can extend the shape via TypeScript declaration
|
|
32
|
+
* merging when they want to register richer payloads (tooltips,
|
|
33
|
+
* icons, badge counts, etc.):
|
|
34
|
+
*
|
|
35
|
+
* declare module 'attaform/zod' {
|
|
36
|
+
* interface FieldMetaPayload {
|
|
37
|
+
* tooltip?: string
|
|
38
|
+
* }
|
|
39
|
+
* }
|
|
40
|
+
*
|
|
41
|
+
* After augmentation, `withMeta(schema, { tooltip: '…' })` is typed
|
|
42
|
+
* and `state.meta.tooltip` reads back as `string | undefined`.
|
|
43
|
+
*
|
|
44
|
+
* Every key is optional. Empty payloads (no keys registered) are
|
|
45
|
+
* indistinguishable from "not registered at all", both surfacing as
|
|
46
|
+
* fallbacks (humanize for label, undefined for the rest).
|
|
47
|
+
*/
|
|
48
|
+
interface FieldMetaPayload {
|
|
49
|
+
label?: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
placeholder?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The fully-resolved metadata returned by
|
|
55
|
+
* `AbstractSchema.getFieldMetaAtPath(path)`. Adapters apply the
|
|
56
|
+
* precedence rules:
|
|
57
|
+
*
|
|
58
|
+
* - `label`: registry payload → `humanize(lastSegment)`
|
|
59
|
+
* - `description`: registry payload → schema's `.describe()` value → `undefined`
|
|
60
|
+
* - `placeholder`: registry payload → `undefined`
|
|
61
|
+
* - `meta`: the full registered payload, frozen; `{}` if nothing registered
|
|
62
|
+
*
|
|
63
|
+
* `label` is always a non-empty string at leaves (humanize fallback
|
|
64
|
+
* guarantees this for any non-numeric segment). For containers it
|
|
65
|
+
* may collapse to the empty string when the path is empty (root) or
|
|
66
|
+
* the segment is a numeric index, so callers display "" or substitute
|
|
67
|
+
* a context-appropriate fallback.
|
|
68
|
+
*/
|
|
69
|
+
type ResolvedFieldMeta = {
|
|
70
|
+
readonly label: string;
|
|
71
|
+
readonly description: string | undefined;
|
|
72
|
+
readonly placeholder: string | undefined;
|
|
73
|
+
readonly meta: Readonly<FieldMetaPayload>;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
declare const pathKeyBrand: unique symbol;
|
|
77
|
+
/**
|
|
78
|
+
* Branded string identifier for a canonicalised path, and a useful `Map`
|
|
79
|
+
* key: two paths resolving to the same canonical form produce the same
|
|
80
|
+
* `PathKey`. Treat it as opaque and do not parse it.
|
|
81
|
+
*/
|
|
82
|
+
type PathKey = string & {
|
|
83
|
+
readonly [pathKeyBrand]: 'PathKey';
|
|
84
|
+
};
|
|
85
|
+
/** A single path segment: a property name, or an array index. */
|
|
86
|
+
type Segment = string | number;
|
|
87
|
+
/** A structured path as a read-only sequence of segments. */
|
|
88
|
+
type Path = readonly Segment[];
|
|
89
|
+
/**
|
|
90
|
+
* Parse a dotted-string path into structured segments.
|
|
91
|
+
*
|
|
92
|
+
* ```ts
|
|
93
|
+
* parseDottedPath('user.address.line1') // ['user', 'address', 'line1']
|
|
94
|
+
* parseDottedPath('items.0.name') // ['items', 0, 'name']
|
|
95
|
+
* parseDottedPath('') // [''] (the empty-string key)
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
98
|
+
* The empty-string input `''` is the **literal empty-key path** `['']`,
|
|
99
|
+
* an ordinary (if rare) field address, not the root. Use the array
|
|
100
|
+
* form `[]` for the root. Form-level errors (root `.refine()`,
|
|
101
|
+
* `setErrors` with no path) live at the root path `[]` and are read via
|
|
102
|
+
* `errors([])`, never `errors('')`.
|
|
103
|
+
*
|
|
104
|
+
* Throws `InvalidPathError` for paths with empty INTERNAL segments
|
|
105
|
+
* (`'a..b'`, leading or trailing dots). For keys containing literal
|
|
106
|
+
* dots, pass an array form (`['user.name']`) instead.
|
|
107
|
+
*/
|
|
108
|
+
declare function parseDottedPath(path: string): Segment[];
|
|
109
|
+
/**
|
|
110
|
+
* Canonicalise a path into structured segments plus a stable string
|
|
111
|
+
* key. Accepts either dotted-string or array form; integer-looking
|
|
112
|
+
* segments normalise to numbers.
|
|
113
|
+
*
|
|
114
|
+
* ```ts
|
|
115
|
+
* canonicalizePath('items.0.name')
|
|
116
|
+
* // { segments: ['items', 0, 'name'], key: '["items",0,"name"]' as PathKey }
|
|
117
|
+
*
|
|
118
|
+
* canonicalizePath(['items', 0, 'name'])
|
|
119
|
+
* // → same result
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* The returned `key` works as a `Map` or `Set` key: equal paths produce
|
|
123
|
+
* equal keys whichever input form they arrived in.
|
|
124
|
+
*/
|
|
125
|
+
declare function canonicalizePath(input: string | Path): {
|
|
126
|
+
segments: readonly Segment[];
|
|
127
|
+
key: PathKey;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* The root path, an empty segment tuple. Pass it to any API taking a
|
|
131
|
+
* `Path` to address the form value whole. It is also where form-level
|
|
132
|
+
* errors live: a root `.refine()` message, a hydration failure, and a
|
|
133
|
+
* `setErrors` entry with no path all land at `[]`. The aggregate reads
|
|
134
|
+
* (`errors()`, `errors([])`, `meta.errors`) surface them alongside field
|
|
135
|
+
* errors, and `meta.ownErrors` returns the root bucket alone.
|
|
136
|
+
*
|
|
137
|
+
* The empty SEGMENT tuple `[]` is structurally unconstructible as a field
|
|
138
|
+
* path, so it can never collide with a schema key. The empty STRING key
|
|
139
|
+
* `''` (path `['']`, key `'[""]'`) is an ordinary field address.
|
|
140
|
+
*/
|
|
141
|
+
declare const ROOT_PATH: Path;
|
|
142
|
+
/** Stable string key for the root path. */
|
|
143
|
+
declare const ROOT_PATH_KEY: PathKey;
|
|
144
|
+
/**
|
|
145
|
+
* `true` when `path` starts with every segment of `prefix`, in order. The
|
|
146
|
+
* empty `prefix` matches every path, the ROOT prefix being universal.
|
|
147
|
+
*
|
|
148
|
+
* Walks segments rather than `PathKey` strings because the data it
|
|
149
|
+
* operates on (`meta.errors[].path`, for one) carries segment arrays
|
|
150
|
+
* directly.
|
|
151
|
+
*
|
|
152
|
+
* ```ts
|
|
153
|
+
* isPathPrefix(['cargo'], ['cargo', 'items', 0, 'sku']) // true
|
|
154
|
+
* isPathPrefix(['cargo', 'items'], ['cargo']) // false (path shorter)
|
|
155
|
+
* isPathPrefix([], ['anything']) // true (root prefix)
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
declare function isPathPrefix(prefix: readonly Segment[], path: readonly Segment[]): boolean;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Per-path field status. Replaced wholesale (not mutated in place) on
|
|
162
|
+
* every change. Five semantic groups:
|
|
163
|
+
*
|
|
164
|
+
* - `connected`: is a DOM element registered for this path?
|
|
165
|
+
* - `focused` / `blurred`: DOM-state flags. `null` while no element
|
|
166
|
+
* is connected (no DOM means the concepts don't apply); plain
|
|
167
|
+
* booleans once connected, with the invariant `blurred === !focused`
|
|
168
|
+
* enforced by `markFocused`.
|
|
169
|
+
* - `touched`: focus/blur history, not DOM state. Always a plain
|
|
170
|
+
* boolean: `false` at registration, sticky `true` after first blur,
|
|
171
|
+
* cleared only by `form.reset()` / `form.resetField(path)`. Persists
|
|
172
|
+
* across disconnects so v-if'd-away fields don't lose their touched
|
|
173
|
+
* state on rehide (wizard "show review of touched fields" patterns
|
|
174
|
+
* rely on this).
|
|
175
|
+
* - `interacted`: value-mutation history, not DOM state. Plain
|
|
176
|
+
* boolean: `false` at registration, sticky `true` once the user
|
|
177
|
+
* issues a value edit through the directive's input listeners
|
|
178
|
+
* (never on hydration, default seeding, or programmatic setValue);
|
|
179
|
+
* cleared with `touched` by `form.reset()` / `form.resetField(path)`.
|
|
180
|
+
* - `blurredAfterInteraction`: the first blur that follows a value
|
|
181
|
+
* edit (the field has been edited and then left). Plain boolean,
|
|
182
|
+
* sticky `true`. A tab-through blur with no prior edit does NOT set
|
|
183
|
+
* it (`interacted` is still false at that blur). Composes
|
|
184
|
+
* `interacted` with the departure; drives the default display gate.
|
|
185
|
+
*/
|
|
186
|
+
type FieldRecord = {
|
|
187
|
+
readonly path: Path;
|
|
188
|
+
readonly updatedAt: string | null;
|
|
189
|
+
readonly connected: boolean;
|
|
190
|
+
readonly focused: boolean | null;
|
|
191
|
+
readonly blurred: boolean | null;
|
|
192
|
+
readonly touched: boolean;
|
|
193
|
+
readonly interacted: boolean;
|
|
194
|
+
readonly blurredAfterInteraction: boolean;
|
|
195
|
+
};
|
|
196
|
+
/** Per-path DOM element tracking. Client-only. */
|
|
197
|
+
type ElementRecord = {
|
|
198
|
+
/**
|
|
199
|
+
* Original Path captured at first registration. Stored alongside the
|
|
200
|
+
* elements Set so the DOM-order sort cache can recover the structured
|
|
201
|
+
* Path without round-tripping through `JSON.parse(pathKey)`.
|
|
202
|
+
*/
|
|
203
|
+
readonly path: Path;
|
|
204
|
+
readonly elements: Set<HTMLElement>;
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* Per-path record stored in `originals`. Pairing `segments` with the tracked
|
|
208
|
+
* value means `dirty` and `resetField`'s container loop don't have to
|
|
209
|
+
* `JSON.parse(pathKey)` on every iteration, the canonical Path already
|
|
210
|
+
* sitting next to the value it belongs to. PathKey still keys the Map (the
|
|
211
|
+
* stable string is the only collision-free identifier), but downstream
|
|
212
|
+
* iteration reads `segments` directly.
|
|
213
|
+
*/
|
|
214
|
+
type OriginalsRecord = {
|
|
215
|
+
readonly segments: Path;
|
|
216
|
+
readonly value: unknown;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/** Internal brand for the `Unset` type. Never exposed at runtime. */
|
|
220
|
+
declare const _unsetBrand: unique symbol;
|
|
221
|
+
/**
|
|
222
|
+
* Brand-typed sentinel admitted at every primitive leaf of
|
|
223
|
+
* `DefaultValuesShape<T>`, `setValue`, and `reset`. The runtime value
|
|
224
|
+
* is exported as {@link unset} under the same name.
|
|
225
|
+
*/
|
|
226
|
+
type Unset = typeof _unsetBrand;
|
|
227
|
+
/**
|
|
228
|
+
* The `unset` sentinel: pass it as a primitive leaf's value to mark
|
|
229
|
+
* the field **displayed-empty** while storage holds the schema's slim
|
|
230
|
+
* default (`0` / `''` / `false` / `0n`).
|
|
231
|
+
*
|
|
232
|
+
* Use it wherever a primitive leaf value is expected:
|
|
233
|
+
*
|
|
234
|
+
* ```ts
|
|
235
|
+
* const form = useForm({
|
|
236
|
+
* schema: z.object({ income: z.number() }),
|
|
237
|
+
* defaultValues: { income: unset }, // UI starts blank, storage holds 0
|
|
238
|
+
* key: 'housing',
|
|
239
|
+
* })
|
|
240
|
+
*
|
|
241
|
+
* form.setValue('income', unset) // re-blank a field after a write
|
|
242
|
+
* form.reset({ income: unset }) // reset to the blank state
|
|
243
|
+
* ```
|
|
244
|
+
*
|
|
245
|
+
* Accepted at any `string` / `number` / `boolean` / `bigint` leaf in
|
|
246
|
+
* `defaultValues`, `setValue(path, unset)`, and `form.reset({ … })`.
|
|
247
|
+
*
|
|
248
|
+
* The path joins the form's `blankPaths` set as long as it stays
|
|
249
|
+
* unset. Required schemas (no `.optional()` / `.nullable()` /
|
|
250
|
+
* `.default(N)`) raise `"No value supplied"` on submit while a leaf
|
|
251
|
+
* is in `blankPaths`; optional / nullable / has-default schemas
|
|
252
|
+
* accept the empty case as their wrapper allows.
|
|
253
|
+
*
|
|
254
|
+
* Storage never holds the symbol; the runtime translates it at the
|
|
255
|
+
* API boundary, so reads through `form.values` always see the slim
|
|
256
|
+
* default. Cross-bundle / SSR-safe: backed by `Symbol.for(...)` so
|
|
257
|
+
* every realm gets the same sentinel.
|
|
258
|
+
*
|
|
259
|
+
* @see {@link isUnset}, the type guard that narrows a value back to {@link Unset}.
|
|
260
|
+
* @see `docs/validation/blank.md` for the model behind blank-aware fields.
|
|
261
|
+
*/
|
|
262
|
+
declare const unset: Unset;
|
|
263
|
+
/**
|
|
264
|
+
* Type guard: `true` when `value` is the `unset` sentinel.
|
|
265
|
+
*
|
|
266
|
+
* ```ts
|
|
267
|
+
* if (isUnset(payload.income)) {
|
|
268
|
+
* // payload.income is the sentinel; the field will display empty
|
|
269
|
+
* }
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
declare function isUnset(value: unknown): value is Unset;
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The minimum shape any form value satisfies, a plain record. Use it as
|
|
276
|
+
* a constraint for composables that work generically across forms, such
|
|
277
|
+
* as a custom hook taking any form's `useForm` return.
|
|
278
|
+
*/
|
|
279
|
+
type GenericForm = Record<string, unknown>;
|
|
280
|
+
/** `true` when `T` is an object or array. */
|
|
281
|
+
type IsObjectOrArray<T> = T extends GenericForm ? true : T extends Array<unknown> ? true : false;
|
|
282
|
+
/**
|
|
283
|
+
* Shared recursion body backing `PartialFlatPath` and
|
|
284
|
+
* `RegisterFlatPath`. One walk over `Form`, with `Mode` deciding
|
|
285
|
+
* whether container paths are emitted alongside their reachable leaves.
|
|
286
|
+
*
|
|
287
|
+
* - `'partial'`: emit every container path (object containers,
|
|
288
|
+
* nested-object array roots, array-of-object element containers).
|
|
289
|
+
* Used by `setValue`, `form.values.<path>`, and every read-side API
|
|
290
|
+
* that can address a container.
|
|
291
|
+
* - `'register'`: skip container paths, since `v-register` binds onto a
|
|
292
|
+
* leaf-backing native element. Primitive arrays still admit the
|
|
293
|
+
* array-root path under both modes, because multi-select and
|
|
294
|
+
* grouped-checkbox bindings register onto the array itself.
|
|
295
|
+
*/
|
|
296
|
+
type FlatPathBuilder<Form, Mode extends 'partial' | 'register', 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 ? Mode extends 'partial' ? `${Key}` | `${Key}.${number}` | `${Key}.${number}.${FlatPathBuilder<ArrayItem, Mode>}` : `${Key}.${number}.${FlatPathBuilder<ArrayItem, Mode>}` : `${Key}` | `${Key}.${number}` : Value extends GenericForm ? Mode extends 'partial' ? `${Key}` | `${Key}.${FlatPathBuilder<Value, Mode>}` : `${Key}.${FlatPathBuilder<Value, Mode>}` : `${Key}` : never : Key extends number ? `${Key}` | (Form[Key] extends GenericForm ? `${Key}.${FlatPathBuilder<Form[Key], Mode>}` : Form[Key] extends Array<infer ArrayItem> ? IsObjectOrArray<ArrayItem> extends true ? Mode extends 'partial' ? `${Key}.${number}` | `${Key}.${number}.${FlatPathBuilder<ArrayItem, Mode>}` : `${Key}.${number}.${FlatPathBuilder<ArrayItem, Mode>}` : `${Key}.${number}` : never) : never : never;
|
|
297
|
+
/**
|
|
298
|
+
* Backs `FlatPath` in its default partial-path mode. Reach for
|
|
299
|
+
* `FlatPath`; this alias is not part of the stable surface.
|
|
300
|
+
*
|
|
301
|
+
* It is exported so `rollup-plugin-dts` keeps it as a named alias in
|
|
302
|
+
* the bundled `.d.ts` rather than inlining the full template-literal
|
|
303
|
+
* recursion at every reference site. Inlined, it compounds into TS2589
|
|
304
|
+
* territory once several complex forms share a scope.
|
|
305
|
+
*
|
|
306
|
+
* The `Form extends unknown` wrapper distributes over a top-level
|
|
307
|
+
* union so each member contributes its OWN `keyof`. That is what makes
|
|
308
|
+
* a `z.discriminatedUnion` root expose every variant's keys as
|
|
309
|
+
* addressable paths, rather than the intersection that naked
|
|
310
|
+
* `keyof (A | B)` collapses to. Interior unions already distribute
|
|
311
|
+
* inside `FlatPathBuilder` at its `infer Value` step.
|
|
312
|
+
*/
|
|
313
|
+
type PartialFlatPath<Form> = Form extends unknown ? FlatPathBuilder<Form, 'partial'> : never;
|
|
314
|
+
/**
|
|
315
|
+
* Union of dotted-string paths reachable inside `Form`. For
|
|
316
|
+
* `{ user: { email: string }, items: string[] }`:
|
|
317
|
+
*
|
|
318
|
+
* `'user' | 'user.email' | 'items' | 'items.0' | 'items.1' | ...`
|
|
319
|
+
*
|
|
320
|
+
* Every path-addressed API (`setValue(path, value)`, `register(path)`,
|
|
321
|
+
* `toRef(path)`) takes one, so paths autocomplete and typos are
|
|
322
|
+
* compile errors.
|
|
323
|
+
*/
|
|
324
|
+
type FlatPath<Form> = PartialFlatPath<Form>;
|
|
325
|
+
/**
|
|
326
|
+
* Convert a tuple of path segments to its dotted-string equivalent.
|
|
327
|
+
*
|
|
328
|
+
* `JoinSegments<['cargo', 'items', 0, 'sku']>` is `'cargo.items.0.sku'`
|
|
329
|
+
*
|
|
330
|
+
* Depth is bounded by the tuple length, typically 3 or 4, so the cost
|
|
331
|
+
* does not scale with `FlatPath<Form>`. Template literal types
|
|
332
|
+
* distribute over unions, so a segment union propagates into the joined
|
|
333
|
+
* path: `JoinSegments<['pickup' | 'delivery', 'line1']>` is
|
|
334
|
+
* `'pickup.line1' | 'delivery.line1'`. That is what makes tuple-form
|
|
335
|
+
* path APIs work inside a `v-for` over a prefix variable, since the
|
|
336
|
+
* joined result is checked against the existing `FlatPath<Form>` /
|
|
337
|
+
* `RegisterFlatPath<Form>` unions instead of a separate enumeration.
|
|
338
|
+
*/
|
|
339
|
+
type JoinSegments<S extends ReadonlyArray<string | number>, Acc extends string = ''> = S extends readonly [
|
|
340
|
+
infer Head extends string | number,
|
|
341
|
+
...infer Rest extends ReadonlyArray<string | number>
|
|
342
|
+
] ? Acc extends '' ? JoinSegments<Rest, `${Head}`> : JoinSegments<Rest, `${Acc}.${Head}`> : Acc;
|
|
343
|
+
/**
|
|
344
|
+
* The advice half of a segment-array rejection, used when `S` never
|
|
345
|
+
* inferred a tuple at all.
|
|
346
|
+
*
|
|
347
|
+
* A caller passing a plain `string` lands here: inference falls back to
|
|
348
|
+
* the constraint, and `JoinSegments` of a non-tuple array is `''`.
|
|
349
|
+
* Naming a path would be a lie, so the message names the remedies
|
|
350
|
+
* instead. This is the shape behind #568, where a row component
|
|
351
|
+
* received its path prefix as an untyped `string` prop and every
|
|
352
|
+
* concatenation widened to `string`.
|
|
353
|
+
*/
|
|
354
|
+
type PlainStringAdvice = 'attaform: a plain string cannot be checked against the schema. Pass a literal path, type the dynamic prefix, or use the segment-array form.';
|
|
355
|
+
/**
|
|
356
|
+
* Why a segment array was rejected, as a type the compiler prints.
|
|
357
|
+
*
|
|
358
|
+
* Every segment-array overload brands its parameter
|
|
359
|
+
* `S & (<joined path is valid> ? unknown : <this>)`. The brand must
|
|
360
|
+
* stay a string literal: `S & never` is `never`, which makes TypeScript
|
|
361
|
+
* report the parameter as `never` and say nothing about what was wrong,
|
|
362
|
+
* while a string literal intersects without collapsing, so the sentence
|
|
363
|
+
* survives into the diagnostic and the argument keeps its inferred
|
|
364
|
+
* tuple type:
|
|
365
|
+
*
|
|
366
|
+
* ```
|
|
367
|
+
* Argument of type '["boxes", 3, "nope"]' is not assignable to parameter
|
|
368
|
+
* of type 'readonly ["boxes", 3, "nope"] &
|
|
369
|
+
* "attaform: 'boxes.3.nope' is not a path in this form's schema"'.
|
|
370
|
+
* ```
|
|
371
|
+
*
|
|
372
|
+
* Nothing can satisfy the brand, so a wrong path stays a compile error;
|
|
373
|
+
* only the message changes.
|
|
374
|
+
*/
|
|
375
|
+
type SegmentPathRejection<Joined extends string> = [Joined] extends [''] ? PlainStringAdvice : `attaform: '${Joined}' is not a path in this form's schema`;
|
|
376
|
+
/**
|
|
377
|
+
* `SegmentPathRejection` for `register`, whose accepted set is
|
|
378
|
+
* `RegisterFlatPath` rather than `FlatPath`.
|
|
379
|
+
*
|
|
380
|
+
* Worth its own sentence: a container path is in the schema and still
|
|
381
|
+
* not registrable, so "not a path in this form's schema" would be false
|
|
382
|
+
* exactly where it is most likely to be read.
|
|
383
|
+
*/
|
|
384
|
+
type SegmentRegisterRejection<Joined extends string> = [Joined] extends [''] ? PlainStringAdvice : `attaform: '${Joined}' is not a registrable path. v-register binds a leaf input, so container paths are excluded.`;
|
|
385
|
+
/**
|
|
386
|
+
* `true` when `T` is a union, `false` for a single type. Gates
|
|
387
|
+
* non-homomorphic mapped-type forms so a single object type keeps its
|
|
388
|
+
* homomorphic `[K in keyof T]` lookup, preserving literal keys instead
|
|
389
|
+
* of widening to an index signature.
|
|
390
|
+
*/
|
|
391
|
+
type IsUnion<T, U = T> = T extends T ? ([U] extends [T] ? false : true) : never;
|
|
392
|
+
/**
|
|
393
|
+
* Union of all keys across all members of `T`. For a single object type
|
|
394
|
+
* this equals `keyof T`; for `A | B` it produces `keyof A | keyof B`,
|
|
395
|
+
* where naked `keyof (A | B)` would intersect to common keys only.
|
|
396
|
+
*
|
|
397
|
+
* Paired with `ValueOfUnion` to merge variant key sets in the chained
|
|
398
|
+
* metadata proxies (`form.fields`, `form.errors`), so per-variant
|
|
399
|
+
* leaves stay addressable through one shape whichever discriminant is
|
|
400
|
+
* active.
|
|
401
|
+
*/
|
|
402
|
+
type KeyofUnion<T> = T extends unknown ? keyof T : never;
|
|
403
|
+
/**
|
|
404
|
+
* Value at key `K` across union members of `T`. Members holding `K`
|
|
405
|
+
* contribute `T[K]`; members lacking it contribute `undefined`.
|
|
406
|
+
*
|
|
407
|
+
* That mirrors the metadata proxies at runtime: chained access works at
|
|
408
|
+
* every union member, and the leaf carries `T | undefined` because the
|
|
409
|
+
* key is absent in some variants, where the runtime returns a stable
|
|
410
|
+
* stub.
|
|
411
|
+
*/
|
|
412
|
+
type ValueOfUnion<T, K extends PropertyKey> = T extends unknown ? K extends keyof T ? T[K] : undefined : never;
|
|
413
|
+
/**
|
|
414
|
+
* Value at key `K` across union members of `T`, dropping members that
|
|
415
|
+
* LACK `K` entirely: they contribute `never`, not `undefined`. The
|
|
416
|
+
* counterpart to `ValueOfUnion`, which injects a synthetic `undefined`
|
|
417
|
+
* for absent-variant keys so chained reads stay safe.
|
|
418
|
+
*
|
|
419
|
+
* `form.fields` uses it at discriminated-union keys so a variant-only
|
|
420
|
+
* field types as node-optional `FieldState<X> | undefined`, the node
|
|
421
|
+
* being absent when its variant is not active, rather than
|
|
422
|
+
* value-optional `FieldState<X | undefined>`, which would falsely
|
|
423
|
+
* promise a readable node. A genuine `undefined` from an OPTIONAL
|
|
424
|
+
* declaration survives; only the synthetic one is stripped.
|
|
425
|
+
*/
|
|
426
|
+
type PresentValueOfUnion<T, K extends PropertyKey> = T extends unknown ? K extends keyof T ? T[K] : never : never;
|
|
427
|
+
/**
|
|
428
|
+
* Apply the discriminated-union key merge to a shape carrying values
|
|
429
|
+
* rather than metadata leaves. Single object types map homomorphically;
|
|
430
|
+
* unions of objects merge keys via `KeyofUnion` / `ValueOfUnion` so
|
|
431
|
+
* per-variant fields are reachable through one chained-access shape.
|
|
432
|
+
*
|
|
433
|
+
* `ValuesSurface` uses it to make an oversized-only
|
|
434
|
+
* `form.values.cargo.permitNumber` typecheck whichever variant is
|
|
435
|
+
* active, matching the runtime, where plain JS access on a missing key
|
|
436
|
+
* returns `undefined` rather than throwing.
|
|
437
|
+
*
|
|
438
|
+
* Date / Map / Set / RegExp / function leaves stay opaque, since a
|
|
439
|
+
* value read of those should preserve the platform shape.
|
|
440
|
+
*/
|
|
441
|
+
type LiftedValueShape<T> = [T] extends [
|
|
442
|
+
string | number | boolean | bigint | symbol | null | undefined
|
|
443
|
+
] ? T : [T] extends [
|
|
444
|
+
Date | RegExp | Map<unknown, unknown> | Set<unknown> | ((...args: never) => unknown)
|
|
445
|
+
] ? T : [T] extends [ReadonlyArray<unknown>] ? T : [T] extends [object] ? [IsUnion<T>] extends [true] ? {
|
|
446
|
+
[K in KeyofUnion<T>]: LiftedValueShape<ValueOfUnion<T, K>>;
|
|
447
|
+
} : {
|
|
448
|
+
[K in keyof T]: LiftedValueShape<T[K]>;
|
|
449
|
+
} : T;
|
|
450
|
+
/**
|
|
451
|
+
* Recursive `Partial`: every property at every depth is optional, so a
|
|
452
|
+
* partial override at any nesting level is valid.
|
|
453
|
+
*/
|
|
454
|
+
type DeepPartial<T> = T extends Primitive ? T : T extends Array<infer ArrayItem> ? DeepPartial<ArrayItem>[] : T extends object ? {
|
|
455
|
+
[Key in keyof T]?: DeepPartial<T[Key]>;
|
|
456
|
+
} : T;
|
|
457
|
+
/**
|
|
458
|
+
* Shared descent body backing `NestedType` and `NestedReadType`. Both
|
|
459
|
+
* walk segment by segment, distributing over union members via
|
|
460
|
+
* `KeyofUnion` / `ValueOfUnion`, and diverge only at the leaf:
|
|
461
|
+
*
|
|
462
|
+
* - `TaintArrayCrossings extends false` (`NestedType`): leaves are
|
|
463
|
+
* returned untouched, which is what the strict write-side APIs need
|
|
464
|
+
* (`setValue`'s value parameter, `form.fields.<path>`).
|
|
465
|
+
* - `TaintArrayCrossings extends true` (`NestedReadType`): leaves widen
|
|
466
|
+
* with `| undefined` once any segment in the walk was an array index,
|
|
467
|
+
* reflecting an out-of-bounds read at runtime.
|
|
468
|
+
*
|
|
469
|
+
* `_Tainted` carries the array crossing under taint mode and stays
|
|
470
|
+
* `false` through every arm under strict mode. Both strip nullishness
|
|
471
|
+
* at the root.
|
|
472
|
+
*
|
|
473
|
+
* Reach for `NestedType` or `NestedReadType`; this is not part of the
|
|
474
|
+
* stable surface.
|
|
475
|
+
*/
|
|
476
|
+
type NestedTypeBuilder<RootValue, FlattenedPath extends string, TaintArrayCrossings extends boolean, _Tainted extends boolean = false, _RootValue = NonNullable<RootValue>> = IsObjectOrArray<_RootValue> extends false ? never : FlattenedPath extends `${infer Key}.${infer Rest}` ? Key extends `${number}` ? Key extends KeyofUnion<_RootValue> ? NestedTypeBuilder<ValueOfUnion<_RootValue, Key>, Rest, TaintArrayCrossings, TaintArrayCrossings extends true ? true : _Tainted> : Key extends `${infer NumericKey extends number}` ? NumericKey extends KeyofUnion<_RootValue> ? NestedTypeBuilder<ValueOfUnion<_RootValue, NumericKey>, Rest, TaintArrayCrossings, TaintArrayCrossings extends true ? true : _Tainted> : never : never : Key extends KeyofUnion<_RootValue> ? NestedTypeBuilder<ValueOfUnion<_RootValue, Key>, Rest, TaintArrayCrossings, _Tainted> : never : FlattenedPath extends `${number}` ? FlattenedPath extends KeyofUnion<_RootValue> ? TaintArrayCrossings extends true ? ValueOfUnion<_RootValue, FlattenedPath> | undefined : ValueOfUnion<_RootValue, FlattenedPath> : FlattenedPath extends `${infer NumericKey extends number}` ? NumericKey extends KeyofUnion<_RootValue> ? TaintArrayCrossings extends true ? ValueOfUnion<_RootValue, NumericKey> | undefined : ValueOfUnion<_RootValue, NumericKey> : never : never : FlattenedPath extends KeyofUnion<_RootValue> ? _Tainted extends true ? ValueOfUnion<_RootValue, FlattenedPath> | undefined : ValueOfUnion<_RootValue, FlattenedPath> : never;
|
|
477
|
+
/**
|
|
478
|
+
* Resolve the type at a dotted-string path inside `RootValue`, for the
|
|
479
|
+
* strict write-side APIs:
|
|
480
|
+
*
|
|
481
|
+
* `NestedType<{ user: { email: string } }, 'user.email'>` is `string`
|
|
482
|
+
*
|
|
483
|
+
* On a discriminated-union descent it uses `KeyofUnion` /
|
|
484
|
+
* `ValueOfUnion`, so a per-variant key resolves to `T | undefined`
|
|
485
|
+
* rather than `never`. That keeps `NestedType` in lockstep with
|
|
486
|
+
* `FlatPath`: every path `FlatPath` says is reachable resolves to a
|
|
487
|
+
* useful value type, instead of collapsing to the intersection of all
|
|
488
|
+
* variants' keys.
|
|
489
|
+
*
|
|
490
|
+
* TypeScript caps conditional-type recursion near 50 levels, so paths
|
|
491
|
+
* deeper than that resolve to `never`. Real schemas do not reach it.
|
|
492
|
+
*/
|
|
493
|
+
type NestedType<RootValue, FlattenedPath extends string> = NestedTypeBuilder<RootValue, FlattenedPath, false>;
|
|
494
|
+
/**
|
|
495
|
+
* Primitive-leaf marker used by `DeepPartial` and the sibling
|
|
496
|
+
* structural walkers. Reach for `DeepPartial`; this is not part of the
|
|
497
|
+
* stable surface, and is exported only so the bundled `.d.ts`
|
|
498
|
+
* references one alias instead of re-emitting the union at every
|
|
499
|
+
* recursion branch of every walker.
|
|
500
|
+
*/
|
|
501
|
+
type Primitive = string | number | boolean | symbol | bigint | null | undefined;
|
|
502
|
+
/**
|
|
503
|
+
* Distinguish a tuple from a regular array, for write-side helpers that
|
|
504
|
+
* must preserve tuple positions instead of widening to
|
|
505
|
+
* `Array<element>`.
|
|
506
|
+
*
|
|
507
|
+
* `IsTuple<[string, number]>` is `true`
|
|
508
|
+
* `IsTuple<string[]>` is `false`
|
|
509
|
+
*/
|
|
510
|
+
type IsTuple<T extends readonly unknown[]> = number extends T['length'] ? false : true;
|
|
511
|
+
/**
|
|
512
|
+
* Path-resolved type for the read-side APIs (`form.values.<path>`,
|
|
513
|
+
* `form.toRef(path)`, `register(path).innerRef`). Like `NestedType`,
|
|
514
|
+
* except that once the walk crosses an array index the result is tagged
|
|
515
|
+
* `| undefined`, honouring the out-of-bounds read the runtime allows.
|
|
516
|
+
* Discriminated-union descents follow the same rule as `NestedType`.
|
|
517
|
+
*/
|
|
518
|
+
type NestedReadType<RootValue, FlattenedPath extends string> = NestedTypeBuilder<RootValue, FlattenedPath, true>;
|
|
519
|
+
/**
|
|
520
|
+
* The array a path actually addresses, nullish stripped, or `never`
|
|
521
|
+
* when the path does not address an array. Shared by `ArrayPath`
|
|
522
|
+
* (membership) and `ArrayItem` (element type) so the filter and the
|
|
523
|
+
* extractor cannot disagree about what counts as one.
|
|
524
|
+
*
|
|
525
|
+
* The strip is load-bearing. `Form` is the schema's INPUT shape, so
|
|
526
|
+
* `z.array(row).default([])` resolves to `row[] | undefined`, and
|
|
527
|
+
* `NestedType` tags a discriminated union's per-variant keys the same
|
|
528
|
+
* way on purpose. A bare `extends readonly unknown[]` predicate reads
|
|
529
|
+
* that tag as "not an array", which is how every optional, defaulted,
|
|
530
|
+
* nullable and DU-variant array fell out of the field-array helpers
|
|
531
|
+
* while the runtime went on accepting all of them (#541).
|
|
532
|
+
*
|
|
533
|
+
* `extends infer Leaf` binds the stripped leaf once so the guards below
|
|
534
|
+
* do not re-instantiate `NestedType`, and the tuple wraps suppress
|
|
535
|
+
* distribution so a union leaf is judged whole: `A[] | B[]` is an
|
|
536
|
+
* array, `A[] | string` is not.
|
|
537
|
+
*
|
|
538
|
+
* Reach for `ArrayPath` / `ArrayItem`; this alias exists so
|
|
539
|
+
* `rollup-plugin-dts` keeps one named reference in the bundled `.d.ts`.
|
|
540
|
+
*/
|
|
541
|
+
type ArrayLeafOf<Form, P extends string> = Exclude<NestedType<Form, P>, undefined | null> extends infer Leaf ? [Leaf] extends [never] ? never : [Leaf] extends [readonly unknown[]] ? Leaf : never : never;
|
|
542
|
+
/**
|
|
543
|
+
* `FlatPath<Form>` narrowed to the paths whose leaf is an array, so the
|
|
544
|
+
* typed field-array helpers (append, remove, swap) accept only paths
|
|
545
|
+
* that address one: `append('email', ...)` on a `{ email: string }` is
|
|
546
|
+
* a compile error. Optionality is not a disqualifier; see `ArrayLeafOf`
|
|
547
|
+
* for what counts as an array here.
|
|
548
|
+
*
|
|
549
|
+
* `P extends string` re-triggers distribution over the `FlatPath<Form>`
|
|
550
|
+
* union so the conditional evaluates per member. Without it the branch
|
|
551
|
+
* reduces against the union as a whole and collapses to `never` as soon
|
|
552
|
+
* as one member fails the predicate.
|
|
553
|
+
*/
|
|
554
|
+
type ArrayPath<Form, P extends FlatPath<Form> = FlatPath<Form>> = P extends string ? [ArrayLeafOf<Form, P>] extends [never] ? never : P : never;
|
|
555
|
+
/**
|
|
556
|
+
* Element type of the array addressed by `Path`. Callers constrain
|
|
557
|
+
* `Path extends ArrayPath<Form>`, so this is always well-defined. It
|
|
558
|
+
* rides on `ArrayLeafOf`, the same alias `ArrayPath` admits the path
|
|
559
|
+
* with, so an accepted path never hands its helper a `never` value
|
|
560
|
+
* slot.
|
|
561
|
+
*/
|
|
562
|
+
type ArrayItem<Form, Path extends ArrayPath<Form>> = ArrayLeafOf<Form, Path> extends ReadonlyArray<infer Item> ? Item : never;
|
|
563
|
+
/**
|
|
564
|
+
* The record a path actually addresses, nullish stripped, or `never`
|
|
565
|
+
* when the path does not address one. Shared by `RecordPath`
|
|
566
|
+
* (membership) and `RecordValue` (value type).
|
|
567
|
+
*
|
|
568
|
+
* `string extends keyof Leaf` is the index-signature probe: it holds
|
|
569
|
+
* for `Record<string, V>`, whose `keyof` is `string`, and fails for a
|
|
570
|
+
* fixed object, whose `keyof` is a literal key union. The leading array
|
|
571
|
+
* guard keeps arrays, which also satisfy the object check, out of the
|
|
572
|
+
* record set.
|
|
573
|
+
*
|
|
574
|
+
* Nullish is stripped first for the reason `ArrayLeafOf` gives, with an
|
|
575
|
+
* extra bite here: `keyof (Record<string, V> | undefined)` is `never`,
|
|
576
|
+
* so on a `.default({})` or `.optional()` record the probe could not
|
|
577
|
+
* fire at all and `form.record(path)` rejected the path (#541).
|
|
578
|
+
*
|
|
579
|
+
* Reach for `RecordPath` / `RecordValue`; exported for the same `.d.ts`
|
|
580
|
+
* reason as `ArrayLeafOf`.
|
|
581
|
+
*/
|
|
582
|
+
type RecordLeafOf<Form, P extends string> = Exclude<NestedType<Form, P>, undefined | null> extends infer Leaf ? [Leaf] extends [never] ? never : [Leaf] extends [readonly unknown[]] ? never : [Leaf] extends [Record<string, unknown>] ? string extends keyof Leaf ? Leaf : never : never : never;
|
|
583
|
+
/**
|
|
584
|
+
* Companion to `ArrayPath`: `FlatPath<Form>` narrowed to the paths
|
|
585
|
+
* whose leaf is a record, meaning an object with an open string-keyed
|
|
586
|
+
* index signature such as `z.record(z.string(), V)`. A fixed-shape
|
|
587
|
+
* `z.object({ ... })` is excluded, since its keys are statically known
|
|
588
|
+
* and it has no `string` index signature.
|
|
589
|
+
*/
|
|
590
|
+
type RecordPath<Form, P extends FlatPath<Form> = FlatPath<Form>> = P extends string ? [RecordLeafOf<Form, P>] extends [never] ? never : P : never;
|
|
591
|
+
/**
|
|
592
|
+
* Value type of the record addressed by `Path`, the `V` in a
|
|
593
|
+
* `Record<string, V>`. Callers constrain `Path extends
|
|
594
|
+
* RecordPath<Form>`, so the leaf is always an open string-keyed record.
|
|
595
|
+
* It rides on `RecordLeafOf`, the same alias `RecordPath` admits the
|
|
596
|
+
* path with, so the two cannot disagree.
|
|
597
|
+
*/
|
|
598
|
+
type RecordValue<Form, Path extends RecordPath<Form>> = RecordLeafOf<Form, Path> extends Record<string, infer Value> ? Value : never;
|
|
599
|
+
/**
|
|
600
|
+
* Widens primitive-literal leaves to their primitive supertype, to
|
|
601
|
+
* match the runtime's slim-primitive write contract.
|
|
602
|
+
*
|
|
603
|
+
* WriteShape<{ color: 'red' | 'green' }>
|
|
604
|
+
* // { color: string }
|
|
605
|
+
* WriteShape<{ count: 42 }>
|
|
606
|
+
* // { count: number }
|
|
607
|
+
*
|
|
608
|
+
* The runtime write gate accepts any value whose primitive type matches
|
|
609
|
+
* the schema's slim primitive set at that path. Refinement-level
|
|
610
|
+
* constraints (enum membership, literal equality, format checks, length
|
|
611
|
+
* and range bounds, regex, custom predicates) are not enforced at write
|
|
612
|
+
* time; they surface through field-level validation. So
|
|
613
|
+
* `setValue('color', 'magenta')` and `defaultValues: { color: 'teal' }`
|
|
614
|
+
* are not type errors despite being out-of-enum at the validation
|
|
615
|
+
* layer.
|
|
616
|
+
*
|
|
617
|
+
* Tuple positions keep their literal types through the homomorphic
|
|
618
|
+
* mapped form, so `[string, number]` stays a 2-tuple of widened
|
|
619
|
+
* primitives instead of collapsing to `Array<string | number>`, and
|
|
620
|
+
* tuple detection runs before the array branch so positionally-typed
|
|
621
|
+
* literals survive. Date / RegExp / Map / Set / function instances pass
|
|
622
|
+
* through unchanged, since the runtime accepts them as their own slim
|
|
623
|
+
* kinds.
|
|
624
|
+
*
|
|
625
|
+
* This stays STRICT, with no `| Unset` widening, because it types the
|
|
626
|
+
* prev-value argument of `setValue(path, prev => ...)`, which always
|
|
627
|
+
* receives a real value, plus read-side types and internals like
|
|
628
|
+
* `FieldStateMap<T>`. The consumer-facing write-value type is
|
|
629
|
+
* `DefaultValuesShape<T>`, which adds `| Unset` at every recursable
|
|
630
|
+
* position.
|
|
631
|
+
*/
|
|
632
|
+
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[]] ? {
|
|
633
|
+
-readonly [K in keyof T]: WriteShape<T[K]>;
|
|
634
|
+
} : T extends ReadonlyArray<infer U> ? IsTuple<T> extends true ? {
|
|
635
|
+
-readonly [K in keyof T]: WriteShape<T[K]>;
|
|
636
|
+
} : Array<WriteShape<U>> : T extends object ? {
|
|
637
|
+
[K in keyof T]: WriteShape<T[K]>;
|
|
638
|
+
} : T;
|
|
639
|
+
/**
|
|
640
|
+
* Walk `T` and add `| Unset` at every primitive leaf except symbol,
|
|
641
|
+
* null and undefined, at every opaque leaf (`Date`, `RegExp`, `Map`,
|
|
642
|
+
* `Set`, functions), and at every container position. The recursion
|
|
643
|
+
* topology mirrors `WriteShape<T>`, which is what lets
|
|
644
|
+
* `DefaultValuesShape<T>` be a one-line composition.
|
|
645
|
+
*
|
|
646
|
+
* Symbol / null / undefined leaves pass through untouched, so the
|
|
647
|
+
* runtime sentinel does not pollute leaf semantics it has no business
|
|
648
|
+
* carrying. Container positions widen so a single `unset` at any level
|
|
649
|
+
* recursively marks every descendant primitive blank.
|
|
650
|
+
*
|
|
651
|
+
* Reach for `DefaultValuesShape`; this is not part of the stable
|
|
652
|
+
* surface.
|
|
653
|
+
*/
|
|
654
|
+
type AugmentWithUnset<T> = T extends string | number | boolean | bigint ? T | Unset : T extends symbol | null | undefined ? T : T extends Date | RegExp | Map<unknown, unknown> | Set<unknown> | ((...args: never) => unknown) ? T | Unset : T extends readonly [unknown, ...unknown[]] ? {
|
|
655
|
+
-readonly [K in keyof T]: AugmentWithUnset<T[K]>;
|
|
656
|
+
} | Unset : T extends ReadonlyArray<infer U> ? IsTuple<T> extends true ? {
|
|
657
|
+
-readonly [K in keyof T]: AugmentWithUnset<T[K]>;
|
|
658
|
+
} | Unset : Array<AugmentWithUnset<U>> | Unset : T extends object ? {
|
|
659
|
+
[K in keyof T]: AugmentWithUnset<T[K]>;
|
|
660
|
+
} | Unset : T;
|
|
661
|
+
/**
|
|
662
|
+
* `WriteShape<T>` plus `Unset`, the brand-typed sentinel that marks a
|
|
663
|
+
* leaf as starting displayed-empty in `defaultValues`, `setValue` and
|
|
664
|
+
* `reset`.
|
|
665
|
+
*
|
|
666
|
+
* `Unset` is admitted at every position: primitive leaves, opaque
|
|
667
|
+
* leaves (`Date`, `RegExp`, `Map`, `Set`, functions), and containers
|
|
668
|
+
* (objects, arrays, tuples, records, discriminated unions, optional and
|
|
669
|
+
* nullable wrappers). A container `unset` recursively marks every
|
|
670
|
+
* primitive descendant blank, so `defaultValues: { profile: unset }`
|
|
671
|
+
* and `setValue('cargo', unset)` typecheck cleanly.
|
|
672
|
+
*
|
|
673
|
+
* DefaultValuesShape<{ income: number; name: string; age: 21 }>
|
|
674
|
+
* // { income: number | Unset; name: string | Unset; age: number | Unset } | Unset
|
|
675
|
+
*/
|
|
676
|
+
type DefaultValuesShape<T> = AugmentWithUnset<WriteShape<T>>;
|
|
677
|
+
/**
|
|
678
|
+
* The type accepted at `defaultValues` and at `reset()`'s parameter:
|
|
679
|
+
* `DeepPartial` and `DefaultValuesShape` fused into a single walk, so
|
|
680
|
+
* every level is optional and every position, primitive leaf, opaque
|
|
681
|
+
* leaf or container, admits `| Unset`.
|
|
682
|
+
*
|
|
683
|
+
* The fusion is deliberate and should not be re-split into
|
|
684
|
+
* `DeepPartial<DefaultValuesShape<F>>`. Both passes have identical
|
|
685
|
+
* topology (object to mapped, tuple to positional, array to recurse,
|
|
686
|
+
* primitive to terminal), and walking twice exhausts the depth budget
|
|
687
|
+
* at call sites wiring several complex forms into one scope. One walk
|
|
688
|
+
* also keeps opaque leaves (`Date`, `Map`, `Set`, `RegExp`, functions)
|
|
689
|
+
* intact when their containing property is optional, where
|
|
690
|
+
* `DeepPartial`'s pass would structurally destructure them.
|
|
691
|
+
*
|
|
692
|
+
* ```ts
|
|
693
|
+
* type T = DefaultValuesInput<{
|
|
694
|
+
* email: string
|
|
695
|
+
* joinedAt: Date
|
|
696
|
+
* profile: { name: string; age: number }
|
|
697
|
+
* }>
|
|
698
|
+
* // → {
|
|
699
|
+
* // email?: string | Unset
|
|
700
|
+
* // joinedAt?: Date | Unset
|
|
701
|
+
* // profile?: { name?: string | Unset; age?: number | Unset } | Unset
|
|
702
|
+
* // } | Unset
|
|
703
|
+
* ```
|
|
704
|
+
*/
|
|
705
|
+
type DefaultValuesInput<T> = 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 extends null | undefined ? T : T extends Date | RegExp | Map<unknown, unknown> | Set<unknown> | ((...args: never) => unknown) ? T | Unset : T extends readonly [unknown, ...unknown[]] ? {
|
|
706
|
+
-readonly [K in keyof T]?: DefaultValuesInput<T[K]>;
|
|
707
|
+
} | Unset : T extends ReadonlyArray<infer U> ? IsTuple<T> extends true ? {
|
|
708
|
+
-readonly [K in keyof T]?: DefaultValuesInput<T[K]>;
|
|
709
|
+
} | Unset : Array<DefaultValuesInput<U>> | Unset : T extends object ? {
|
|
710
|
+
[K in keyof T]?: DefaultValuesInput<T[K]>;
|
|
711
|
+
} | Unset : T;
|
|
712
|
+
/**
|
|
713
|
+
* The type accepted at a `useForm` overload's `defaultValues` slot.
|
|
714
|
+
* `Form` is the schema's input projection; `SchemaInput` is the
|
|
715
|
+
* schema's own `z.input<Schema>`. A value is one of:
|
|
716
|
+
*
|
|
717
|
+
* - `DefaultValuesInput<Form>`, the partial, `Unset`-widened
|
|
718
|
+
* in-progress shape. `defaultValues` deliberately accepts a form
|
|
719
|
+
* mid-completion: a leaf may be blank or hold a not-yet-valid value,
|
|
720
|
+
* so a `z.email()` field accepts any `string`. Sharp, fully
|
|
721
|
+
* validated types are produced only at `handleSubmit`.
|
|
722
|
+
* - `SchemaInput`, the schema's full input shape.
|
|
723
|
+
* - a sync or async factory returning either.
|
|
724
|
+
*
|
|
725
|
+
* The `SchemaInput` arm is redundant at a concrete call site, since a
|
|
726
|
+
* schema's input is always assignable to its own `DefaultValuesInput`,
|
|
727
|
+
* and it does not weaken per-field checking (`number` is still rejected
|
|
728
|
+
* where `string` is expected). It earns its place in the generic
|
|
729
|
+
* form-wrapper case (#422): when the schema is a free type parameter
|
|
730
|
+
* `S`, a forwarded `z.input<S>` is REFLEXIVELY assignable to that arm,
|
|
731
|
+
* a relation TypeScript can decide even under a generic, whereas
|
|
732
|
+
* `DefaultValuesInput<Form>` alone stays a deferred conditional cascade
|
|
733
|
+
* that is not provably assignable and trips TS2589 / TS2769. No runtime
|
|
734
|
+
* effect; the slot only governs what the type checker accepts.
|
|
735
|
+
*
|
|
736
|
+
* `DefaultValuesInput<Form>` is computed ONCE, as the `DVI` argument to
|
|
737
|
+
* `AcceptableDefaultsOf`, rather than inlined in each arm, which would
|
|
738
|
+
* re-instantiate the deep cascade three times per overload and, across
|
|
739
|
+
* the unified entry's two overloads, tip the bundled `.d.ts` into
|
|
740
|
+
* TS2589. The arms stay a DIRECT union rather than a conditional over
|
|
741
|
+
* the cascade, so the `SchemaInput` arm remains reflexively matchable
|
|
742
|
+
* under a generic.
|
|
743
|
+
*/
|
|
744
|
+
type AcceptableDefaults<Form, SchemaInput> = AcceptableDefaultsOf<DefaultValuesInput<Form>, SchemaInput>;
|
|
745
|
+
type AcceptableDefaultsOf<DVI, SchemaInput> = DVI | SchemaInput | (() => DVI | SchemaInput) | (() => Promise<DVI | SchemaInput>);
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Identifier for a form: the string passed to `useForm({ key })`, used
|
|
749
|
+
* to look a form up by name from a distant component and to label
|
|
750
|
+
* errors and DevTools entries. An anonymous `useForm` allocates one
|
|
751
|
+
* automatically, so pick one only when the form needs stable identity.
|
|
752
|
+
*/
|
|
753
|
+
type FormKey = string;
|
|
754
|
+
/** Per-form options threaded from `useForm` into the adapter factory. */
|
|
755
|
+
interface SchemaFactoryOptions {
|
|
756
|
+
/** Recursion ceiling for walks through recursive schemas. */
|
|
757
|
+
maxRecursionDepth: number;
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* A JSON-serialisable value: the recursive shape of anything that
|
|
761
|
+
* survives a `JSON.stringify` / `JSON.parse` round-trip unchanged. The
|
|
762
|
+
* type of `ValidationError.data`.
|
|
763
|
+
*
|
|
764
|
+
* The object arm is a named interface rather than an inline index
|
|
765
|
+
* signature because TypeScript expands an anonymous recursive alias
|
|
766
|
+
* eagerly and hits TS2589 once `Json` is checked inside a large
|
|
767
|
+
* structural type such as the form store. A named reference defers
|
|
768
|
+
* that expansion.
|
|
769
|
+
*/
|
|
770
|
+
type Json = string | number | boolean | null | JsonArray | JsonObject;
|
|
771
|
+
interface JsonArray extends Array<Json> {
|
|
772
|
+
}
|
|
773
|
+
interface JsonObject {
|
|
774
|
+
[key: string]: Json;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* One validation failure, as returned by `validate()`, `parse()` and
|
|
778
|
+
* `handleSubmit`'s `onError`, and accepted leniently as `ErrorInput` by
|
|
779
|
+
* `form.setErrors`.
|
|
780
|
+
*
|
|
781
|
+
* Which form produced an error is envelope-level identity: the
|
|
782
|
+
* `ValidationResponse` or submit result carrying the list stamps
|
|
783
|
+
* `formKey` once, and an aggregator merging lists across forms (the
|
|
784
|
+
* wizard) stamps its own envelope.
|
|
785
|
+
*/
|
|
786
|
+
type ValidationError = {
|
|
787
|
+
/** Human-readable message describing the failure. */
|
|
788
|
+
message: string;
|
|
789
|
+
/**
|
|
790
|
+
* Structured path of the offending field, such as
|
|
791
|
+
* `['user', 'address', 0, 'line1']`.
|
|
792
|
+
*
|
|
793
|
+
* The root path `[]` is the form-level bucket, the home for errors
|
|
794
|
+
* belonging to no specific field: root `.refine()` messages,
|
|
795
|
+
* `setErrors` entries with no path, hydration failures,
|
|
796
|
+
* server-emitted banners. The empty-STRING path `['']` is unrelated
|
|
797
|
+
* and means an ordinary field whose key is the empty string.
|
|
798
|
+
*/
|
|
799
|
+
path: (string | number)[];
|
|
800
|
+
/**
|
|
801
|
+
* Stable machine identifier for the failure, scoped by prefix:
|
|
802
|
+
*
|
|
803
|
+
* - `atta:` for Attaform's own codes, see `AttaformErrorCode`.
|
|
804
|
+
* - an adapter prefix such as `zod:`, forwarded from the underlying
|
|
805
|
+
* schema library's issue code where it has one.
|
|
806
|
+
* - anything else for consumer-defined codes (`api:duplicate-email`,
|
|
807
|
+
* `auth:expired-token`). Pick a prefix and stay consistent, so
|
|
808
|
+
* error renderers and tests branch on `code` rather than matching
|
|
809
|
+
* exact message strings.
|
|
810
|
+
*/
|
|
811
|
+
code: string;
|
|
812
|
+
/**
|
|
813
|
+
* Optional structured payload. Attaform never sets or reads this; it
|
|
814
|
+
* is a passthrough for whatever a server sends alongside the message
|
|
815
|
+
* (a captcha challenge, a lockout `unlocks_at`, an MFA step-up
|
|
816
|
+
* descriptor) so the UI can act on it. Survives serialise, hydrate,
|
|
817
|
+
* undo and redo unchanged.
|
|
818
|
+
*/
|
|
819
|
+
data?: Json | null;
|
|
820
|
+
};
|
|
821
|
+
/**
|
|
822
|
+
* One path's slot in the form's tagged error store. The two sources
|
|
823
|
+
* that can put an error at a path stay segregated: `schema` holds the
|
|
824
|
+
* validation pipeline's verdicts, `user` holds `setErrors` entries and
|
|
825
|
+
* their history and SSR replays. Reads merge schema, then derived
|
|
826
|
+
* blank, then user, and each writer replaces exactly its own side.
|
|
827
|
+
*
|
|
828
|
+
* Cells are immutable. Every write replaces the whole cell, or deletes
|
|
829
|
+
* the key when both sides are empty, so Vue's per-key Map tracking
|
|
830
|
+
* fires for a change on either side. An empty side is the shared frozen
|
|
831
|
+
* `NO_ERRORS` array, never a fresh allocation.
|
|
832
|
+
*
|
|
833
|
+
* @internal
|
|
834
|
+
*/
|
|
835
|
+
type ErrorCell = {
|
|
836
|
+
readonly schema: readonly ValidationError[];
|
|
837
|
+
readonly user: readonly ValidationError[];
|
|
838
|
+
};
|
|
839
|
+
/**
|
|
840
|
+
* The lenient input `form.setErrors` accepts: a real `Error`, or a
|
|
841
|
+
* partial `ValidationError` where every field is optional.
|
|
842
|
+
*
|
|
843
|
+
* - `message`: omitted or empty coerces to `"Unknown error"`.
|
|
844
|
+
* - `path`: defaults to `[]`, a form-level error. Ignored by the
|
|
845
|
+
* path-scoped `setErrors(path, ...)` form, which stamps its own.
|
|
846
|
+
* - `code`: defaults to `atta:user-error`.
|
|
847
|
+
* - `data`: forwarded verbatim onto the produced `ValidationError`.
|
|
848
|
+
*
|
|
849
|
+
* Because every field is optional, `ValidationError` is a subtype of
|
|
850
|
+
* `ErrorInput`: a `ValidationError[]` read back out, or a server
|
|
851
|
+
* response already emitting the shape, pipes straight into
|
|
852
|
+
* `form.setErrors` with no adapter.
|
|
853
|
+
*/
|
|
854
|
+
type ErrorInput = Error | {
|
|
855
|
+
message?: string;
|
|
856
|
+
path?: (string | number)[];
|
|
857
|
+
code?: string;
|
|
858
|
+
data?: Json | null;
|
|
859
|
+
};
|
|
860
|
+
/**
|
|
861
|
+
* A parse verdict as an `AbstractSchema` produces it. Adapter authors
|
|
862
|
+
* implement THESE.
|
|
863
|
+
*
|
|
864
|
+
* A schema knows nothing about which form asked, being a pure function
|
|
865
|
+
* of the schema it wraps, which is what lets one `AbstractSchema` serve
|
|
866
|
+
* every form built on the same schema. The owning store stamps its own
|
|
867
|
+
* `formKey` on the way out, producing the `ValidationResponse` arms
|
|
868
|
+
* below.
|
|
869
|
+
*/
|
|
870
|
+
type SchemaParseSuccess<TData> = {
|
|
871
|
+
/** The parsed value at the validated subtree, or the whole form when `validate()` took no path. */
|
|
872
|
+
data: TData;
|
|
873
|
+
errors: undefined;
|
|
874
|
+
success: true;
|
|
875
|
+
};
|
|
876
|
+
/** Schema-level verdict when no data could be produced, such as a top-level type mismatch. */
|
|
877
|
+
type SchemaParseErrorWithoutData = {
|
|
878
|
+
data: undefined;
|
|
879
|
+
/** Non-empty list of failures. */
|
|
880
|
+
errors: ValidationError[];
|
|
881
|
+
success: false;
|
|
882
|
+
};
|
|
883
|
+
/** Schema-level verdict when the parser produced partial data alongside failures. */
|
|
884
|
+
type SchemaParseErrorWithData<TData> = {
|
|
885
|
+
data: TData;
|
|
886
|
+
errors: ValidationError[];
|
|
887
|
+
success: false;
|
|
888
|
+
};
|
|
889
|
+
/** Settled schema-level verdict. Discriminate on `success`. */
|
|
890
|
+
type SchemaParseResult<TData> = SchemaParseSuccess<TData> | SchemaParseErrorWithData<TData> | SchemaParseErrorWithoutData;
|
|
891
|
+
/** Settled validation result when the form or subtree parsed successfully. */
|
|
892
|
+
type ValidationResponseSuccess<TData> = SchemaParseSuccess<TData> & {
|
|
893
|
+
/** The form this verdict belongs to. Stamped by the store, not the schema. */
|
|
894
|
+
formKey: FormKey;
|
|
895
|
+
};
|
|
896
|
+
/** Settled validation result when no data could be produced. */
|
|
897
|
+
type ValidationResponseErrorWithoutData = SchemaParseErrorWithoutData & {
|
|
898
|
+
formKey: FormKey;
|
|
899
|
+
};
|
|
900
|
+
/** Settled validation result when the parser produced partial data alongside failures. */
|
|
901
|
+
type ValidationResponseErrorWithData<TData> = SchemaParseErrorWithData<TData> & {
|
|
902
|
+
formKey: FormKey;
|
|
903
|
+
};
|
|
904
|
+
/**
|
|
905
|
+
* Settled validation result. Discriminate on `success`:
|
|
906
|
+
*
|
|
907
|
+
* ```ts
|
|
908
|
+
* if (result.success) {
|
|
909
|
+
* // result.data is the parsed value, errors is undefined
|
|
910
|
+
* } else {
|
|
911
|
+
* // result.errors is non-empty, data may or may not be set
|
|
912
|
+
* }
|
|
913
|
+
* ```
|
|
914
|
+
*/
|
|
915
|
+
type ValidationResponse<TData> = ValidationResponseSuccess<TData> | ValidationResponseErrorWithData<TData> | ValidationResponseErrorWithoutData;
|
|
916
|
+
/**
|
|
917
|
+
* `ValidationResponse` without the `data` payload, for the reactive
|
|
918
|
+
* `validate()` status, whose consumers need only the success flag and
|
|
919
|
+
* the error list.
|
|
920
|
+
*/
|
|
921
|
+
type ValidationResponseWithoutValue<Form> = Omit<ValidationResponse<Form>, 'data'>;
|
|
922
|
+
/** Options bag for `form.parse`. */
|
|
923
|
+
type ParseOptions = {
|
|
924
|
+
/**
|
|
925
|
+
* Turn the pure read into an authoritative run: the verdict is
|
|
926
|
+
* committed to the error store at the parsed scope, and in-flight
|
|
927
|
+
* per-field validation is cancelled first. Default `false`.
|
|
928
|
+
*/
|
|
929
|
+
readonly commit?: boolean;
|
|
930
|
+
};
|
|
931
|
+
/**
|
|
932
|
+
* Sync-or-async return shape for `AbstractSchema.validateAtPath`. The
|
|
933
|
+
* adapter returns the response inline when the schema and the caller's
|
|
934
|
+
* options permit synchronous validation, and a `Promise<T>` otherwise.
|
|
935
|
+
* Callers that do not care simply `await`, which works for both; the
|
|
936
|
+
* reshape pre-pass, which prevents flicker, branches on
|
|
937
|
+
* `instanceof Promise`.
|
|
938
|
+
*/
|
|
939
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
940
|
+
/** Options accepted by `AbstractSchema.validateAtPath`. */
|
|
941
|
+
type ValidateOptions = {
|
|
942
|
+
/**
|
|
943
|
+
* Ask the adapter to return the response inline where the schema
|
|
944
|
+
* permits synchronous validation. A preference, not a guarantee: a
|
|
945
|
+
* structurally async schema, meaning any verdict that resolves only
|
|
946
|
+
* through a Promise (async refinements, async transforms or pipes)
|
|
947
|
+
* in whichever library the adapter wraps, still falls back to a
|
|
948
|
+
* `Promise<T>`.
|
|
949
|
+
*
|
|
950
|
+
* Omitted or `false`, the adapter is free to use its async path.
|
|
951
|
+
* Every call site outside the reshape pre-pass leaves it so.
|
|
952
|
+
*/
|
|
953
|
+
sync?: boolean;
|
|
954
|
+
};
|
|
955
|
+
/**
|
|
956
|
+
* Configuration passed to `AbstractSchema.getDefaultValues`. Adapters
|
|
957
|
+
* receive `useDefaultSchemaValues`, which chooses between honoring
|
|
958
|
+
* `.default(x)` wrappers and using empty or falsy fallbacks, plus an
|
|
959
|
+
* optional `constraints` overlay merged into the derived defaults so
|
|
960
|
+
* the runtime can stamp user-supplied defaults at construction.
|
|
961
|
+
* Exported so adapter authors can co-implement the service contract.
|
|
962
|
+
*/
|
|
963
|
+
type GetDefaultValuesConfig<Form> = {
|
|
964
|
+
useDefaultSchemaValues: boolean;
|
|
965
|
+
constraints?: DeepPartial<WriteShape<Form>> | undefined;
|
|
966
|
+
};
|
|
967
|
+
/**
|
|
968
|
+
* The contract a schema adapter implements so the form runtime can
|
|
969
|
+
* read defaults, validate, and walk paths against any underlying
|
|
970
|
+
* schema library.
|
|
971
|
+
*
|
|
972
|
+
* Most consumers never touch this type directly, since the typed entry
|
|
973
|
+
* points (`attaform/zod`, `attaform/zod-v3`) wire an adapter
|
|
974
|
+
* automatically. Implement it only to add support for a new schema
|
|
975
|
+
* library.
|
|
976
|
+
*/
|
|
977
|
+
type AbstractSchema<Form, GetValueFormType> = {
|
|
978
|
+
/**
|
|
979
|
+
* The form's starting value: the schema's defaults, with
|
|
980
|
+
* `config.constraints` merged over them and any shape the merge broke
|
|
981
|
+
* repaired. Derivation only. Nothing is validated here, so no
|
|
982
|
+
* refinement, check or transform runs; `validateSync` is the verdict.
|
|
983
|
+
*/
|
|
984
|
+
getDefaultValues(config: GetDefaultValuesConfig<Form>): Form;
|
|
985
|
+
/**
|
|
986
|
+
* The schema's synchronous verdict on `data` as a whole form, as a
|
|
987
|
+
* list of failures: empty when the data passes.
|
|
988
|
+
*
|
|
989
|
+
* The runtime seeds the schema error channel with it whenever it
|
|
990
|
+
* establishes a baseline: on the starting value, the first time
|
|
991
|
+
* anything reads or writes the form's errors, and on `reset()`. A form
|
|
992
|
+
* starting from invalid defaults therefore reads invalid from its
|
|
993
|
+
* first render.
|
|
994
|
+
* A hydrating client never calls it, since the server's verdict
|
|
995
|
+
* arrives with the payload.
|
|
996
|
+
*
|
|
997
|
+
* Adapters MUST NOT throw: a refinement that throws, or a schema whose
|
|
998
|
+
* verdict resolves only through a Promise, returns `[]` and leaves the
|
|
999
|
+
* verdict to the async pass that `needsAsyncValidation` queues. An
|
|
1000
|
+
* adapter with no synchronous parse returns `[]` unconditionally.
|
|
1001
|
+
*/
|
|
1002
|
+
validateSync(data: unknown): ValidationError[];
|
|
1003
|
+
/**
|
|
1004
|
+
* The schema-prescribed default at `path`. The runtime uses it to
|
|
1005
|
+
* fill structural gaps, so every `setValue` write leaves the form
|
|
1006
|
+
* satisfying the slim schema, meaning objects, arrays and primitives
|
|
1007
|
+
* without refinement-level constraints.
|
|
1008
|
+
*
|
|
1009
|
+
* - Object property: the property's schema default.
|
|
1010
|
+
* - Array element: the element default. Positions past the array's
|
|
1011
|
+
* current length still resolve, since every position has the same
|
|
1012
|
+
* element type.
|
|
1013
|
+
* - Tuple position: the position's default, and `undefined` out of
|
|
1014
|
+
* range.
|
|
1015
|
+
* - Optional / Default / Nullable / Readonly / Catch / Pipe: the
|
|
1016
|
+
* inner default.
|
|
1017
|
+
* - Discriminated union: the first variant's default, matching
|
|
1018
|
+
* `validateAtPath`'s first-success rule.
|
|
1019
|
+
* - Leaf: the primitive default (`''`, `0`, `false`), or the
|
|
1020
|
+
* wrapper's `.default(x)` value when present.
|
|
1021
|
+
* - A path the schema does not declare: `undefined`.
|
|
1022
|
+
*
|
|
1023
|
+
* An adapter may return `undefined` for any path it cannot resolve;
|
|
1024
|
+
* callers read that as "do not fill" and keep the existing data.
|
|
1025
|
+
*/
|
|
1026
|
+
getDefaultAtPath(path: Path): unknown;
|
|
1027
|
+
/**
|
|
1028
|
+
* The schema's appropriate nullish value at `path`: the underlying
|
|
1029
|
+
* type's empty concrete, with `.default(x)` wrappers explicitly NOT
|
|
1030
|
+
* honoured. This is what makes `form.clear(path)` differ from
|
|
1031
|
+
* `reset`: it ignores declared defaults and produces `false`, `0`,
|
|
1032
|
+
* `''`, `[]` or a recursively empty object instead.
|
|
1033
|
+
*
|
|
1034
|
+
* Sub-path resolution mirrors `getDefaultAtPath`; only leaves differ.
|
|
1035
|
+
*
|
|
1036
|
+
* - Primitive leaf: the falsy concrete (`''`, `0`, `false`, `0n`,
|
|
1037
|
+
* `new Date(0)`).
|
|
1038
|
+
* - Array / Set / Record: empty.
|
|
1039
|
+
* - `Optional<T>`: `undefined`, the wrapper's absent marker.
|
|
1040
|
+
* - `Nullable<T>`: `null`, the wrapper's explicit empty.
|
|
1041
|
+
* - `Default<T>` / `Prefault<T>` / `Catch<T>`: the inner schema's
|
|
1042
|
+
* empty. The declared default is INTENTIONALLY skipped.
|
|
1043
|
+
* - `Readonly<T>` / `preprocess(fn, T)`: the inner schema's empty.
|
|
1044
|
+
* - Object: recursive, every property gets its own empty.
|
|
1045
|
+
* - Discriminated union: the first variant's recursive empty.
|
|
1046
|
+
* - A path the schema does not declare: `undefined`.
|
|
1047
|
+
*
|
|
1048
|
+
* An adapter may return `undefined` for any path it cannot resolve;
|
|
1049
|
+
* callers read that as "do not write" and leave storage unchanged.
|
|
1050
|
+
*/
|
|
1051
|
+
getEmptyValueAtPath(path: Path): unknown;
|
|
1052
|
+
/**
|
|
1053
|
+
* Whether `path` resolves to, or descends through, a schema-side
|
|
1054
|
+
* normalizer that runs at parse rather than at the write boundary.
|
|
1055
|
+
* In Zod v4 that is `z.preprocess(fn, inner)` and `z.coerce.X()`,
|
|
1056
|
+
* which both desugar to `ZodPipe<ZodTransform, inner>`; in Zod v3 it
|
|
1057
|
+
* is `ZodEffects` with `_def.effect.type === 'preprocess'`.
|
|
1058
|
+
*
|
|
1059
|
+
* The slim-primitive write gate consults it. True at a path means the
|
|
1060
|
+
* gate takes the consumer's raw value verbatim and stops walking
|
|
1061
|
+
* children: storage holds the user's input, and the normalizer fires
|
|
1062
|
+
* during `safeParse`, not at `setValue` time.
|
|
1063
|
+
*
|
|
1064
|
+
* Prefix semantic: true if ANY ancestor of `path` resolves to such a
|
|
1065
|
+
* wrapper, so descendants of a preprocess-wrapped container
|
|
1066
|
+
* short-circuit the gate too. Adapters cache by canonical path key.
|
|
1067
|
+
*/
|
|
1068
|
+
isPreprocessOrCoerceLeaf(path: Path): boolean;
|
|
1069
|
+
/**
|
|
1070
|
+
* Whether `path` resolves to an opaque leaf: a schema that declares a
|
|
1071
|
+
* value without describing its shape. In Zod v4 that is `z.any()`,
|
|
1072
|
+
* `z.unknown()` and `z.custom(...)`, which is what both
|
|
1073
|
+
* `z.instanceof(X)` and `z.custom<T>()` compile to; in Zod v3 it is
|
|
1074
|
+
* `z.any()` and `z.unknown()`, with `z.instanceof(X)` peeling through
|
|
1075
|
+
* `ZodEffects` to `ZodAny`.
|
|
1076
|
+
*
|
|
1077
|
+
* The slim-primitive write gate consults it. An opaque leaf admits
|
|
1078
|
+
* every kind, containers included, and declares no sub-paths. Without
|
|
1079
|
+
* this the gate accepts an array at the leaf and then walks into it,
|
|
1080
|
+
* checking `files.0` against a schema that never declared that path;
|
|
1081
|
+
* the empty accept set there reads as "not in your schema" and no-ops
|
|
1082
|
+
* the whole write (#542).
|
|
1083
|
+
*
|
|
1084
|
+
* Exact-path semantic, unlike `isPreprocessOrCoerceLeaf`: it answers
|
|
1085
|
+
* for the node at `path` only. Descendants stay unwritable on their
|
|
1086
|
+
* own, because the schema genuinely does not declare them and
|
|
1087
|
+
* fabricating them would put phantom nodes on `form.fields`. Writing
|
|
1088
|
+
* the whole value at the opaque leaf is the supported move.
|
|
1089
|
+
*/
|
|
1090
|
+
isOpaqueLeafAtPath(path: Path): boolean;
|
|
1091
|
+
/**
|
|
1092
|
+
* Distinguish a tuple, fixed-length and position-typed, from an
|
|
1093
|
+
* unbounded array at `path`. The runtime calls this on every write
|
|
1094
|
+
* that descends into an array branch and treats the answer as
|
|
1095
|
+
* definitive.
|
|
1096
|
+
*
|
|
1097
|
+
* - `number`: a tuple of this structural length. The runtime pads the
|
|
1098
|
+
* consumer's value to that length and recurses position by
|
|
1099
|
+
* position.
|
|
1100
|
+
* - `null`: not a tuple. Covers the unbounded array, where the
|
|
1101
|
+
* runtime uses the consumer's length and reuses one element default
|
|
1102
|
+
* for every position, and also every path that does not resolve to
|
|
1103
|
+
* an array at all, where `getDefaultAtPath` yields no element
|
|
1104
|
+
* default either and the consumer's array passes through unchanged.
|
|
1105
|
+
*
|
|
1106
|
+
* Wrappers (optional, nullable, default, readonly, catch, pipe, lazy)
|
|
1107
|
+
* are peeled before the type check, so `optional(z.tuple([...]))`
|
|
1108
|
+
* reports its tuple length.
|
|
1109
|
+
*/
|
|
1110
|
+
arrayShapeAtPath(path: Path): number | null;
|
|
1111
|
+
/**
|
|
1112
|
+
* Whether the schema at `path` is a FIXED object, meaning a closed
|
|
1113
|
+
* set of declared keys (`z.object`), as opposed to an open or union
|
|
1114
|
+
* container (array, record, map, set, union, discriminated union)
|
|
1115
|
+
* whose element schema matches any segment.
|
|
1116
|
+
*
|
|
1117
|
+
* `form.fields` and `form.errors` use this to resolve a collision. A
|
|
1118
|
+
* fixed object's declared keys are known ahead of any data, so a key
|
|
1119
|
+
* the schema owns descends to a real terminal even before the live
|
|
1120
|
+
* value is populated, which keeps a declared-but-absent `optional`
|
|
1121
|
+
* field registrable. An open container cannot promise that: its
|
|
1122
|
+
* element schema accepts ANY segment, so the proxy falls back to the
|
|
1123
|
+
* keys the data currently holds and reads a genuinely absent key (an
|
|
1124
|
+
* out-of-bounds index, a missing record key, an inactive variant key)
|
|
1125
|
+
* as `undefined` rather than a phantom node.
|
|
1126
|
+
*
|
|
1127
|
+
* The empty path, the root form, is always a fixed object. Wrappers
|
|
1128
|
+
* are peeled before the kind check, so `z.object({...}).optional()`
|
|
1129
|
+
* still reports `true`. An undeclared path reports `false`.
|
|
1130
|
+
*/
|
|
1131
|
+
isFixedObjectAtPath(path: Path): boolean;
|
|
1132
|
+
/**
|
|
1133
|
+
* How the container at `path` spells the keys of its own entries:
|
|
1134
|
+
* `'number'` for an array or tuple, `'string'` for an object or
|
|
1135
|
+
* record, and for a `z.map` the kind its declared key type accepts.
|
|
1136
|
+
* `undefined` for anything else, a leaf and an undeclared path
|
|
1137
|
+
* included.
|
|
1138
|
+
*
|
|
1139
|
+
* The write walkers consult it at a `Map`, where a new entry must be
|
|
1140
|
+
* filed under a key of the declared type and the path segment alone
|
|
1141
|
+
* cannot say which: an integer-looking segment canonicalises to a
|
|
1142
|
+
* number, so `scores.42` against a `z.map(z.string(), V)` would
|
|
1143
|
+
* otherwise take the number `42` as its key and fail the map's own
|
|
1144
|
+
* parse. An entry the map already holds needs no ruling, since its
|
|
1145
|
+
* existing key wins.
|
|
1146
|
+
*
|
|
1147
|
+
* A map whose key type is neither string-ish nor number-ish, or which
|
|
1148
|
+
* admits both, reports `undefined`: no segment can spell such a key,
|
|
1149
|
+
* so the map stays a whole value with no addressable entries.
|
|
1150
|
+
*
|
|
1151
|
+
* Adapters MAY cache per path; the runtime calls this on the write
|
|
1152
|
+
* path.
|
|
1153
|
+
*/
|
|
1154
|
+
entryKeyKindAtPath(path: Path): 'string' | 'number' | undefined;
|
|
1155
|
+
/**
|
|
1156
|
+
* Every sub-schema that could resolve at `path`. Multiple results are
|
|
1157
|
+
* expected only for union branches, where the adapter cannot pick a
|
|
1158
|
+
* single winner until the data lands.
|
|
1159
|
+
*
|
|
1160
|
+
* `path` is the canonical `Segment[]`, walked segment by segment, so
|
|
1161
|
+
* a literal-dot key (`['user.name']`) never collides with the
|
|
1162
|
+
* sibling-pair form (`['user', 'name']`).
|
|
1163
|
+
*/
|
|
1164
|
+
getSchemasAtPath(path: Path): AbstractSchema<unknown, GetValueFormType>[];
|
|
1165
|
+
/**
|
|
1166
|
+
* Validate the subtree at `path`, or the whole form when `path` is
|
|
1167
|
+
* `undefined`. As with `getSchemasAtPath`, `path` is the canonical
|
|
1168
|
+
* `Segment[]` rather than a dotted string, so two schemas with
|
|
1169
|
+
* colliding dotted forms stay distinct at the adapter boundary.
|
|
1170
|
+
*
|
|
1171
|
+
* Adapters MUST NOT throw: an error comes back as a `success: false`
|
|
1172
|
+
* response with a populated `errors` array.
|
|
1173
|
+
*
|
|
1174
|
+
* The `MaybePromise` return has three cases:
|
|
1175
|
+
* - `options.sync === true` and a sync-capable schema: the adapter
|
|
1176
|
+
* SHOULD return the response inline. That lets the runtime batch
|
|
1177
|
+
* error writes with a coincident value mutation into one Vue
|
|
1178
|
+
* reactive flush, which is what prevents the `{}` flicker
|
|
1179
|
+
* observable during a discriminated-union variant reshape.
|
|
1180
|
+
* - `options.sync === true` and an async-only schema, meaning any
|
|
1181
|
+
* verdict that resolves only through a Promise: the adapter MUST
|
|
1182
|
+
* fall back to `Promise<T>`. The flag is a preference; sync is not
|
|
1183
|
+
* always achievable.
|
|
1184
|
+
* - `options.sync` omitted or `false`: the adapter SHOULD return
|
|
1185
|
+
* `Promise<T>`.
|
|
1186
|
+
*
|
|
1187
|
+
* Callers that do not care simply `await`, which works for both arms.
|
|
1188
|
+
* A caller that must tell them apart branches on `instanceof Promise`.
|
|
1189
|
+
*/
|
|
1190
|
+
validateAtPath(data: unknown, path: Path | undefined, options?: ValidateOptions): MaybePromise<SchemaParseResult<GetValueFormType>>;
|
|
1191
|
+
/**
|
|
1192
|
+
* The set of primitive `typeof`-style kinds the leaf schema at `path`
|
|
1193
|
+
* accepts at write time. The sync sister to `getSchemasAtPath` and
|
|
1194
|
+
* `validateAtPath`, used by `setValueAtPath` to gate writes without
|
|
1195
|
+
* round-tripping through async validation.
|
|
1196
|
+
*
|
|
1197
|
+
* Wrappers (optional, nullable, default, refinement, transform, pipe,
|
|
1198
|
+
* readonly, catch, lazy) are peeled. Refinement-level constraints,
|
|
1199
|
+
* meaning format checks such as email or uuid, min and max length,
|
|
1200
|
+
* enum membership, literal equality and regex, are IGNORED here:
|
|
1201
|
+
* those are a validation-time concern.
|
|
1202
|
+
*
|
|
1203
|
+
* The returned set unions across union branches and intersects across
|
|
1204
|
+
* intersection sides. Conventions:
|
|
1205
|
+
*
|
|
1206
|
+
* - Empty set: no kind admitted, so the gate rejects every write to
|
|
1207
|
+
* the path. Surfaces for `never`-typed schemas and for paths that
|
|
1208
|
+
* do not resolve in the schema at all, such as a typo.
|
|
1209
|
+
* - Permissive set, every kind: unknown or unconstrained, so the gate
|
|
1210
|
+
* accepts any value. Surfaces for `any`, `unknown` and `void`, and
|
|
1211
|
+
* where a lazy peel failed and the adapter cannot introspect.
|
|
1212
|
+
* - String-valued enums return `{'string'}`; numeric enums
|
|
1213
|
+
* `{'number'}`; literal types `{primitiveKindOf(literalValue)}`.
|
|
1214
|
+
* - Object and array containers return `{'object'}` / `{'array'}`;
|
|
1215
|
+
* the runtime walker recurses into entries at write time.
|
|
1216
|
+
* - Nullable and optional wrappers add `'null'` / `'undefined'` to
|
|
1217
|
+
* the inner set.
|
|
1218
|
+
*/
|
|
1219
|
+
getSlimPrimitiveTypesAtPath(path: Path): ReadonlySet<SlimPrimitiveKind>;
|
|
1220
|
+
/**
|
|
1221
|
+
* Whether `path` resolves to a LEAF, meaning a path the schema
|
|
1222
|
+
* declares no sub-paths under. `form.values`, `form.errors` and
|
|
1223
|
+
* `form.fields` query this at every step to choose between
|
|
1224
|
+
* descending into a sub-proxy and terminating with a leaf value.
|
|
1225
|
+
*
|
|
1226
|
+
* That branching is what keeps reserved leaf-prop names (`dirty`,
|
|
1227
|
+
* `errors`, `valid`) from shadowing schema keys: they inject only at
|
|
1228
|
+
* the FieldState terminal, not at every depth, so a schema field
|
|
1229
|
+
* literally named `dirty` at depth 2 or deeper stays reachable in its
|
|
1230
|
+
* own right.
|
|
1231
|
+
*
|
|
1232
|
+
* - Object / Array / Map / Set, under any wrapper: `false`, a
|
|
1233
|
+
* container, so descend further.
|
|
1234
|
+
* - Primitive (string, number, boolean, bigint, symbol, null,
|
|
1235
|
+
* undefined, date, function): `true`. `date` and `function` are
|
|
1236
|
+
* leaves because their values are opaque; do not drill into them.
|
|
1237
|
+
* - Opaque leaf (`z.any()`, `z.unknown()`, `z.custom()`, and the
|
|
1238
|
+
* `z.instanceof(X)` that compiles to it): `true`, and this answers
|
|
1239
|
+
* AHEAD of the kind test. An opaque leaf admits every kind
|
|
1240
|
+
* including the container ones, so classifying it off the slim
|
|
1241
|
+
* primitive set alone would call it a container and hang phantom
|
|
1242
|
+
* nodes under it on `form.fields`. See `isOpaqueLeafAtPath`.
|
|
1243
|
+
* Leafness asks whether the schema declares sub-paths here, and an
|
|
1244
|
+
* opaque leaf declares none.
|
|
1245
|
+
* - Optional / Nullable / Default / Catch: transparent. They add
|
|
1246
|
+
* `'null'` / `'undefined'` to the inner kind set without changing
|
|
1247
|
+
* the classification.
|
|
1248
|
+
* - Discriminated union root: `false`, since the variants are
|
|
1249
|
+
* objects and the kind set holds `'object'`.
|
|
1250
|
+
* - A union's discriminator key: `true`, since the literal type
|
|
1251
|
+
* resolves to `{'string'}` or `{'number'}`.
|
|
1252
|
+
* - A variant-only key: `true` if it resolves to a primitive in any
|
|
1253
|
+
* variant. Schema-static, so it does NOT read live storage to
|
|
1254
|
+
* decide which variant is active.
|
|
1255
|
+
* - Empty path, the root: `false`, the form-as-object.
|
|
1256
|
+
* - A path the schema does not declare: `false`. The proxy descends
|
|
1257
|
+
* permissively and leaf-prop reads at the unknown path return
|
|
1258
|
+
* `undefined` from the store. Treating unknown paths as containers
|
|
1259
|
+
* preserves the schema's authority and keeps a typo from
|
|
1260
|
+
* re-introducing shadowing.
|
|
1261
|
+
*
|
|
1262
|
+
* Adapters MAY cache per path: this is called on every proxy `get`
|
|
1263
|
+
* trap hit. The reference implementation memoises a
|
|
1264
|
+
* `Map<PathKey, boolean>` keyed by `canonicalizePath(path).key`, with
|
|
1265
|
+
* a lifetime tied to the adapter, one per `useForm()` call.
|
|
1266
|
+
*/
|
|
1267
|
+
isLeafAtPath(path: Path): boolean;
|
|
1268
|
+
/**
|
|
1269
|
+
* Whether the leaf at `path` is required, meaning the schema does NOT
|
|
1270
|
+
* admit empty through `.optional()`, `.nullable()`, `.default(N)` or
|
|
1271
|
+
* `.catch(N)` at the leaf or any wrapper.
|
|
1272
|
+
*
|
|
1273
|
+
* The submit and validate path uses it to raise "No value supplied"
|
|
1274
|
+
* when a field is in the form's `blankPaths` set, because the user
|
|
1275
|
+
* cleared it or never answered, AND the schema treats it as required.
|
|
1276
|
+
* Without it a strict numeric leaf silently accepts its slim default
|
|
1277
|
+
* of `0` for an unanswered field, so an unanswered income reads as
|
|
1278
|
+
* zero and passes validation.
|
|
1279
|
+
*
|
|
1280
|
+
* - Optional / Nullable / Default / Catch at any wrapper layer, root
|
|
1281
|
+
* or nested: `false`. The schema author opted into accepting empty.
|
|
1282
|
+
* - Readonly / Pipe / Lazy: transparent, peel and re-check the inner
|
|
1283
|
+
* schema.
|
|
1284
|
+
* - Union or discriminated union: `false` if ANY branch admits empty,
|
|
1285
|
+
* since a union accepts what its most permissive branch accepts.
|
|
1286
|
+
* This matches `validateAtPath`'s first-success rule.
|
|
1287
|
+
* - Intersection: `true` if EITHER side requires the path, since an
|
|
1288
|
+
* intersection needs both sides to accept, so one side rejecting
|
|
1289
|
+
* empty makes the whole rejection.
|
|
1290
|
+
* - A path the schema does not declare: `false`.
|
|
1291
|
+
* - Empty path, the root: `true`, always required as an object.
|
|
1292
|
+
*
|
|
1293
|
+
* Refinement-level constraints (length, format, custom predicates)
|
|
1294
|
+
* are not consulted. Those run at parse time inside `validateAtPath`
|
|
1295
|
+
* and surface as schema errors regardless. This answers only whether
|
|
1296
|
+
* the leaf is required at all; refinements layer on top.
|
|
1297
|
+
*/
|
|
1298
|
+
isRequiredAtPath(path: Path): boolean;
|
|
1299
|
+
/**
|
|
1300
|
+
* If the schema at `path` is, or wraps, a discriminated union, its
|
|
1301
|
+
* discriminator key plus a `getVariantDefault(value)` lookup;
|
|
1302
|
+
* otherwise `undefined`. Wrappers are peeled transparently.
|
|
1303
|
+
*
|
|
1304
|
+
* Two reshapes share this lookup:
|
|
1305
|
+
*
|
|
1306
|
+
* 1. Discriminator-key write. The runtime calls it with the PARENT
|
|
1307
|
+
* path. If `discriminatorKey` matches the path's last segment,
|
|
1308
|
+
* the write changes which variant is active, so parent storage
|
|
1309
|
+
* is replaced with the matching variant's slim default and the
|
|
1310
|
+
* old variant's keys (an `address` left over after switching to
|
|
1311
|
+
* `sms`) cannot leak.
|
|
1312
|
+
* 2. Whole-union write. The runtime calls it with the path itself.
|
|
1313
|
+
* If the info exists and the consumer's value carries the
|
|
1314
|
+
* discriminator key, the merge uses that variant's default
|
|
1315
|
+
* instead of the first-variant fallback `getDefaultAtPath`
|
|
1316
|
+
* returns for a union.
|
|
1317
|
+
*
|
|
1318
|
+
* An adapter that does not model discriminated unions can return
|
|
1319
|
+
* `undefined` unconditionally; the reshape is then a no-op.
|
|
1320
|
+
*/
|
|
1321
|
+
getUnionDiscriminatorAtPath(path: Path): UnionDiscriminatorContext | undefined;
|
|
1322
|
+
/**
|
|
1323
|
+
* Resolved field metadata for the schema node at `path`: label,
|
|
1324
|
+
* description and placeholder, plus the full registered payload as
|
|
1325
|
+
* `meta` for consumer-augmented keys. Reads through the shared
|
|
1326
|
+
* cross-adapter field-meta store, with one-way fallbacks:
|
|
1327
|
+
*
|
|
1328
|
+
* - `label`: registry payload, else `humanize(lastSegment)`
|
|
1329
|
+
* - `description`: registry payload, else `schema.description`
|
|
1330
|
+
* (the `.describe()` value), else `undefined`
|
|
1331
|
+
* - `placeholder`: registry payload, else `undefined`
|
|
1332
|
+
* - `meta`: registry payload, frozen; an empty object when
|
|
1333
|
+
* nothing was registered
|
|
1334
|
+
*
|
|
1335
|
+
* `path` is the canonical `Segment[]`, and the empty path resolves to
|
|
1336
|
+
* the root schema's metadata. Multiple candidates from union branches
|
|
1337
|
+
* resolve against the first match, following the same first-success
|
|
1338
|
+
* precedent as `getDefaultAtPath` and `validateAtPath`: register on
|
|
1339
|
+
* the union root for shared metadata, on individual branches for
|
|
1340
|
+
* variant-specific metadata.
|
|
1341
|
+
*
|
|
1342
|
+
* Optional. A missing implementation is treated as a stub returning
|
|
1343
|
+
* `EMPTY_RESOLVED_FIELD_META`, so consumers see the humanized `label`
|
|
1344
|
+
* fallback and `undefined` elsewhere.
|
|
1345
|
+
*/
|
|
1346
|
+
getFieldMetaAtPath?(path: Path): ResolvedFieldMeta;
|
|
1347
|
+
/**
|
|
1348
|
+
* Whether `validateAtPath` MAY have to run asynchronously to surface
|
|
1349
|
+
* every error this schema can produce. The runtime asks at
|
|
1350
|
+
* construction: `false` means the synchronous verdict on the starting
|
|
1351
|
+
* value is authoritative and no extra microtask is spent, `true` queues a
|
|
1352
|
+
* one-shot full-form async pass so async-only verdicts (refinements,
|
|
1353
|
+
* transforms or pipes that resolve only through a Promise) surface
|
|
1354
|
+
* without waiting for a user mutation.
|
|
1355
|
+
*
|
|
1356
|
+
* Optional and best-effort. A missing implementation is treated as
|
|
1357
|
+
* `() => false`, and async-only errors then fire on first user
|
|
1358
|
+
* mutation instead.
|
|
1359
|
+
*
|
|
1360
|
+
* For a per-path query, compose with `getSchemasAtPath(path)`: each
|
|
1361
|
+
* candidate sub-schema exposes its own `needsAsyncValidation`, so
|
|
1362
|
+
* "does the cargo subtree contain async work?" is the union of the
|
|
1363
|
+
* per-candidate answers, with no separate top-level overload. A root
|
|
1364
|
+
* that answers `false` answers for every path, and the runtime then
|
|
1365
|
+
* asks no sub-schema, so never answer `false` at the root of a tree
|
|
1366
|
+
* where a sub-schema would answer `true`.
|
|
1367
|
+
*/
|
|
1368
|
+
needsAsyncValidation?(): boolean;
|
|
1369
|
+
/**
|
|
1370
|
+
* Whether the schema carries a refine, check or transform at any
|
|
1371
|
+
* NON-LEAF position: a container node (object, array, tuple, union,
|
|
1372
|
+
* intersection, record, map, set) or the root itself. `false` means
|
|
1373
|
+
* every check is leaf-local, so a per-keystroke
|
|
1374
|
+
* `validateAtPath(form, leafPath)` catches the same verdicts as a
|
|
1375
|
+
* whole-form pass, because no ancestor refine reads the form's wider
|
|
1376
|
+
* state.
|
|
1377
|
+
*
|
|
1378
|
+
* The runtime uses it at the per-keystroke schedule to scope
|
|
1379
|
+
* field-level validation to the changed subtree where it can, and
|
|
1380
|
+
* falls back to a whole-form pass when an ancestor refine, such as a
|
|
1381
|
+
* cross-field equality or a sum constraint, could be moved by a leaf
|
|
1382
|
+
* write.
|
|
1383
|
+
*
|
|
1384
|
+
* Optional, and a missing implementation is treated as `() => true`,
|
|
1385
|
+
* the conservative whole-form answer. Detection is best-effort and
|
|
1386
|
+
* the two error directions are not symmetric: returning `true` where
|
|
1387
|
+
* no container refine exists only loses a perf win, while returning
|
|
1388
|
+
* `false` where one exists lets an ancestor verdict go stale. Bias
|
|
1389
|
+
* toward `true` when in doubt.
|
|
1390
|
+
*/
|
|
1391
|
+
hasContainerOrRootRefine?(): boolean;
|
|
1392
|
+
/**
|
|
1393
|
+
* Whether the schema tree holds at least one discriminated union at
|
|
1394
|
+
* any depth, inside arrays, tuples, records and lazy schemas
|
|
1395
|
+
* included. The store computes its per-form union capability flag
|
|
1396
|
+
* from this once at construction: `false` lets every write skip the
|
|
1397
|
+
* cross-variant ancestor guard and the variant-reshape dispatch, and
|
|
1398
|
+
* stops stub correction from running at all.
|
|
1399
|
+
*
|
|
1400
|
+
* Optional, and an adapter that omits it is treated as containing
|
|
1401
|
+
* unions, which keeps the conservative per-write probes on. Never
|
|
1402
|
+
* return `false` for a schema that DOES hold a discriminated union:
|
|
1403
|
+
* that disables variant reshape and stub correction for the form.
|
|
1404
|
+
*/
|
|
1405
|
+
hasDiscriminatedUnions?(): boolean;
|
|
1406
|
+
/**
|
|
1407
|
+
* Whether `isPreprocessOrCoerceLeaf` can answer `true` at any path:
|
|
1408
|
+
* the schema holds a preprocess-style normalizer or a coerce
|
|
1409
|
+
* primitive somewhere. The store reads it once at construction.
|
|
1410
|
+
* `false` lets it skip recording which paths the consumer authored,
|
|
1411
|
+
* a set that is only read at such leaves.
|
|
1412
|
+
*
|
|
1413
|
+
* Optional, and an adapter that omits it is treated as holding one,
|
|
1414
|
+
* which keeps that bookkeeping on. Never return `false` for a schema
|
|
1415
|
+
* where `isPreprocessOrCoerceLeaf` answers `true` anywhere: verdicts
|
|
1416
|
+
* at a path the consumer authored with `undefined` would then stay
|
|
1417
|
+
* hidden.
|
|
1418
|
+
*/
|
|
1419
|
+
hasPreprocessOrCoerce?(): boolean;
|
|
1420
|
+
};
|
|
1421
|
+
/**
|
|
1422
|
+
* A discriminated union as the adapter reports it: the discriminator
|
|
1423
|
+
* key plus a lookup from a discriminator literal to the matching
|
|
1424
|
+
* variant's slim default. Returned by
|
|
1425
|
+
* `AbstractSchema.getUnionDiscriminatorAtPath`.
|
|
1426
|
+
*/
|
|
1427
|
+
type UnionDiscriminatorContext = {
|
|
1428
|
+
/**
|
|
1429
|
+
* The property name whose literal value selects the variant, such as
|
|
1430
|
+
* `'channel'` for a union split on `{ channel: 'sms' | 'email' }`.
|
|
1431
|
+
*/
|
|
1432
|
+
readonly discriminatorKey: string;
|
|
1433
|
+
/**
|
|
1434
|
+
* Slim default for the variant whose discriminator literal equals
|
|
1435
|
+
* `value`, or `undefined` when no variant matches, in which case the
|
|
1436
|
+
* runtime skips the reshape and falls back to a plain write.
|
|
1437
|
+
*/
|
|
1438
|
+
getVariantDefault(value: unknown): unknown;
|
|
1439
|
+
/**
|
|
1440
|
+
* Whether `value` is a literal one of the discriminator's variants
|
|
1441
|
+
* recognises. Reshape uses it to choose between seeking a variant
|
|
1442
|
+
* default and emitting a stub state. NOT used at the write gate,
|
|
1443
|
+
* where consumer-side value validity is a validation-time concern.
|
|
1444
|
+
*/
|
|
1445
|
+
isVariantSelected(value: unknown): boolean;
|
|
1446
|
+
};
|
|
1447
|
+
/**
|
|
1448
|
+
* The primitive kinds the slim-primitive write contract recognises:
|
|
1449
|
+
* `typeof` plus a few well-known reference shapes (`Date`, `Array`,
|
|
1450
|
+
* `Map`, `Set`, plain `object`, `null`).
|
|
1451
|
+
*
|
|
1452
|
+
* The runtime gate's `slimKindOf(value)` returns one of these for a
|
|
1453
|
+
* value, and the adapter's `getSlimPrimitiveTypesAtPath(path)` returns
|
|
1454
|
+
* the set a leaf accepts. A write is gated by
|
|
1455
|
+
* `accepted.has(slimKindOf(value))`.
|
|
1456
|
+
*/
|
|
1457
|
+
type SlimPrimitiveKind = 'string' | 'number' | 'boolean' | 'bigint' | 'date' | 'null' | 'undefined' | 'object' | 'array' | 'symbol' | 'function' | 'map' | 'set' | 'file';
|
|
1458
|
+
/**
|
|
1459
|
+
* The "no result yet" status the reactive `validate()` ref holds while
|
|
1460
|
+
* a run is in flight. Narrow against `pending` to reach the settled
|
|
1461
|
+
* fields:
|
|
1462
|
+
*
|
|
1463
|
+
* ```ts
|
|
1464
|
+
* const status = form.validate()
|
|
1465
|
+
* watchEffect(() => {
|
|
1466
|
+
* if (status.value.pending) return
|
|
1467
|
+
* // status.value.success / status.value.errors are now safe to read
|
|
1468
|
+
* })
|
|
1469
|
+
* ```
|
|
1470
|
+
*/
|
|
1471
|
+
type PendingValidationStatus = {
|
|
1472
|
+
readonly pending: true;
|
|
1473
|
+
readonly errors: undefined;
|
|
1474
|
+
readonly success: false;
|
|
1475
|
+
readonly formKey: FormKey;
|
|
1476
|
+
};
|
|
1477
|
+
/** Settled status of a reactive `validate()` call. Mirrors the latest result. */
|
|
1478
|
+
type SettledValidationStatus<Form> = {
|
|
1479
|
+
readonly pending: false;
|
|
1480
|
+
} & ValidationResponseWithoutValue<Form>;
|
|
1481
|
+
/**
|
|
1482
|
+
* The value type of the ref `validate()` returns. Discriminate on
|
|
1483
|
+
* `pending` to switch between the in-flight and settled states.
|
|
1484
|
+
*/
|
|
1485
|
+
type ReactiveValidationStatus<Form> = PendingValidationStatus | SettledValidationStatus<Form>;
|
|
1486
|
+
/**
|
|
1487
|
+
* When per-field validation runs.
|
|
1488
|
+
*
|
|
1489
|
+
* - `'change'` (default): every committed write schedules a validation
|
|
1490
|
+
* for the affected path. At `debounceMs: 0`, also the default, the
|
|
1491
|
+
* run is synchronous in the write handler; a positive `debounceMs`
|
|
1492
|
+
* coalesces rapid bursts.
|
|
1493
|
+
* - `'blur'`: validate when the user tabs away from a registered
|
|
1494
|
+
* field. No debounce; `debounceMs` is a type error.
|
|
1495
|
+
* - `'submit'`: no live validation. `handleSubmit` and explicit
|
|
1496
|
+
* `validate()` / `parse()` calls are the only validation surfaces.
|
|
1497
|
+
* `debounceMs` is a type error.
|
|
1498
|
+
*
|
|
1499
|
+
* Only validation timing varies by mode. How often a value commits to
|
|
1500
|
+
* storage is the directive's concern: `<input v-register>` commits per
|
|
1501
|
+
* keystroke, `<input v-register.lazy>` defers to blur.
|
|
1502
|
+
*/
|
|
1503
|
+
type ValidateOn = 'change' | 'blur' | 'submit';
|
|
1504
|
+
/**
|
|
1505
|
+
* Validation timing: `validateOn` is the trigger and `debounceMs` the
|
|
1506
|
+
* wait after the last committed write. The union is what makes pairing
|
|
1507
|
+
* `debounceMs` with `'blur'` or `'submit'` a type error rather than a
|
|
1508
|
+
* silent runtime drop.
|
|
1509
|
+
*/
|
|
1510
|
+
type ValidateOnConfig = {
|
|
1511
|
+
/** Validation trigger. Default `'change'`. */
|
|
1512
|
+
validateOn?: 'change';
|
|
1513
|
+
/**
|
|
1514
|
+
* Milliseconds to wait after the last committed write before
|
|
1515
|
+
* running validation, to coalesce rapid bursts into one pass.
|
|
1516
|
+
* Useful for slow async adapters, or to smooth inline feedback
|
|
1517
|
+
* under heavy typing.
|
|
1518
|
+
*
|
|
1519
|
+
* Default `0`: validation runs synchronously after the write,
|
|
1520
|
+
* with no `setTimeout`. Schema work still rides
|
|
1521
|
+
* `Promise.resolve().then(validateAtPath)`, so even at `0` errors
|
|
1522
|
+
* land a microtask later rather than in the same statement.
|
|
1523
|
+
*
|
|
1524
|
+
* This debounces validation only. A value commits to
|
|
1525
|
+
* `form.values` immediately either way.
|
|
1526
|
+
*/
|
|
1527
|
+
debounceMs?: number;
|
|
1528
|
+
} | {
|
|
1529
|
+
/** Validation trigger. */
|
|
1530
|
+
validateOn: 'blur' | 'submit';
|
|
1531
|
+
/** `debounceMs` is not allowed with `'blur'` or `'submit'`. */
|
|
1532
|
+
debounceMs?: never;
|
|
1533
|
+
};
|
|
1534
|
+
/**
|
|
1535
|
+
* Per-write metadata, tagging a write so listeners and the write funnel
|
|
1536
|
+
* can treat it specially. Every field is internal; do not set any of
|
|
1537
|
+
* them from consumer code.
|
|
1538
|
+
*/
|
|
1539
|
+
type WriteMeta = {
|
|
1540
|
+
/**
|
|
1541
|
+
* Add the written path to the store's `blankPaths` set: storage holds
|
|
1542
|
+
* a real, schema-conformant value, the slim default, but the UI shows
|
|
1543
|
+
* the field as empty. The next write to that path WITHOUT this flag
|
|
1544
|
+
* removes it from the set, the user having typed something real.
|
|
1545
|
+
*
|
|
1546
|
+
* Set by `markBlank()` on the register binding, and by the `unset`
|
|
1547
|
+
* translation in `setValue`, `reset` and `useAbstractForm`
|
|
1548
|
+
* construction.
|
|
1549
|
+
*/
|
|
1550
|
+
readonly blank?: boolean;
|
|
1551
|
+
/**
|
|
1552
|
+
* Skip the discriminator-aware variant reshape inside
|
|
1553
|
+
* `setValueAtPath` for this write. Set by the reshape itself when it
|
|
1554
|
+
* re-enters with the new variant default, so the literal
|
|
1555
|
+
* discriminator inside that default cannot loop forever.
|
|
1556
|
+
*/
|
|
1557
|
+
readonly skipDiscriminatorReshape?: boolean;
|
|
1558
|
+
/**
|
|
1559
|
+
* Record an array structural mutation precisely enough to replay the
|
|
1560
|
+
* index permutation it produced. Set by the typed array helpers in
|
|
1561
|
+
* `array-engine.ts`, and used by `setValueAtPath` to surgically clear
|
|
1562
|
+
* variant memory for just the indices the operation invalidated.
|
|
1563
|
+
*
|
|
1564
|
+
* Without the hint, a raw whole-array `setValue(arrayPath, [...])`
|
|
1565
|
+
* clears all memory under the array, because the runtime cannot tell
|
|
1566
|
+
* which indices stayed put.
|
|
1567
|
+
*/
|
|
1568
|
+
readonly arrayOp?: {
|
|
1569
|
+
readonly kind: 'insert';
|
|
1570
|
+
readonly index: number;
|
|
1571
|
+
} | {
|
|
1572
|
+
readonly kind: 'remove';
|
|
1573
|
+
readonly index: number;
|
|
1574
|
+
} | {
|
|
1575
|
+
readonly kind: 'move';
|
|
1576
|
+
readonly from: number;
|
|
1577
|
+
readonly to: number;
|
|
1578
|
+
} | {
|
|
1579
|
+
readonly kind: 'swap';
|
|
1580
|
+
readonly a: number;
|
|
1581
|
+
readonly b: number;
|
|
1582
|
+
} | {
|
|
1583
|
+
readonly kind: 'replace-at';
|
|
1584
|
+
readonly index: number;
|
|
1585
|
+
};
|
|
1586
|
+
/**
|
|
1587
|
+
* Per-instance config threaded through writes so each
|
|
1588
|
+
* `useForm({ key })` call site honors its own `validateOn`,
|
|
1589
|
+
* `debounceMs` and `rememberVariants` even while sharing a store with
|
|
1590
|
+
* sibling calls, as a modal and a main form rendering the same
|
|
1591
|
+
* logical form do.
|
|
1592
|
+
*
|
|
1593
|
+
* Set by `buildFormApi` from the per-instance options bag; the store
|
|
1594
|
+
* reads each field with a fallback to its construction-time defaults.
|
|
1595
|
+
*/
|
|
1596
|
+
readonly instance?: {
|
|
1597
|
+
readonly validateOn?: ValidateOn;
|
|
1598
|
+
readonly debounceMs?: number;
|
|
1599
|
+
readonly rememberVariants?: boolean;
|
|
1600
|
+
};
|
|
1601
|
+
/**
|
|
1602
|
+
* Mark this `applyFormReplacement` call as a hydration step, meaning
|
|
1603
|
+
* the async-`defaultValues`, `activate()` or `rehydrate()` path.
|
|
1604
|
+
* Modules that snapshot form state, the history module in particular,
|
|
1605
|
+
* treat hydration as the baseline: stacks reset to a single seed of
|
|
1606
|
+
* the post-hydration value, so a later `undo()` cannot recover the
|
|
1607
|
+
* transient pre-hydration default.
|
|
1608
|
+
*
|
|
1609
|
+
* Set by the activate path in `create-form-store.ts`.
|
|
1610
|
+
*/
|
|
1611
|
+
readonly hydration?: boolean;
|
|
1612
|
+
};
|
|
1613
|
+
/**
|
|
1614
|
+
* The store slice the history runtime binds to. Structural on purpose:
|
|
1615
|
+
* `historyPlugin()` ships from the separate `attaform/history` entry,
|
|
1616
|
+
* so the runtime receives the store through this seam instead of
|
|
1617
|
+
* importing the store module, which would pull history internals onto
|
|
1618
|
+
* every form's eager path. A FormStore satisfies it as-is.
|
|
1619
|
+
*
|
|
1620
|
+
* @internal
|
|
1621
|
+
*/
|
|
1622
|
+
type HistoryKernel = {
|
|
1623
|
+
readonly form: Ref<unknown>;
|
|
1624
|
+
readonly blankPaths: Set<PathKey>;
|
|
1625
|
+
readonly errorCells: () => Map<PathKey, ErrorCell>;
|
|
1626
|
+
onFormChange(listener: (next: unknown, meta?: WriteMeta) => void): () => void;
|
|
1627
|
+
applyFormReplacement(next: unknown): void;
|
|
1628
|
+
restoreErrorCells(entries: ReadonlyArray<readonly [PathKey, ErrorCell]>): void;
|
|
1629
|
+
};
|
|
1630
|
+
/**
|
|
1631
|
+
* The live undo/redo runtime a {@link HistoryPlugin} attaches to one
|
|
1632
|
+
* form. Cached on the store so every `useForm` and `injectForm`
|
|
1633
|
+
* consumer of the same key shares one chain; `buildFormApi` adapts it
|
|
1634
|
+
* into the public `form.history` namespace.
|
|
1635
|
+
*
|
|
1636
|
+
* @internal
|
|
1637
|
+
*/
|
|
1638
|
+
type HistoryModule = {
|
|
1639
|
+
undo(): boolean;
|
|
1640
|
+
redo(): boolean;
|
|
1641
|
+
clear(): void;
|
|
1642
|
+
canUndo: Readonly<ComputedRef<boolean>>;
|
|
1643
|
+
canRedo: Readonly<ComputedRef<boolean>>;
|
|
1644
|
+
historySize: Readonly<ComputedRef<number>>;
|
|
1645
|
+
dispose(): void;
|
|
1646
|
+
};
|
|
1647
|
+
/**
|
|
1648
|
+
* Opt-in undo/redo, created by `historyPlugin()` from
|
|
1649
|
+
* `attaform/history` and passed to `useForm({ history })`:
|
|
1650
|
+
*
|
|
1651
|
+
* ```ts
|
|
1652
|
+
* import { historyPlugin } from 'attaform/history'
|
|
1653
|
+
*
|
|
1654
|
+
* const form = useForm({ schema, history: historyPlugin({ max: 200 }) })
|
|
1655
|
+
* ```
|
|
1656
|
+
*
|
|
1657
|
+
* Every mutation records a position, and `form.history.undo()` /
|
|
1658
|
+
* `form.history.redo()` walk the chain. `reset()` is itself a mutation,
|
|
1659
|
+
* so the pre-reset state stays one undo away. Hydration is the floor:
|
|
1660
|
+
* once it applies, the chain reseeds with the hydrated value and
|
|
1661
|
+
* `undo()` cannot reach the transient pre-hydration default.
|
|
1662
|
+
*
|
|
1663
|
+
* A plugin instance is a reusable configuration, not per-form state, so
|
|
1664
|
+
* passing one instance to several forms gives each its own independent
|
|
1665
|
+
* chain.
|
|
1666
|
+
*/
|
|
1667
|
+
type HistoryPlugin = {
|
|
1668
|
+
/**
|
|
1669
|
+
* Bind a fresh history runtime to one form's store. Called by
|
|
1670
|
+
* `useForm` when the store is first created.
|
|
1671
|
+
*
|
|
1672
|
+
* @internal
|
|
1673
|
+
*/
|
|
1674
|
+
readonly attach: (kernel: HistoryKernel) => HistoryModule;
|
|
1675
|
+
};
|
|
1676
|
+
/**
|
|
1677
|
+
* The undo/redo namespace at `form.history`, holding every
|
|
1678
|
+
* history-related method and reactive flag, so there is one address to
|
|
1679
|
+
* read from.
|
|
1680
|
+
*
|
|
1681
|
+
* Always present on a `useForm()` return whether or not `history` was
|
|
1682
|
+
* configured. Unconfigured, the methods are no-ops returning `false` or
|
|
1683
|
+
* `void`, `canUndo` and `canRedo` read `false`, and `size` reads `0`,
|
|
1684
|
+
* so a template needs no conditional.
|
|
1685
|
+
*
|
|
1686
|
+
* `canUndo`, `canRedo` and `size` read as plain values rather than
|
|
1687
|
+
* refs; no `.value`.
|
|
1688
|
+
*/
|
|
1689
|
+
type FormHistoryNamespace = {
|
|
1690
|
+
/**
|
|
1691
|
+
* Step back one position. `true` when a step was taken, `false` at
|
|
1692
|
+
* the oldest reachable position or when history is not configured.
|
|
1693
|
+
*/
|
|
1694
|
+
readonly undo: () => boolean;
|
|
1695
|
+
/**
|
|
1696
|
+
* Replay the next step forward. `true` on success, `false` when
|
|
1697
|
+
* nothing is queued or history is not configured. The forward branch
|
|
1698
|
+
* is dropped as soon as a new mutation lands.
|
|
1699
|
+
*/
|
|
1700
|
+
readonly redo: () => boolean;
|
|
1701
|
+
/**
|
|
1702
|
+
* Wipe the undo and redo branches and reseed the chain with the
|
|
1703
|
+
* current form state as the new baseline. Values, errors and
|
|
1704
|
+
* blankPaths all stay put; only past and future reset. Afterwards
|
|
1705
|
+
* `canUndo` and `canRedo` are `false` and `size` is `1`. A no-op when
|
|
1706
|
+
* history is not configured.
|
|
1707
|
+
*/
|
|
1708
|
+
readonly clear: () => void;
|
|
1709
|
+
/** `true` when at least one undo step is available. */
|
|
1710
|
+
readonly canUndo: boolean;
|
|
1711
|
+
/** `true` when `undo()` has been called and a `redo()` would replay. */
|
|
1712
|
+
readonly canRedo: boolean;
|
|
1713
|
+
/**
|
|
1714
|
+
* Total reachable positions: the current one plus everything
|
|
1715
|
+
* reachable by `undo()` and `redo()`. Useful for a debug overlay; UI
|
|
1716
|
+
* driving undo and redo buttons should gate on `canUndo` and
|
|
1717
|
+
* `canRedo`. Reads `0` when history is not configured.
|
|
1718
|
+
*/
|
|
1719
|
+
readonly size: number;
|
|
1720
|
+
};
|
|
1721
|
+
/**
|
|
1722
|
+
* Configuration passed to `useForm`. Only `schema` is required.
|
|
1723
|
+
*
|
|
1724
|
+
* ```ts
|
|
1725
|
+
* const form = useForm({
|
|
1726
|
+
* schema: signupSchema,
|
|
1727
|
+
* defaultValues: { email: '' },
|
|
1728
|
+
* validateOn: 'change',
|
|
1729
|
+
* debounceMs: 200,
|
|
1730
|
+
* })
|
|
1731
|
+
* ```
|
|
1732
|
+
*/
|
|
1733
|
+
type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema extends AbstractSchema<Form, GetValueFormType>, DefaultValues extends DefaultValuesInput<Form>, K extends FormKey = FormKey> = {
|
|
1734
|
+
/**
|
|
1735
|
+
* The schema describing the form's shape and validation rules. A
|
|
1736
|
+
* typed entry point such as `attaform/zod` takes the underlying
|
|
1737
|
+
* library's schema directly and wraps an adapter; the abstract entry
|
|
1738
|
+
* point takes any object implementing `AbstractSchema`.
|
|
1739
|
+
*
|
|
1740
|
+
* For a schema that depends on the form's identity or per-form
|
|
1741
|
+
* options, pass a factory `(key, options) => schema` instead, called
|
|
1742
|
+
* once per form. Most adapters ignore the options argument; the typed
|
|
1743
|
+
* Zod entry points use it to thread the recursion cap into the
|
|
1744
|
+
* adapter closure.
|
|
1745
|
+
*/
|
|
1746
|
+
schema: Schema | ((key: FormKey, options: SchemaFactoryOptions) => Schema);
|
|
1747
|
+
/**
|
|
1748
|
+
* Optional identifier for this form. Omit it for a one-off form and
|
|
1749
|
+
* Attaform allocates a unique key automatically, SSR-safe and stable
|
|
1750
|
+
* across hydration.
|
|
1751
|
+
*
|
|
1752
|
+
* Pass a string when the form needs identity:
|
|
1753
|
+
* - to look it up from a distant component via `injectForm(key)`;
|
|
1754
|
+
* - to share state across components, since several `useForm({ key })`
|
|
1755
|
+
* calls with the same key resolve to the same form;
|
|
1756
|
+
* - to give DevTools and validation errors a recognisable label.
|
|
1757
|
+
*
|
|
1758
|
+
* Keys starting with `__atta:` are reserved and throw
|
|
1759
|
+
* `ReservedFormKeyError`.
|
|
1760
|
+
*
|
|
1761
|
+
* A string literal is preserved on `form.key`, so `useWizard` and
|
|
1762
|
+
* other consumers can discriminate against the union of known keys at
|
|
1763
|
+
* compile time.
|
|
1764
|
+
*/
|
|
1765
|
+
key?: K;
|
|
1766
|
+
/**
|
|
1767
|
+
* Initial values applied over the schema's defaults. A field not
|
|
1768
|
+
* named here falls back to the schema default, or to the primitive
|
|
1769
|
+
* default for the slot's type.
|
|
1770
|
+
*
|
|
1771
|
+
* Values must satisfy the slim primitive type at each path (string,
|
|
1772
|
+
* number, boolean, Date) but do NOT have to satisfy refinement-level
|
|
1773
|
+
* constraints such as format checks, enum membership or length and
|
|
1774
|
+
* range bounds. A refinement-invalid default passes through and
|
|
1775
|
+
* surfaces as a field error, which is what lets you rehydrate stale
|
|
1776
|
+
* saved data without discarding the user's input.
|
|
1777
|
+
*
|
|
1778
|
+
* Accepts a plain value, a sync function, or an async function:
|
|
1779
|
+
*
|
|
1780
|
+
* ```ts
|
|
1781
|
+
* // Plain value: applies at construction.
|
|
1782
|
+
* defaultValues: { email: '' }
|
|
1783
|
+
*
|
|
1784
|
+
* // Sync function: invoked on a microtask after construction.
|
|
1785
|
+
* defaultValues: () => buildDraft()
|
|
1786
|
+
*
|
|
1787
|
+
* // Async function: the form starts with the schema's slim defaults
|
|
1788
|
+
* // and `form.hydrating` is true while the promise is in flight; on
|
|
1789
|
+
* // resolve the values apply and `hydrating` flips false. Under SSR
|
|
1790
|
+
* // the factory fires via `onServerPrefetch`, so the resolved payload
|
|
1791
|
+
* // bakes into hydration transfer state and the client never
|
|
1792
|
+
* // re-fetches.
|
|
1793
|
+
* defaultValues: async () => api.fetchDraft(userId)
|
|
1794
|
+
* ```
|
|
1795
|
+
*
|
|
1796
|
+
* An error thrown by a function-form factory surfaces on
|
|
1797
|
+
* `form.hydrateError` and leaves the form usable with slim defaults.
|
|
1798
|
+
* Call `form.rehydrate()` to re-fire the factory.
|
|
1799
|
+
*/
|
|
1800
|
+
defaultValues?: DefaultValues | (() => DefaultValues) | (() => Promise<DefaultValues>);
|
|
1801
|
+
/**
|
|
1802
|
+
* Move keyboard and screen-reader focus to the first errored field
|
|
1803
|
+
* when a submit attempt fails validation, after errors are populated
|
|
1804
|
+
* and before your `onError` callback runs. Default `true`: focusing
|
|
1805
|
+
* the broken field is an accessibility baseline, and browsers scroll
|
|
1806
|
+
* the focused element into view as part of the move.
|
|
1807
|
+
*
|
|
1808
|
+
* Pass `false` to drive the nudge yourself from `onError` with
|
|
1809
|
+
* `form.focusFirstError()` or `form.scrollToFirstError()`, both of
|
|
1810
|
+
* which stay available either way.
|
|
1811
|
+
*
|
|
1812
|
+
* The focus requests `focusVisible: true` so the browser paints a
|
|
1813
|
+
* focus ring despite the move being programmatic. On a UA that does
|
|
1814
|
+
* not support that hint, a non-text control (radio, checkbox, custom
|
|
1815
|
+
* widget) can end up focused with no visible ring after a
|
|
1816
|
+
* pointer-driven submit; pair this with your own indicator, such as a
|
|
1817
|
+
* `:focus-within` ring on the option wrapper, if you target those
|
|
1818
|
+
* browsers.
|
|
1819
|
+
*
|
|
1820
|
+
* If no errored field has a mounted, visible element, the focus
|
|
1821
|
+
* silently no-ops.
|
|
1822
|
+
*/
|
|
1823
|
+
focusOnInvalidSubmit?: boolean;
|
|
1824
|
+
/**
|
|
1825
|
+
* Freeze the form's data. While this resolves truthy every value
|
|
1826
|
+
* write no-ops at the store's write chokepoint, programmatic
|
|
1827
|
+
* `setValue`, the `v-register` directive and host-model events alike;
|
|
1828
|
+
* native registered inputs render the HTML `disabled` attribute;
|
|
1829
|
+
* component hosts receive a `disabled` flag on their register value;
|
|
1830
|
+
* and every field's `displayState` drops to `'idle'`, so error,
|
|
1831
|
+
* pending and success signals stand down. The first blocked write
|
|
1832
|
+
* logs a one-time dev warning. Writes never throw.
|
|
1833
|
+
*
|
|
1834
|
+
* `reset()` and `defaultValues` hydration still apply while frozen,
|
|
1835
|
+
* so a disabled form can be populated or cleared programmatically.
|
|
1836
|
+
* This is a data freeze only: the form stays navigable, which is what
|
|
1837
|
+
* a read-only review page wants.
|
|
1838
|
+
*
|
|
1839
|
+
* Accepts a boolean, ref, computed or getter, read live so the freeze
|
|
1840
|
+
* tracks a reactive source. `undefined` resolves to `false`.
|
|
1841
|
+
*
|
|
1842
|
+
* A shared store resolves `disabled` from its first
|
|
1843
|
+
* `useForm({ key })` call; a later caller passing a different value
|
|
1844
|
+
* is ignored.
|
|
1845
|
+
*/
|
|
1846
|
+
disabled?: MaybeRefOrGetter<boolean | undefined>;
|
|
1847
|
+
/**
|
|
1848
|
+
* When per-field validation runs. Default `'change'`. See
|
|
1849
|
+
* `ValidateOn` for mode semantics.
|
|
1850
|
+
*
|
|
1851
|
+
* This sets validation timing only. How often a value commits to
|
|
1852
|
+
* storage is the directive's concern: per keystroke for
|
|
1853
|
+
* `<input v-register>`, per blur for `<input v-register.lazy>`.
|
|
1854
|
+
*/
|
|
1855
|
+
validateOn?: ValidateOn;
|
|
1856
|
+
/**
|
|
1857
|
+
* Milliseconds to wait after the last committed write before running
|
|
1858
|
+
* validation, to coalesce rapid bursts. Default `0`: validation runs
|
|
1859
|
+
* synchronously after the write, with no `setTimeout`.
|
|
1860
|
+
*
|
|
1861
|
+
* Only accepted under `validateOn: 'change'`. Passing it with
|
|
1862
|
+
* `'blur'` or `'submit'` is a type error.
|
|
1863
|
+
*/
|
|
1864
|
+
debounceMs?: number;
|
|
1865
|
+
/**
|
|
1866
|
+
* Opt-in undo/redo, off by default. Pass `historyPlugin()` from
|
|
1867
|
+
* `attaform/history`, which caps at 128 positions, or
|
|
1868
|
+
* `historyPlugin({ max: N })` to tune it. See {@link HistoryPlugin}
|
|
1869
|
+
* for how the chain behaves, and `form.history` for the surface it
|
|
1870
|
+
* adds.
|
|
1871
|
+
*/
|
|
1872
|
+
history?: HistoryPlugin;
|
|
1873
|
+
/**
|
|
1874
|
+
* Whether to remember each discriminated-union variant's typed state
|
|
1875
|
+
* across switches. Default `true`.
|
|
1876
|
+
*
|
|
1877
|
+
* At `true`, switching `notify.channel` from `email`, holding
|
|
1878
|
+
* `address: 'foo@bar.com'`, to `sms` and back lands on
|
|
1879
|
+
* `address: 'foo@bar.com'` again: the runtime snapshots the outgoing
|
|
1880
|
+
* variant's subtree on switch-out and restores the incoming variant's
|
|
1881
|
+
* prior subtree on switch-in. Every union at every nesting depth is
|
|
1882
|
+
* memorized independently.
|
|
1883
|
+
*
|
|
1884
|
+
* At `false`, the outgoing variant's typed state is dropped on every
|
|
1885
|
+
* switch and the incoming variant initializes from its slim default.
|
|
1886
|
+
*
|
|
1887
|
+
* Memory is in-memory only and does not survive a fresh mount, so a
|
|
1888
|
+
* page reload starts every discriminator empty. `reset()` clears it;
|
|
1889
|
+
* `resetField(path)` clears any entry whose union path equals or sits
|
|
1890
|
+
* under `path`.
|
|
1891
|
+
*/
|
|
1892
|
+
rememberVariants?: boolean;
|
|
1893
|
+
/**
|
|
1894
|
+
* Schema-driven coercion of user-typed DOM values at the v-register
|
|
1895
|
+
* layer. Two rules ship, string to number and string to boolean, each
|
|
1896
|
+
* firing only where the schema declares that single type at the path.
|
|
1897
|
+
* Defaults to on; `false` disables coercion form-wide and the slim
|
|
1898
|
+
* gate rejects mismatches instead.
|
|
1899
|
+
*
|
|
1900
|
+
* Coercion applies ONLY to user-typed DOM values. A programmatic
|
|
1901
|
+
* write is never coerced.
|
|
1902
|
+
*/
|
|
1903
|
+
coerce?: boolean;
|
|
1904
|
+
/**
|
|
1905
|
+
* @internal
|
|
1906
|
+
* SSR prefetch mark, set by the `attaform/vite` compile-time
|
|
1907
|
+
* transform on a `useForm` call whose surrounding SFC template, or a
|
|
1908
|
+
* computed feeding it, reads the form's reactive state. The flag
|
|
1909
|
+
* enqueues the form on the registry's SSR prefetch queue, so an async
|
|
1910
|
+
* `defaultValues` factory runs inside `onServerPrefetch` and the
|
|
1911
|
+
* resolved payload bakes into the hydration transfer state.
|
|
1912
|
+
*
|
|
1913
|
+
* Consumers do not write this; `form.activate()` is the documented
|
|
1914
|
+
* escape hatch for where the transform's static analysis cannot see a
|
|
1915
|
+
* reference, such as cross-module sharing, dynamic property access or
|
|
1916
|
+
* a headless context.
|
|
1917
|
+
*/
|
|
1918
|
+
__ssrAccessed?: boolean;
|
|
1919
|
+
};
|
|
1920
|
+
/**
|
|
1921
|
+
* Callback invoked by `handleSubmit` after the form parses successfully.
|
|
1922
|
+
* It receives the strictly-typed parsed value, with refinements already
|
|
1923
|
+
* run, so enum, literal and format constraints are honoured.
|
|
1924
|
+
*/
|
|
1925
|
+
type OnSubmit<Form extends GenericForm> = (form: Form) => void | Promise<void>;
|
|
1926
|
+
/**
|
|
1927
|
+
* Callback invoked by `handleSubmit` on a failed submit. Fires when
|
|
1928
|
+
* client validation fails AND when the submit callback leaves errors in
|
|
1929
|
+
* the user-error layer (the `setErrors(...); return` server-rejection
|
|
1930
|
+
* pattern). Receives the full list of errors. Bind this when you want to
|
|
1931
|
+
* react to submit failures explicitly (alongside or instead of the
|
|
1932
|
+
* automatic `focusOnInvalidSubmit` nudge).
|
|
1933
|
+
*/
|
|
1934
|
+
type OnError = (error: ValidationError[]) => void | Promise<void>;
|
|
1935
|
+
/**
|
|
1936
|
+
* The display-state verdict at a path: the one signal a UI needs to
|
|
1937
|
+
* decide what, if anything, to surface about validation right now.
|
|
1938
|
+
* Rolled up at containers and at the form root
|
|
1939
|
+
* (`form.meta.displayState`).
|
|
1940
|
+
*
|
|
1941
|
+
* - `'idle'`: nothing to surface, either because the timing gate has
|
|
1942
|
+
* not opened yet or because it has and no verdict is worth showing.
|
|
1943
|
+
* - `'pending'`: a run is in flight at this path and the prior verdict
|
|
1944
|
+
* is stale. Drive a spinner or a "Checking..." affordance.
|
|
1945
|
+
* - `'error'`: a blocking error the timing gate has cleared for
|
|
1946
|
+
* display.
|
|
1947
|
+
* - `'success'`: validation passed and the gate has cleared a positive
|
|
1948
|
+
* confirmation, the green-check pattern.
|
|
1949
|
+
*
|
|
1950
|
+
* The four `show*` booleans on `FieldState` are sugar over this enum
|
|
1951
|
+
* (`showErrors === (displayState === 'error')`, and so on), so they can
|
|
1952
|
+
* never contradict it.
|
|
1953
|
+
*/
|
|
1954
|
+
type DisplayState = 'idle' | 'pending' | 'error' | 'success';
|
|
1955
|
+
/**
|
|
1956
|
+
* The `FieldState` keys layered on FROM the display-state predicate,
|
|
1957
|
+
* plus `firstError` and `firstOwnError`, computed alongside them.
|
|
1958
|
+
*
|
|
1959
|
+
* They are `Omit`'d from the predicate's own arguments so it cannot
|
|
1960
|
+
* read its own output and form a cycle. Enforced at the type level and
|
|
1961
|
+
* at runtime both: the base objects passed in literally lack these
|
|
1962
|
+
* keys, so neither an `as` cast nor a vanilla-JS caller can reach them.
|
|
1963
|
+
* `FieldStateBase` and `FormMetaBase` in `field-state-api.ts` omit the
|
|
1964
|
+
* same set in lockstep.
|
|
1965
|
+
*/
|
|
1966
|
+
type FieldStateDerivedKey = 'displayState' | 'showErrors' | 'showPending' | 'showSuccess' | 'showIdle' | 'firstError' | 'firstOwnError';
|
|
1967
|
+
/**
|
|
1968
|
+
* One step of the display state machine: the verdict the field should
|
|
1969
|
+
* render now, as `display`, projected to `displayState` and the `show*`
|
|
1970
|
+
* booleans, plus two optional timing cells the engine reads.
|
|
1971
|
+
*/
|
|
1972
|
+
type DisplayMachine = {
|
|
1973
|
+
readonly display: DisplayState;
|
|
1974
|
+
/**
|
|
1975
|
+
* An absolute `Date.now()` stamp meaning "re-evaluate this field no
|
|
1976
|
+
* later than here". The engine keeps one timer per form aimed at the
|
|
1977
|
+
* nearest `reviewAt` across all active fields; when it fires, the
|
|
1978
|
+
* dependent field computeds re-run and call the reducer again.
|
|
1979
|
+
*
|
|
1980
|
+
* A machine with no `reviewAt` and a non-pending `display` is
|
|
1981
|
+
* terminal, and the engine evicts it.
|
|
1982
|
+
*/
|
|
1983
|
+
readonly reviewAt?: number;
|
|
1984
|
+
/**
|
|
1985
|
+
* The stamp at which `'pending'` was first shown: the memory the
|
|
1986
|
+
* min-visible hold needs, so a spinner that has only just appeared is
|
|
1987
|
+
* not yanked away the instant validation resolves. Opaque to the
|
|
1988
|
+
* engine, and a custom reducer may carry extra memory fields of its
|
|
1989
|
+
* own alongside it.
|
|
1990
|
+
*/
|
|
1991
|
+
readonly pendingShownAt?: number;
|
|
1992
|
+
};
|
|
1993
|
+
/**
|
|
1994
|
+
* Inputs to the display reducer. `field` and `formMeta` are the
|
|
1995
|
+
* reactive snapshots it resolves against, minus the derived
|
|
1996
|
+
* `displayState`, `show*` and `firstError` keys, so the reducer can
|
|
1997
|
+
* never read its own output and form a cycle. See
|
|
1998
|
+
* `FieldStateDerivedKey`.
|
|
1999
|
+
*/
|
|
2000
|
+
type DisplayCtx = {
|
|
2001
|
+
readonly field: Omit<FieldState, FieldStateDerivedKey>;
|
|
2002
|
+
readonly formMeta: Omit<FormMeta, FieldStateDerivedKey>;
|
|
2003
|
+
/**
|
|
2004
|
+
* `Date.now()` at which the field's current validation streak opened,
|
|
2005
|
+
* or `null` when nothing is in flight. This, not `field.validating`,
|
|
2006
|
+
* is the timing anchor: the elapsed wait is `now - validatingSince`.
|
|
2007
|
+
* Pinned to the start of the streak, so overlapping sub-runs do not
|
|
2008
|
+
* reset it.
|
|
2009
|
+
*/
|
|
2010
|
+
readonly validatingSince: number | null;
|
|
2011
|
+
/**
|
|
2012
|
+
* The same anchor for an in-flight async `register` transform, or
|
|
2013
|
+
* `null` when none is running, including for a sync-only chain, which
|
|
2014
|
+
* never defers. It folds into the one in-flight clock the reducer
|
|
2015
|
+
* already runs for validation, so a deferred transform rides the
|
|
2016
|
+
* anti-flash spinner timing identically.
|
|
2017
|
+
*/
|
|
2018
|
+
readonly transformingSince: number | null;
|
|
2019
|
+
/**
|
|
2020
|
+
* The engine's clock, injected so the reducer stays pure and
|
|
2021
|
+
* deterministic. Frozen to `0` under SSR, where there is no clock.
|
|
2022
|
+
*/
|
|
2023
|
+
readonly now: number;
|
|
2024
|
+
};
|
|
2025
|
+
/**
|
|
2026
|
+
* Pure transition reducer resolving a path's `displayState`. Given the
|
|
2027
|
+
* field's previous `DisplayMachine` and the current `DisplayCtx` it
|
|
2028
|
+
* returns the next machine: the engine owns the clock and the timers,
|
|
2029
|
+
* the reducer owns the timing policy.
|
|
2030
|
+
*
|
|
2031
|
+
* It runs on every field-state read, and again whenever a `reviewAt`
|
|
2032
|
+
* deadline fires, so the whole app's validation-display behavior flows
|
|
2033
|
+
* from this one function in `core/display-state.ts`.
|
|
2034
|
+
*
|
|
2035
|
+
* @internal
|
|
2036
|
+
*/
|
|
2037
|
+
type GetDisplayState = (prev: DisplayMachine, ctx: DisplayCtx) => DisplayMachine;
|
|
2038
|
+
/**
|
|
2039
|
+
* The submit handler `handleSubmit(onSubmit, onError)` returns. Bind it
|
|
2040
|
+
* to a `<form>`:
|
|
2041
|
+
*
|
|
2042
|
+
* ```vue
|
|
2043
|
+
* <form @submit="onSubmit">...</form>
|
|
2044
|
+
* ```
|
|
2045
|
+
*
|
|
2046
|
+
* It takes the originating `Event` and calls `event.preventDefault()`
|
|
2047
|
+
* itself, so bind with `@submit`, not `@submit.prevent`, which would
|
|
2048
|
+
* only prevent the default a second time. Called imperatively with no
|
|
2049
|
+
* event, the `preventDefault` step is skipped.
|
|
2050
|
+
*/
|
|
2051
|
+
type SubmitHandler = (event?: Event) => Promise<void>;
|
|
2052
|
+
/**
|
|
2053
|
+
* The type of `form.handleSubmit`. Pass an `onSubmit` for the happy
|
|
2054
|
+
* path, and optionally an `onError` that receives the validation errors
|
|
2055
|
+
* when parsing fails.
|
|
2056
|
+
*
|
|
2057
|
+
* ```ts
|
|
2058
|
+
* const onSubmit = form.handleSubmit(
|
|
2059
|
+
* (data) => api.signup(data),
|
|
2060
|
+
* (errors) => console.log(errors),
|
|
2061
|
+
* )
|
|
2062
|
+
* ```
|
|
2063
|
+
*/
|
|
2064
|
+
type HandleSubmit<Form extends GenericForm> = (onSubmit: OnSubmit<Form>, onError?: OnError) => SubmitHandler;
|
|
2065
|
+
/**
|
|
2066
|
+
* Per-leaf internal tracker record, distinct from `FieldState.meta`,
|
|
2067
|
+
* which surfaces the registry-attached label, description and
|
|
2068
|
+
* placeholder payload as `Readonly<FieldMetaPayload>`.
|
|
2069
|
+
*
|
|
2070
|
+
* Surfaced for custom-adapter authors threading metadata through their
|
|
2071
|
+
* own pipelines. Most consumers do not reach for it, since the matching
|
|
2072
|
+
* fields appear in friendlier shape on `FieldState`.
|
|
2073
|
+
*/
|
|
2074
|
+
type MetaTrackerValue = {
|
|
2075
|
+
/** ISO timestamp of the most recent write at this path. `null` if never written. */
|
|
2076
|
+
updatedAt: string | null;
|
|
2077
|
+
/** Value as it arrived, before any transforms. Distinguishes a parse-coerced read from raw user input. */
|
|
2078
|
+
rawValue: unknown;
|
|
2079
|
+
/** `true` while at least one binding to this path is currently mounted. */
|
|
2080
|
+
connected: boolean;
|
|
2081
|
+
/** Form this metadata belongs to. */
|
|
2082
|
+
formKey: FormKey;
|
|
2083
|
+
/** Dotted-string path to this leaf, or `null` when not applicable. */
|
|
2084
|
+
path: string | null;
|
|
2085
|
+
/**
|
|
2086
|
+
* `true` when storage and the visible display diverge at this path.
|
|
2087
|
+
* Reserved for the case the schema cannot see on its own: storage
|
|
2088
|
+
* forces a value, `0` for a numeric leaf or `0n` for a bigint leaf,
|
|
2089
|
+
* while the DOM input shows `''`, so the runtime needs a side channel
|
|
2090
|
+
* to tell "user typed 0" from "user supplied nothing".
|
|
2091
|
+
*
|
|
2092
|
+
* Set automatically for numeric leaves, by the directive's input
|
|
2093
|
+
* listener on clear and by the construction-time pass when the
|
|
2094
|
+
* consumer supplied no value. Set explicitly for any primitive leaf
|
|
2095
|
+
* through `setValue(path, unset)`, `defaultValues: { x: unset }` or
|
|
2096
|
+
* `reset({ x: unset })`, which is the documented opt-in for strings,
|
|
2097
|
+
* booleans and other types that do not otherwise diverge. Cleared on
|
|
2098
|
+
* the first non-`unset` write.
|
|
2099
|
+
*
|
|
2100
|
+
* Errors are reactive end to end, so any required path with
|
|
2101
|
+
* `blank: true` puts a "No value supplied" entry in `form.errors`
|
|
2102
|
+
* immediately, with no `validate()` or `handleSubmit` call. Most
|
|
2103
|
+
* consumers gate UI on `errors[path]` and `touched` instead; read
|
|
2104
|
+
* `blank` when you want pre-error introspection, such as a "the user
|
|
2105
|
+
* has not decided yet" indicator or a "review unanswered fields"
|
|
2106
|
+
* hint.
|
|
2107
|
+
*
|
|
2108
|
+
* See `docs/validation/blank.md` for the full conceptual model.
|
|
2109
|
+
*/
|
|
2110
|
+
blank: boolean;
|
|
2111
|
+
};
|
|
2112
|
+
type RegisterFlatPath<Form> = Form extends unknown ? FlatPathBuilder<Form, 'register'> : never;
|
|
2113
|
+
/**
|
|
2114
|
+
* A transformation applied to a field's value as user input flows from
|
|
2115
|
+
* the DOM through the directive's assigner. Composes left to right via
|
|
2116
|
+
* the `transforms: [...]` array on `register()`.
|
|
2117
|
+
*
|
|
2118
|
+
* The shape is deliberately generic-erased rather than per-path-typed,
|
|
2119
|
+
* so a personal library of transforms (`trim`, `lowercase`, `slugify`,
|
|
2120
|
+
* `clamp`) plugs into any `register()` slot whatever the path's value
|
|
2121
|
+
* type. Write defensive bodies that no-op on a type mismatch:
|
|
2122
|
+
*
|
|
2123
|
+
* ```ts
|
|
2124
|
+
* export const trim: RegisterTransform = (v) =>
|
|
2125
|
+
* typeof v === 'string' ? v.trim() : v
|
|
2126
|
+
* ```
|
|
2127
|
+
*
|
|
2128
|
+
* Call-site type safety is delegated to the slim-primitive gate: a
|
|
2129
|
+
* transform producing a value the path's storage does not accept is
|
|
2130
|
+
* rejected at write time with a standard diagnostic.
|
|
2131
|
+
*
|
|
2132
|
+
* Transforms may be sync or async. The chain stays fully synchronous,
|
|
2133
|
+
* with the value reaching form state in the same tick, until a
|
|
2134
|
+
* transform returns a thenable; from there the write defers, the field
|
|
2135
|
+
* reads `busy` and `transforming` while the chain settles, and the
|
|
2136
|
+
* resolved value commits once it lands. Rapid edits discard all but the
|
|
2137
|
+
* latest, and a rejection surfaces on `field.transformError` rather
|
|
2138
|
+
* than throwing or logging:
|
|
2139
|
+
*
|
|
2140
|
+
* ```ts
|
|
2141
|
+
* export const normalize: RegisterTransform = async (v, ctx) => {
|
|
2142
|
+
* const res = await fetch(`/normalize?q=${v}`, { signal: ctx?.signal })
|
|
2143
|
+
* return res.text()
|
|
2144
|
+
* }
|
|
2145
|
+
* ```
|
|
2146
|
+
*
|
|
2147
|
+
* `ctx.signal` aborts when the run is superseded by a newer edit, or is
|
|
2148
|
+
* torn down by `reset()` or unmount. Thread it into cancellable I/O so
|
|
2149
|
+
* a stale request is dropped. A sync chain never touches it and
|
|
2150
|
+
* allocates no controller.
|
|
2151
|
+
*
|
|
2152
|
+
* A synchronous throw is caught and aborts the pipeline: later
|
|
2153
|
+
* transforms do not run, nothing is written, and the assigner returns
|
|
2154
|
+
* `false`, so a buggy or defensively-throwing transform never crashes
|
|
2155
|
+
* the host app. An async rejection is the `transformError` channel
|
|
2156
|
+
* above, not a throw into the host app.
|
|
2157
|
+
*/
|
|
2158
|
+
type RegisterTransform = (value: unknown, ctx?: TransformContext) => unknown;
|
|
2159
|
+
/** Options for `register(path, options)`, applied at the binding's own call site. */
|
|
2160
|
+
type RegisterOptions = {
|
|
2161
|
+
/**
|
|
2162
|
+
* Transformation pipeline applied to user-typed values before they
|
|
2163
|
+
* reach form state, composing left to right: each transform receives
|
|
2164
|
+
* the previous one's output, and the first receives the
|
|
2165
|
+
* directive-extracted DOM value.
|
|
2166
|
+
*
|
|
2167
|
+
* The full order is
|
|
2168
|
+
* `DOM event -> modifier cast (.lazy/.trim/.number) -> transforms[0] -> ... -> assigner`.
|
|
2169
|
+
*
|
|
2170
|
+
* User input only. `form.setValue(...)`,
|
|
2171
|
+
* `rv.setValueWithInternalPath(...)`, `form.reset()`, hydration, SSR
|
|
2172
|
+
* replay and `markBlank()` all bypass transforms, because they write
|
|
2173
|
+
* canonical state rather than normalized user input. To apply the
|
|
2174
|
+
* same normalization to a programmatic write, compose it yourself:
|
|
2175
|
+
*
|
|
2176
|
+
* ```ts
|
|
2177
|
+
* form.setValue('email', slugify(lowercase(rawValue)))
|
|
2178
|
+
* ```
|
|
2179
|
+
*
|
|
2180
|
+
* See `RegisterTransform` for the sync and async contract. For a
|
|
2181
|
+
* pattern that needs to inspect the `RegisterValue` itself, such as
|
|
2182
|
+
* rejection with a side effect, redirection to other fields or custom
|
|
2183
|
+
* DOM mutation, use `@update:registerValue` on the bound element
|
|
2184
|
+
* instead; see "Custom assigners" in the API docs.
|
|
2185
|
+
*/
|
|
2186
|
+
transforms?: ReadonlyArray<RegisterTransform>;
|
|
2187
|
+
};
|
|
2188
|
+
/**
|
|
2189
|
+
* The narrow kernel surface the DOM binding drives, structurally a
|
|
2190
|
+
* subset of the form store: the field-record connect and disconnect
|
|
2191
|
+
* transitions, focus marking, which owns blur-validation, the record
|
|
2192
|
+
* and merged-error reads the focus walk needs, and the async-transform
|
|
2193
|
+
* abort for a fully detached path.
|
|
2194
|
+
*
|
|
2195
|
+
* @internal
|
|
2196
|
+
*/
|
|
2197
|
+
type DomBindingKernel = {
|
|
2198
|
+
readonly noteDomConnected: (path: Path) => void;
|
|
2199
|
+
readonly noteDomDisconnected: (path: Path) => void;
|
|
2200
|
+
readonly markFocused: (path: Path, focused: boolean, meta?: {
|
|
2201
|
+
readonly instance?: WriteMeta['instance'];
|
|
2202
|
+
}) => void;
|
|
2203
|
+
readonly getFieldRecord: (path: Path) => FieldRecord | undefined;
|
|
2204
|
+
readonly getErrorsForPath: (path: Path) => ValidationError[];
|
|
2205
|
+
readonly cancelTransformsUnder: (prefix: Path) => void;
|
|
2206
|
+
};
|
|
2207
|
+
/**
|
|
2208
|
+
* The form store's DOM slice: the element registry behind
|
|
2209
|
+
* `field.element` and `field.elements`, no-latch host focus anchors,
|
|
2210
|
+
* and first-error focus resolution. Implemented in the directive
|
|
2211
|
+
* cluster's lazy graph and armed into the store's `domBinding` slot on
|
|
2212
|
+
* first use, see `RegisterValue.ensureDomBinding`. A `null` slot means
|
|
2213
|
+
* nothing in the app ever registered an element.
|
|
2214
|
+
*
|
|
2215
|
+
* @internal
|
|
2216
|
+
*/
|
|
2217
|
+
type AttaformDomBinding = {
|
|
2218
|
+
readonly elements: Map<PathKey, ElementRecord>;
|
|
2219
|
+
readonly attach: (segments: Path, element: HTMLElement, formInstanceId: string, instanceMeta: WriteMeta['instance'] | undefined) => void;
|
|
2220
|
+
readonly detach: (segments: Path, element: HTMLElement) => void;
|
|
2221
|
+
readonly markHostConnected: (segments: Path, connected: boolean, hostEl: HTMLElement, formInstanceId: string) => void;
|
|
2222
|
+
readonly getFirstErrorElement: (formInstanceId: string) => {
|
|
2223
|
+
path: Path;
|
|
2224
|
+
element: HTMLElement;
|
|
2225
|
+
} | null;
|
|
2226
|
+
readonly hostChannel: HostChannel;
|
|
2227
|
+
};
|
|
2228
|
+
/**
|
|
2229
|
+
* The write policy for a `v-register` component host, held on the DOM
|
|
2230
|
+
* binding so it stays in the directive cluster's lazy graph. Applies the
|
|
2231
|
+
* binding site's modifiers, runs register `transforms` and schema
|
|
2232
|
+
* coercion, and buffers `.lazy` writes while focus is inside the host.
|
|
2233
|
+
*
|
|
2234
|
+
* - `write` commits a value the host emitted through
|
|
2235
|
+
* `update:modelValue`.
|
|
2236
|
+
* - `writeInput` takes an `input` emit and commits it only when the
|
|
2237
|
+
* payload carries the live value as an own `value` property.
|
|
2238
|
+
* - `markEditing` records focus entering or leaving the host. Leaving
|
|
2239
|
+
* commits a buffered `.lazy` value, or the trimmed form under `.trim`.
|
|
2240
|
+
* - `holdsDraft` reports a buffered `.lazy` value that storage has not
|
|
2241
|
+
* moved past, the edit a re-render must leave on screen.
|
|
2242
|
+
* - `release` drops any buffered value when the host unmounts.
|
|
2243
|
+
*
|
|
2244
|
+
* @internal
|
|
2245
|
+
*/
|
|
2246
|
+
type HostChannel = {
|
|
2247
|
+
readonly write: (rv: RegisterValue, value: unknown, modifiers: HostModifiers) => boolean;
|
|
2248
|
+
readonly writeInput: (rv: RegisterValue, payload: unknown, modifiers: HostModifiers) => boolean;
|
|
2249
|
+
readonly markEditing: (rv: RegisterValue, editing: boolean, modifiers: HostModifiers) => void;
|
|
2250
|
+
readonly holdsDraft: (rv: RegisterValue) => boolean;
|
|
2251
|
+
readonly release: (rv: RegisterValue) => void;
|
|
2252
|
+
};
|
|
2253
|
+
/**
|
|
2254
|
+
* Factory the directive and `useRegister` inject through
|
|
2255
|
+
* `RegisterValue.ensureDomBinding` to arm a store's `domBinding` slot.
|
|
2256
|
+
*
|
|
2257
|
+
* @internal
|
|
2258
|
+
*/
|
|
2259
|
+
type DomBindingFactory = (kernel: DomBindingKernel) => AttaformDomBinding;
|
|
2260
|
+
/**
|
|
2261
|
+
* What `form.register(path)` returns. Pass it to a native input via
|
|
2262
|
+
* `v-register`:
|
|
2263
|
+
*
|
|
2264
|
+
* ```vue
|
|
2265
|
+
* <input v-register="form.register('email')" />
|
|
2266
|
+
* ```
|
|
2267
|
+
*
|
|
2268
|
+
* Or read `innerRef` directly when integrating a custom component.
|
|
2269
|
+
*
|
|
2270
|
+
* It is a `shallowReadonly` reactive proxy: top-level reads track in
|
|
2271
|
+
* reactive scopes, mutations are blocked, and inner refs (`innerRef`,
|
|
2272
|
+
* `displayValue`) keep their `Ref` shape.
|
|
2273
|
+
*
|
|
2274
|
+
* `path`, `formKey` and `formInstanceId` are the wrapper-component
|
|
2275
|
+
* primitives: a generic component using `useRegister()` derives field
|
|
2276
|
+
* state and form identity from them without re-threading props from
|
|
2277
|
+
* its parent.
|
|
2278
|
+
*/
|
|
2279
|
+
type RegisterValue<Value = unknown> = Readonly<{
|
|
2280
|
+
/**
|
|
2281
|
+
* Live, read-only value at this path. Watch it to drive UI that
|
|
2282
|
+
* depends on the field's current value.
|
|
2283
|
+
*/
|
|
2284
|
+
innerRef: Readonly<Ref<Value>>;
|
|
2285
|
+
/**
|
|
2286
|
+
* Attach an HTML element to this binding. `v-register` calls it
|
|
2287
|
+
* automatically; expose it to a custom integration that registers an
|
|
2288
|
+
* element by hand.
|
|
2289
|
+
*
|
|
2290
|
+
* Recording the element drives the form's element map, behind
|
|
2291
|
+
* `field.meta.connected`, `focusFirstError` and `scrollToFirstError`.
|
|
2292
|
+
*/
|
|
2293
|
+
registerElement: (el: HTMLElement) => void;
|
|
2294
|
+
/**
|
|
2295
|
+
* Detach an HTML element, dropping it from the form's element map.
|
|
2296
|
+
* Pair with `registerElement` for custom integrations.
|
|
2297
|
+
*/
|
|
2298
|
+
deregisterElement: (el: HTMLElement) => void;
|
|
2299
|
+
/**
|
|
2300
|
+
* Write the field's value programmatically, returning `true` when
|
|
2301
|
+
* accepted and `false` when rejected, as for a wrong primitive type
|
|
2302
|
+
* at the path.
|
|
2303
|
+
*
|
|
2304
|
+
* This is the write path for custom directives and consumer
|
|
2305
|
+
* assigners: it routes through the same funnel, and the same
|
|
2306
|
+
* per-instance meta, as the directive's default assigner.
|
|
2307
|
+
* Caller-supplied `meta` passes through unchanged.
|
|
2308
|
+
*/
|
|
2309
|
+
setValueWithInternalPath: (value: unknown, meta?: WriteMeta) => boolean;
|
|
2310
|
+
/**
|
|
2311
|
+
* Commit a value emitted by a third-party component bound through
|
|
2312
|
+
* `v-register`'s compile-time v-model desugar, and mark the field
|
|
2313
|
+
* interacted, since a v-model host has no DOM input listener to flip
|
|
2314
|
+
* the sticky `interacted` bit. The write takes the same path a native
|
|
2315
|
+
* input's does: the binding site's `.number` / `.trim` / `.lazy`
|
|
2316
|
+
* modifiers, then register `transforms`, then schema coercion. The
|
|
2317
|
+
* injected `onUpdate:modelValue` handler is the caller, passing the
|
|
2318
|
+
* modifiers as a static literal. `true` when the write was accepted
|
|
2319
|
+
* or buffered.
|
|
2320
|
+
*
|
|
2321
|
+
* @internal
|
|
2322
|
+
*/
|
|
2323
|
+
setValueFromHost: (value: unknown, modifiers: HostModifiers) => boolean;
|
|
2324
|
+
/**
|
|
2325
|
+
* Commit the live value from a component host's `input` emit. A
|
|
2326
|
+
* payload that is a non-Event object owning a `value` property (the
|
|
2327
|
+
* shape PrimeVue's InputNumber reports per keystroke) writes that
|
|
2328
|
+
* value through `setValueFromHost`; anything else, a native `Event`
|
|
2329
|
+
* reaching a fallthrough listener included, is ignored and returns
|
|
2330
|
+
* `false`. The injected `onInput` handler is the only caller.
|
|
2331
|
+
*
|
|
2332
|
+
* @internal
|
|
2333
|
+
*/
|
|
2334
|
+
setValueFromHostInput: (payload: unknown, modifiers: HostModifiers) => boolean;
|
|
2335
|
+
/**
|
|
2336
|
+
* The final step of a component-host write, after modifiers,
|
|
2337
|
+
* transforms and coercion: an empty signal (`''`, `null` or
|
|
2338
|
+
* `undefined`) the field's schema does not admit marks the field
|
|
2339
|
+
* blank, and anything else writes through. Called by the host write
|
|
2340
|
+
* channel.
|
|
2341
|
+
*
|
|
2342
|
+
* @internal
|
|
2343
|
+
*/
|
|
2344
|
+
commitFromHost: (value: unknown) => boolean;
|
|
2345
|
+
/**
|
|
2346
|
+
* Mark this field DOM-connected during SSR, so a server-rendered
|
|
2347
|
+
* template reading `form.fields.<path>.connected` does not flicker on
|
|
2348
|
+
* hydration. `v-register` calls it for you; a no-op on the client.
|
|
2349
|
+
*
|
|
2350
|
+
* @internal
|
|
2351
|
+
*/
|
|
2352
|
+
markConnectedOptimistically: () => void;
|
|
2353
|
+
/**
|
|
2354
|
+
* Mark this field DOM-connected or disconnected for a `v-register`
|
|
2355
|
+
* component host that binds value through the v-model desugar but
|
|
2356
|
+
* exposes no single inner control to register: a composite widget, or
|
|
2357
|
+
* one where none was found. The directive calls it on mount and
|
|
2358
|
+
* unmount. Distinct from the SSR-only
|
|
2359
|
+
* `markConnectedOptimistically`.
|
|
2360
|
+
*
|
|
2361
|
+
* `hostEl` is the host root, recorded on connect as the field's
|
|
2362
|
+
* focus-first-error anchor. A no-latch host registers no control, so
|
|
2363
|
+
* it would otherwise be invisible to `focusFirstError` and
|
|
2364
|
+
* `scrollToFirstError`, which walk the registered-element set. On an
|
|
2365
|
+
* invalid submit the error walk resolves the host root to its first
|
|
2366
|
+
* focusable descendant. The directive passes the same element on both
|
|
2367
|
+
* edges.
|
|
2368
|
+
*
|
|
2369
|
+
* @internal
|
|
2370
|
+
*/
|
|
2371
|
+
markHostConnected: (connected: boolean, hostEl: HTMLElement) => void;
|
|
2372
|
+
/**
|
|
2373
|
+
* Mark this field focused or blurred for a `v-register` component
|
|
2374
|
+
* host with no single latched control: a composite widget whose focus
|
|
2375
|
+
* moves between inner segments, or a control-less one. The directive
|
|
2376
|
+
* tracks focusin and focusout on the widget root and forwards here,
|
|
2377
|
+
* so focus state and blur-validation still arm without an
|
|
2378
|
+
* element-level focus listener. Carries this binding's instance meta,
|
|
2379
|
+
* including its `validateOn`.
|
|
2380
|
+
*
|
|
2381
|
+
* @internal
|
|
2382
|
+
*/
|
|
2383
|
+
markFocused: (focused: boolean) => void;
|
|
2384
|
+
/**
|
|
2385
|
+
* `true` when an element already registered for this binding's path
|
|
2386
|
+
* is inside, or is, `hostElement`. The directive's component-host
|
|
2387
|
+
* branch reads it to tell a `useRegister` wrapper, whose inner
|
|
2388
|
+
* control self-registered, from a third-party component, which
|
|
2389
|
+
* registered nothing, so the latch runs only for the latter.
|
|
2390
|
+
*
|
|
2391
|
+
* @internal
|
|
2392
|
+
*/
|
|
2393
|
+
hasRegisteredDescendant: (hostElement: HTMLElement) => boolean;
|
|
2394
|
+
/**
|
|
2395
|
+
* Arm this binding's form store with the DOM-binding implementation:
|
|
2396
|
+
* element registry, focus listeners, first-error focus resolution.
|
|
2397
|
+
* That implementation lives in the directive cluster's lazy graph, so
|
|
2398
|
+
* the directive and `useRegister` inject its factory here before any
|
|
2399
|
+
* element call, and once armed the store's `domBinding` slot serves
|
|
2400
|
+
* every later registration.
|
|
2401
|
+
*
|
|
2402
|
+
* Optional, so a hand-rolled RegisterValue owning its own element
|
|
2403
|
+
* handling need not declare it. Returns the armed binding, which is how
|
|
2404
|
+
* the directive reaches the host write channel.
|
|
2405
|
+
*
|
|
2406
|
+
* @internal
|
|
2407
|
+
*/
|
|
2408
|
+
ensureDomBinding?: (factory: DomBindingFactory) => AttaformDomBinding;
|
|
2409
|
+
/**
|
|
2410
|
+
* Canonical JSON-encoded path key for this binding, such as
|
|
2411
|
+
* `'["items",0,"name"]'`. Useful for stable Map and Set keys, log
|
|
2412
|
+
* messages, and equality against another `RegisterValue`'s path.
|
|
2413
|
+
* Treat it as opaque: for `form.fields(...)` and `form.values(...)`
|
|
2414
|
+
* lookups inside a wrapper component, use `segments`.
|
|
2415
|
+
*/
|
|
2416
|
+
path: PathKey;
|
|
2417
|
+
/**
|
|
2418
|
+
* Structured path segments for this binding, such as
|
|
2419
|
+
* `['items', 0, 'name']`. The consumer-friendly form for
|
|
2420
|
+
* `form.fields(...)` and `form.values(...)` lookups in a generic
|
|
2421
|
+
* wrapper:
|
|
2422
|
+
*
|
|
2423
|
+
* ```ts
|
|
2424
|
+
* const rv = useRegister()
|
|
2425
|
+
* const form = injectForm()
|
|
2426
|
+
* const field = computed(() => form.fields(rv.value?.segments ?? []))
|
|
2427
|
+
* ```
|
|
2428
|
+
*
|
|
2429
|
+
* Frozen at runtime, so a wrapper can read it without defensive
|
|
2430
|
+
* copying.
|
|
2431
|
+
*/
|
|
2432
|
+
segments: Path;
|
|
2433
|
+
/**
|
|
2434
|
+
* The form's `key`, supplied or auto-allocated, mirroring `form.key`.
|
|
2435
|
+
* Useful in a wrapper that targets a specific form by key without
|
|
2436
|
+
* prop-drilling.
|
|
2437
|
+
*/
|
|
2438
|
+
formKey: string;
|
|
2439
|
+
/**
|
|
2440
|
+
* Per-mount identifier for the form instance, stable across the
|
|
2441
|
+
* form's lifetime. The directive uses it to scope element
|
|
2442
|
+
* registrations to a single mount; it is exposed here for wrappers
|
|
2443
|
+
* that must disambiguate sibling forms sharing a `key`.
|
|
2444
|
+
*/
|
|
2445
|
+
formInstanceId: string;
|
|
2446
|
+
/**
|
|
2447
|
+
* The readonly handle the directive iterates for this binding's
|
|
2448
|
+
* transform pipeline. See `RegisterOptions.transforms` for the public
|
|
2449
|
+
* contract. Optional, so a hand-rolled mock need not declare an empty
|
|
2450
|
+
* array; the directive falls back to a no-op pipeline.
|
|
2451
|
+
*
|
|
2452
|
+
* @internal
|
|
2453
|
+
*/
|
|
2454
|
+
transforms?: ReadonlyArray<RegisterTransform>;
|
|
2455
|
+
/**
|
|
2456
|
+
* Schema-driven coercion closure baked at register time, capturing
|
|
2457
|
+
* the path's slim accept set and the resolved coercion index so the
|
|
2458
|
+
* per-event hot path is one function call. The identity function when
|
|
2459
|
+
* coercion is disabled or the path admits no coercion target.
|
|
2460
|
+
* Optional, so a hand-rolled mock need not declare it; the directive
|
|
2461
|
+
* falls back to identity.
|
|
2462
|
+
*
|
|
2463
|
+
* @internal
|
|
2464
|
+
*/
|
|
2465
|
+
coerce?: (value: unknown) => unknown;
|
|
2466
|
+
/**
|
|
2467
|
+
* Element-level coercion closure for container paths (`z.array(...)`,
|
|
2468
|
+
* `z.set(...)`), coercing a scalar DOM-side value such as an option's
|
|
2469
|
+
* `value` attribute or a checkbox's value against the container's
|
|
2470
|
+
* element type. `undefined` when the path is not a container, where
|
|
2471
|
+
* `coerce` is used exclusively.
|
|
2472
|
+
*
|
|
2473
|
+
* The directive's read-side comparisons in `setChecked` and
|
|
2474
|
+
* `setSelected` use it to keep parity with the change handler's
|
|
2475
|
+
* write-side path-level coerce.
|
|
2476
|
+
*
|
|
2477
|
+
* @internal
|
|
2478
|
+
*/
|
|
2479
|
+
coerceElement?: (value: unknown) => unknown;
|
|
2480
|
+
/**
|
|
2481
|
+
* Read-only string view of the field's current value: what the
|
|
2482
|
+
* compile-time `:value` injection reads on every input, textarea and
|
|
2483
|
+
* select bound by `v-register`.
|
|
2484
|
+
*
|
|
2485
|
+
* `''` when the path is in the form's `blankPaths` set, or storage is
|
|
2486
|
+
* `null` or `undefined`; otherwise `String(storage)`. The blank branch
|
|
2487
|
+
* is what lets a user clear a numeric field without the next render
|
|
2488
|
+
* patching `el.value` back to the slim default `'0'`.
|
|
2489
|
+
*/
|
|
2490
|
+
displayValue: Readonly<Ref<string>>;
|
|
2491
|
+
/**
|
|
2492
|
+
* Blank-aware presentation for a component host's `:modelValue`, the
|
|
2493
|
+
* typed-model analog of `displayValue`'s `''`: `undefined` when the
|
|
2494
|
+
* path is in `blankPaths`, otherwise the raw typed storage. Lets a
|
|
2495
|
+
* cleared numeric field read empty in a v-model-bound component as it
|
|
2496
|
+
* does in a native input. Read only by the compile-time
|
|
2497
|
+
* component-bridge transform.
|
|
2498
|
+
*
|
|
2499
|
+
* @internal
|
|
2500
|
+
*/
|
|
2501
|
+
hostModelValue: Readonly<Ref<Value | undefined>>;
|
|
2502
|
+
/**
|
|
2503
|
+
* Live `true` when this binding's form is frozen via
|
|
2504
|
+
* `useForm({ disabled })`. The compile-time transforms bind it to the
|
|
2505
|
+
* host's `:disabled`, so a native `<input v-register>` renders the
|
|
2506
|
+
* HTML `disabled` attribute on both server and client, and a
|
|
2507
|
+
* component host receives a `disabled` prop. Read it from a custom
|
|
2508
|
+
* `useRegister` integration to drive your own disabled affordance.
|
|
2509
|
+
*/
|
|
2510
|
+
disabled: Readonly<Ref<boolean>>;
|
|
2511
|
+
/**
|
|
2512
|
+
* Add this field's path to the form's `blankPaths` set, writing the
|
|
2513
|
+
* slim default to storage. Returns the `setValueAtPath` verdict:
|
|
2514
|
+
* `true` accepted, `false` rejected by the slim-primitive gate.
|
|
2515
|
+
*
|
|
2516
|
+
* Called by the directive's input listener on a numeric clear, and by
|
|
2517
|
+
* the `setValue(path, unset)` translation.
|
|
2518
|
+
*
|
|
2519
|
+
* @internal
|
|
2520
|
+
*/
|
|
2521
|
+
markBlank: () => boolean;
|
|
2522
|
+
/**
|
|
2523
|
+
* Flip this field's sticky `interacted` flag, the signal that the
|
|
2524
|
+
* user has made at least one value edit here, an insert or a delete.
|
|
2525
|
+
* Called by the directive's input and change listeners on genuine
|
|
2526
|
+
* user input, never by hydration or a programmatic write. Idempotent,
|
|
2527
|
+
* since the store skips the write once set.
|
|
2528
|
+
*
|
|
2529
|
+
* @internal
|
|
2530
|
+
*/
|
|
2531
|
+
markInteracted: () => void;
|
|
2532
|
+
/**
|
|
2533
|
+
* `true` when the schema's slim primitive set at this path includes
|
|
2534
|
+
* `'undefined'`, meaning the leaf was declared `.optional()`, or sits
|
|
2535
|
+
* in a union admitting `undefined`. Cached at register time.
|
|
2536
|
+
*
|
|
2537
|
+
* The directive's text-input listener reads it to map a DOM clear
|
|
2538
|
+
* onto `undefined` storage rather than `''`, so the `.optional()`
|
|
2539
|
+
* absent semantic survives user interaction. Without it, a user who
|
|
2540
|
+
* typed an invalid value into an optional field and then cleared it
|
|
2541
|
+
* would be stuck with a permanent validation error, storage holding
|
|
2542
|
+
* `''`, which is neither `undefined` nor a valid inner value.
|
|
2543
|
+
*
|
|
2544
|
+
* @internal
|
|
2545
|
+
*/
|
|
2546
|
+
acceptsUndefined: boolean;
|
|
2547
|
+
/**
|
|
2548
|
+
* `true` when the schema's slim primitive set at this path includes
|
|
2549
|
+
* `'string'`. Cached at register time alongside `acceptsUndefined`.
|
|
2550
|
+
*
|
|
2551
|
+
* The directive's text-input listener reads it so a DOM clear on a
|
|
2552
|
+
* numeric-only, boolean-only or bigint-only leaf takes the
|
|
2553
|
+
* `markBlank` path instead of writing `''` through the assigner. The
|
|
2554
|
+
* slim gate would reject the empty string anyway, and the directive's
|
|
2555
|
+
* post-write force-sync would then snap the DOM back to the last
|
|
2556
|
+
* accepted value, making the final character undeletable. Through
|
|
2557
|
+
* `markBlank`, storage holds the slim default with the blank meta and
|
|
2558
|
+
* the DOM stays empty.
|
|
2559
|
+
*
|
|
2560
|
+
* @internal
|
|
2561
|
+
*/
|
|
2562
|
+
acceptsString: boolean;
|
|
2563
|
+
/**
|
|
2564
|
+
* The field's aria satellite ids, mirroring `FieldState.aria`. The
|
|
2565
|
+
* directive points `aria-describedby` at `errorId` while the field is
|
|
2566
|
+
* in its error state. Optional for mock tolerance; the directive
|
|
2567
|
+
* skips aria wiring when absent.
|
|
2568
|
+
*
|
|
2569
|
+
* @internal
|
|
2570
|
+
*/
|
|
2571
|
+
aria?: {
|
|
2572
|
+
readonly errorId: string;
|
|
2573
|
+
readonly descriptionId: string;
|
|
2574
|
+
};
|
|
2575
|
+
/**
|
|
2576
|
+
* Whether the schema marks this path required, from
|
|
2577
|
+
* `schema.isRequiredAtPath(segments)`. Drives `aria-required`.
|
|
2578
|
+
* Optional for the same mock tolerance as `aria`.
|
|
2579
|
+
*
|
|
2580
|
+
* @internal
|
|
2581
|
+
*/
|
|
2582
|
+
isRequired?: boolean;
|
|
2583
|
+
/**
|
|
2584
|
+
* The gated display-state verdict for this path, reusing the same
|
|
2585
|
+
* field-state identity as `form.fields`. The directive watches it to
|
|
2586
|
+
* keep `aria-invalid`, `aria-busy` and `aria-describedby` in lockstep
|
|
2587
|
+
* with the visible error state, even on an async tick with no parent
|
|
2588
|
+
* re-render. Optional; the directive skips aria wiring when absent.
|
|
2589
|
+
*
|
|
2590
|
+
* @internal
|
|
2591
|
+
*/
|
|
2592
|
+
ariaDisplayState?: Readonly<Ref<DisplayState>>;
|
|
2593
|
+
}>;
|
|
2594
|
+
/**
|
|
2595
|
+
* Mutable holder for an async-transform run's `AbortController`, shared
|
|
2596
|
+
* by the directive, which creates the controller the first time a
|
|
2597
|
+
* transform reaches for `ctx.signal`, and the store, which aborts it
|
|
2598
|
+
* when the run is superseded, cancelled or reset. `controller` stays
|
|
2599
|
+
* `null` until `ctx.signal` is touched, so a purely sync chain never
|
|
2600
|
+
* allocates one. `aborted` latches `true` the moment the store tears the
|
|
2601
|
+
* run down, so a signal accessed AFTER teardown still resolves to an
|
|
2602
|
+
* already-aborted signal rather than a live one.
|
|
2603
|
+
*/
|
|
2604
|
+
type TransformAbortHolder = {
|
|
2605
|
+
controller: AbortController | null;
|
|
2606
|
+
aborted: boolean;
|
|
2607
|
+
};
|
|
2608
|
+
/**
|
|
2609
|
+
* The second argument handed to every transform in a `transforms: [...]`
|
|
2610
|
+
* chain. `signal` is an `AbortSignal` that aborts when the run is
|
|
2611
|
+
* superseded by newer input or torn down by a reset or cancel, so a
|
|
2612
|
+
* transform doing cancellable I/O (a `fetch`, a worker round-trip) can
|
|
2613
|
+
* pass `ctx.signal` through and bail the moment its result stops
|
|
2614
|
+
* mattering.
|
|
2615
|
+
*
|
|
2616
|
+
* The signal is lazy: its backing `AbortController` is allocated on
|
|
2617
|
+
* first access, so a purely synchronous chain that never reaches for
|
|
2618
|
+
* `ctx.signal` allocates nothing, and having no in-flight I/O to cancel,
|
|
2619
|
+
* never aborts.
|
|
2620
|
+
*/
|
|
2621
|
+
type TransformContext = {
|
|
2622
|
+
readonly signal: AbortSignal;
|
|
2623
|
+
};
|
|
2624
|
+
/**
|
|
2625
|
+
* Custom assigner installed on an element via the directive's
|
|
2626
|
+
* `[assignKey]` slot OR an `@update:registerValue` listener. Called
|
|
2627
|
+
* by the directive when a DOM event (input / change / etc.) fires
|
|
2628
|
+
* on the bound element.
|
|
2629
|
+
*
|
|
2630
|
+
* The directive passes the extracted value plus the `RegisterValue`
|
|
2631
|
+
* the directive is currently bound to, regardless of install path.
|
|
2632
|
+
* The second arg lets a top-level handler write back to form state
|
|
2633
|
+
* without having to capture the RV via closure:
|
|
2634
|
+
*
|
|
2635
|
+
* ```ts
|
|
2636
|
+
* function upperCaseAssigner(value: unknown, rv: RegisterValue): void {
|
|
2637
|
+
* rv.setValueWithInternalPath(String(value ?? '').toUpperCase())
|
|
2638
|
+
* }
|
|
2639
|
+
* ```
|
|
2640
|
+
*
|
|
2641
|
+
* The `registerValue` parameter is typed optional only to keep
|
|
2642
|
+
* standalone invocations from outside the directive (rare; manual
|
|
2643
|
+
* dispatch in tests, for example) type-checkable; the directive
|
|
2644
|
+
* itself always supplies it at fire time.
|
|
2645
|
+
*
|
|
2646
|
+
* Return `true` when the write was accepted, `false` when it was
|
|
2647
|
+
* rejected (e.g. the value didn't match the path's expected type).
|
|
2648
|
+
* `undefined` is treated as "succeeded" so simple assigners can
|
|
2649
|
+
* just return `void`.
|
|
2650
|
+
*/
|
|
2651
|
+
type CustomDirectiveRegisterAssignerFn = (value: unknown, registerValue?: RegisterValue) => boolean | undefined;
|
|
2652
|
+
/**
|
|
2653
|
+
* Generic shape of a v-register directive variant. Used by the
|
|
2654
|
+
* library's text / checkbox / radio / select directive types and
|
|
2655
|
+
* available for custom integrations that need to drop in their own
|
|
2656
|
+
* variant.
|
|
2657
|
+
*
|
|
2658
|
+
* The value generic admits `undefined` because `useRegister()` may
|
|
2659
|
+
* return `undefined` (a wrapper component rendered without a parent
|
|
2660
|
+
* `registerValue`); binding that value to `v-register` is supported
|
|
2661
|
+
* and installs a no-op assigner at runtime.
|
|
2662
|
+
*/
|
|
2663
|
+
type CustomRegisterDirective<T, Modifiers extends string = string> = ObjectDirective<T & {
|
|
2664
|
+
_assigning?: boolean;
|
|
2665
|
+
/**
|
|
2666
|
+
* Snapshot of the last `value.innerRef.value` reference the
|
|
2667
|
+
* directive's DOM-sync (setSelected / setChecked / radio
|
|
2668
|
+
* `el.checked = …` / text `el.value = …`) was applied for. Used by
|
|
2669
|
+
* every input directive's `updated` / `beforeUpdate` to skip the
|
|
2670
|
+
* per-render DOM sync when the model is identity-unchanged (for
|
|
2671
|
+
* text, a focused `.lazy` edit is held through it), so a parent
|
|
2672
|
+
* re-render (a typed character in a sibling, an async-validation
|
|
2673
|
+
* tick, any reactive read) cannot clobber an in-progress user
|
|
2674
|
+
* interaction. Identity comparison is sound because every form
|
|
2675
|
+
* write produces a fresh value at the path: scalars are new
|
|
2676
|
+
* primitives, and arrays and Sets get fresh references along the
|
|
2677
|
+
* spine through diff-apply, so reference equality on
|
|
2678
|
+
* `innerRef.value` tracks "did the model move" exactly. The
|
|
2679
|
+
* `_assigning` gate stays alongside it, short-circuiting the
|
|
2680
|
+
* immediate post-write render where the user's own input has
|
|
2681
|
+
* already put the DOM in sync.
|
|
2682
|
+
*/
|
|
2683
|
+
_lastAppliedModel?: unknown;
|
|
2684
|
+
/**
|
|
2685
|
+
* Variant-specific "repaint the DOM from current storage" closure,
|
|
2686
|
+
* stashed by each input directive's `created` hook (it mirrors that
|
|
2687
|
+
* variant's post-write force-sync block). The deferred async-transform
|
|
2688
|
+
* orchestrator calls it once the resolved value has committed, so a
|
|
2689
|
+
* bare `<input v-register>` with no other reactive reader still paints
|
|
2690
|
+
* the normalized result without depending on a parent re-render.
|
|
2691
|
+
*/
|
|
2692
|
+
_syncFromStorage?: () => void;
|
|
2693
|
+
[S: symbol]: CustomDirectiveRegisterAssignerFn;
|
|
2694
|
+
}, RegisterValue | undefined, Modifiers, string>;
|
|
2695
|
+
/**
|
|
2696
|
+
* Modifier names supported by `v-register` on `<input type="text">`,
|
|
2697
|
+
* `<input type="number">`, `<textarea>`, and component hosts. Mirrors
|
|
2698
|
+
* Vue's `v-model` modifier semantics on the same elements; combine
|
|
2699
|
+
* freely (`<input v-register.lazy.trim.number="..." />`).
|
|
2700
|
+
*/
|
|
2701
|
+
type RegisterTextModifier =
|
|
2702
|
+
/**
|
|
2703
|
+
* Write on `change` (blur) instead of `input`. The reactive
|
|
2704
|
+
* model only updates after the user tabs/clicks out of the
|
|
2705
|
+
* field. IME composition handlers are skipped under `.lazy`, since
|
|
2706
|
+
* composition events do not gate writes. On a component host the
|
|
2707
|
+
* last value emitted while focus is inside the host commits when
|
|
2708
|
+
* focus leaves it.
|
|
2709
|
+
*/
|
|
2710
|
+
'lazy'
|
|
2711
|
+
/**
|
|
2712
|
+
* Strip leading and trailing whitespace on blur. The form holds
|
|
2713
|
+
* the user's raw input (whitespace included) while they're
|
|
2714
|
+
* typing; on `change` (blur / commit) the value is trimmed
|
|
2715
|
+
* once and written back to both the model and the visible DOM.
|
|
2716
|
+
* Combine with `.lazy` to skip the mid-typing writes entirely. On a
|
|
2717
|
+
* component host the trimmed value commits when focus leaves the
|
|
2718
|
+
* host.
|
|
2719
|
+
*/
|
|
2720
|
+
| 'trim'
|
|
2721
|
+
/**
|
|
2722
|
+
* Cast the value with `parseFloat` before writing. An empty field,
|
|
2723
|
+
* or text that does not parse as a number (`'abc'`), marks the field
|
|
2724
|
+
* blank instead of writing a string. On a component host the cast
|
|
2725
|
+
* applies to string emits, and a value the component already typed
|
|
2726
|
+
* passes through. Auto-applied for `<input type="number">`, where an
|
|
2727
|
+
* explicit `.number` is redundant.
|
|
2728
|
+
*/
|
|
2729
|
+
| 'number';
|
|
2730
|
+
/**
|
|
2731
|
+
* The `v-register` modifiers a component host honours, carried from the
|
|
2732
|
+
* binding site to the host write channel as a static literal that the
|
|
2733
|
+
* compile-time bridge transform emits. Shaped like Vue's directive
|
|
2734
|
+
* modifiers, so the directive passes a binding's modifiers to the channel
|
|
2735
|
+
* unchanged.
|
|
2736
|
+
*
|
|
2737
|
+
* @internal
|
|
2738
|
+
*/
|
|
2739
|
+
type HostModifiers = Readonly<Partial<Record<RegisterTextModifier, boolean>>>;
|
|
2740
|
+
/**
|
|
2741
|
+
* v-register directive variant for `<input type="text">`,
|
|
2742
|
+
* `<input type="number">` and `<textarea>`. Supports the `.lazy`,
|
|
2743
|
+
* `.trim` and `.number` modifiers; `RegisterTextModifier` carries
|
|
2744
|
+
* the per-modifier semantics.
|
|
2745
|
+
*/
|
|
2746
|
+
type RegisterTextCustomDirective = CustomRegisterDirective<HTMLInputElement | HTMLTextAreaElement, RegisterTextModifier>;
|
|
2747
|
+
/** v-register directive variant for checkboxes. No modifiers. */
|
|
2748
|
+
type RegisterCheckboxCustomDirective = CustomRegisterDirective<HTMLInputElement>;
|
|
2749
|
+
/** v-register directive variant for radio inputs. No modifiers. */
|
|
2750
|
+
type RegisterRadioCustomDirective = CustomRegisterDirective<HTMLInputElement>;
|
|
2751
|
+
/**
|
|
2752
|
+
* Modifier name supported by `v-register` on `<select>`. Mirrors
|
|
2753
|
+
* Vue's `v-model` `.number` on the same element.
|
|
2754
|
+
*/
|
|
2755
|
+
type RegisterSelectModifier =
|
|
2756
|
+
/**
|
|
2757
|
+
* Cast each selected option's `value` with `parseFloat` before
|
|
2758
|
+
* writing, so form state holds numbers rather than numeric
|
|
2759
|
+
* strings. The case for it is markup whose option values are
|
|
2760
|
+
* written as strings against a schema expecting numbers.
|
|
2761
|
+
*/
|
|
2762
|
+
'number';
|
|
2763
|
+
/**
|
|
2764
|
+
* v-register directive variant for `<select>`. Supports `.number`;
|
|
2765
|
+
* `RegisterSelectModifier` carries the semantics.
|
|
2766
|
+
*/
|
|
2767
|
+
type RegisterSelectCustomDirective = CustomRegisterDirective<HTMLSelectElement, RegisterSelectModifier>;
|
|
2768
|
+
/** v-register directive variant for the dynamic input/select/textarea bridge. */
|
|
2769
|
+
type RegisterModelDynamicCustomDirective = ObjectDirective<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement, RegisterValue | undefined, string>;
|
|
2770
|
+
/**
|
|
2771
|
+
* The `v-register` directive. Binds a form field to a native
|
|
2772
|
+
* input, select, textarea, checkbox, or radio:
|
|
2773
|
+
*
|
|
2774
|
+
* ```vue
|
|
2775
|
+
* <input v-register="form.register('email')" />
|
|
2776
|
+
* <select v-register="form.register('country')">
|
|
2777
|
+
* <option value="us">US</option>
|
|
2778
|
+
* <option value="uk">UK</option>
|
|
2779
|
+
* </select>
|
|
2780
|
+
* ```
|
|
2781
|
+
*
|
|
2782
|
+
* It also works on a custom component whose root is NOT a native
|
|
2783
|
+
* input: call `useRegister()` in the child's setup to read the
|
|
2784
|
+
* parent's binding, then re-bind `v-register` onto an inner native
|
|
2785
|
+
* element. When the wrapper's root IS the input, attribute
|
|
2786
|
+
* fallthrough handles it and `useRegister` is unnecessary.
|
|
2787
|
+
*
|
|
2788
|
+
* ```vue
|
|
2789
|
+
* <!-- Parent -->
|
|
2790
|
+
* <MyField label="Email" v-register="form.register('email')" />
|
|
2791
|
+
*
|
|
2792
|
+
* <!-- MyField.vue (root is <label>, not <input>) -->
|
|
2793
|
+
* <script setup>
|
|
2794
|
+
* import { useRegister } from 'attaform'
|
|
2795
|
+
* defineProps<{ label: string }>()
|
|
2796
|
+
* const register = useRegister()
|
|
2797
|
+
* </script>
|
|
2798
|
+
* <template>
|
|
2799
|
+
* <label>
|
|
2800
|
+
* <span>{{ label }}</span>
|
|
2801
|
+
* <input v-register="register" />
|
|
2802
|
+
* </label>
|
|
2803
|
+
* </template>
|
|
2804
|
+
* ```
|
|
2805
|
+
*
|
|
2806
|
+
* Modifier support varies by element:
|
|
2807
|
+
* - text / number / textarea: `.lazy`, `.trim`, `.number`
|
|
2808
|
+
* - select: `.number`
|
|
2809
|
+
* - checkbox / radio: none
|
|
2810
|
+
*
|
|
2811
|
+
* See `RegisterTextModifier` / `RegisterSelectModifier` for
|
|
2812
|
+
* per-modifier semantics.
|
|
2813
|
+
*
|
|
2814
|
+
* Delivered by the Vite or Nuxt plugin's compile-time binding, and by
|
|
2815
|
+
* `installVRegister(app)` from `attaform/directive` everywhere else.
|
|
2816
|
+
* Most consumers never import the directive itself; it is exposed for
|
|
2817
|
+
* integrations that install directives manually.
|
|
2818
|
+
*/
|
|
2819
|
+
type RegisterDirective = RegisterTextCustomDirective | RegisterCheckboxCustomDirective | RegisterSelectCustomDirective | RegisterRadioCustomDirective | RegisterModelDynamicCustomDirective;
|
|
2820
|
+
/**
|
|
2821
|
+
* Module augmentation: register `v-register` with Vue's template
|
|
2822
|
+
* type system. Lives in `types-api` because every public entry
|
|
2823
|
+
* (`attaform`, `attaform/zod`, `attaform/zod-v3`, `attaform/zod-v4`)
|
|
2824
|
+
* transitively reaches this file via the `useForm` return type, so
|
|
2825
|
+
* the augmentation propagates to consumer SFCs regardless of which
|
|
2826
|
+
* entry they import from, and whether or not they install
|
|
2827
|
+
* `attaform/nuxt` or the Vite plugin.
|
|
2828
|
+
*
|
|
2829
|
+
* Augmentation targets `vue` rather than `@vue/runtime-core`:
|
|
2830
|
+
* `GlobalDirectives` is originally declared in `@vue/runtime-core`,
|
|
2831
|
+
* but consumers and Volar's strict-template codegen both resolve
|
|
2832
|
+
* the interface through `vue`'s `export * from '@vue/runtime-dom'`
|
|
2833
|
+
* to `export * from '@vue/runtime-core'` chain. TypeScript merges
|
|
2834
|
+
* interfaces across re-exports, so augmenting `'vue'` reaches Volar
|
|
2835
|
+
* without needing `@vue/runtime-core` to be hoisted into the
|
|
2836
|
+
* library's own `node_modules` for its own typecheck.
|
|
2837
|
+
*/
|
|
2838
|
+
declare module 'vue' {
|
|
2839
|
+
interface GlobalDirectives {
|
|
2840
|
+
/**
|
|
2841
|
+
* The `v-register` directive. Binds a form field to a native
|
|
2842
|
+
* input, select, textarea, checkbox, or radio:
|
|
2843
|
+
*
|
|
2844
|
+
* ```vue
|
|
2845
|
+
* <input v-register="form.register('email')" />
|
|
2846
|
+
* ```
|
|
2847
|
+
*
|
|
2848
|
+
* It also works on a custom component whose root is NOT a native
|
|
2849
|
+
* input: call `useRegister()` in the child's setup to read the
|
|
2850
|
+
* parent's binding, then re-bind `v-register` onto an inner
|
|
2851
|
+
* native element. When the wrapper's root IS the input,
|
|
2852
|
+
* attribute fallthrough handles it and `useRegister` is
|
|
2853
|
+
* unnecessary.
|
|
2854
|
+
*
|
|
2855
|
+
* Modifier support varies by element:
|
|
2856
|
+
* - text / number / textarea: `.lazy`, `.trim`, `.number`
|
|
2857
|
+
* - select: `.number`
|
|
2858
|
+
* - checkbox / radio: none
|
|
2859
|
+
*/
|
|
2860
|
+
vRegister: RegisterDirective;
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
/**
|
|
2864
|
+
* Callback form of `setValue`'s value argument. Receives the previous
|
|
2865
|
+
* value at the path and returns the next value:
|
|
2866
|
+
*
|
|
2867
|
+
* ```ts
|
|
2868
|
+
* form.setValue('count', (prev) => prev + 1)
|
|
2869
|
+
* form.setValue((prev) => ({ ...prev, name: 'Ada' }))
|
|
2870
|
+
* ```
|
|
2871
|
+
*
|
|
2872
|
+
* A whole-form callback replaces the whole form. Any key the return
|
|
2873
|
+
* omits is refilled from the SCHEMA's declared defaults, or the slim
|
|
2874
|
+
* value where none is declared, so an omitted key is never carried over
|
|
2875
|
+
* from the previous value or from `defaultValues`. Spread `prev` to keep
|
|
2876
|
+
* the rest of the form.
|
|
2877
|
+
*/
|
|
2878
|
+
type SetValueCallback<Read, Write = Read> = (prev: Read) => Read | Write;
|
|
2879
|
+
/**
|
|
2880
|
+
* The value argument of `form.setValue`. Either the next value
|
|
2881
|
+
* directly, or a callback that derives it from the previous value.
|
|
2882
|
+
*
|
|
2883
|
+
* Type parameters:
|
|
2884
|
+
* - `Write`: what the direct value form accepts, the storable shape at
|
|
2885
|
+
* the path.
|
|
2886
|
+
* - `Read`: what the callback's `prev` argument exposes, defaulting to
|
|
2887
|
+
* `Write`. For a whole-form callback the read shape tags array
|
|
2888
|
+
* elements as possibly-undefined, to match runtime reality.
|
|
2889
|
+
*/
|
|
2890
|
+
type SetValuePayload<Write, Read = Write> = Write | SetValueCallback<Read, Write>;
|
|
2891
|
+
/**
|
|
2892
|
+
* Detect `any` distinctly from `unknown`. The trick: `1 & any` is `any`
|
|
2893
|
+
* and `0 extends any` is `true`; `1 & unknown` is `1` and `0 extends 1`
|
|
2894
|
+
* is `false`. Used to fork `PathSetValuePayload` so `z.any()` paths
|
|
2895
|
+
* resolve to `any` (matching the read-side surface) and `z.unknown()` /
|
|
2896
|
+
* preprocess paths resolve to `unknown` (matching Zod's input typing).
|
|
2897
|
+
*/
|
|
2898
|
+
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
2899
|
+
/**
|
|
2900
|
+
* Resolves `setValue`'s `value` argument type at a single `Path` leaf.
|
|
2901
|
+
*
|
|
2902
|
+
* Three branches, one per Zod input-typing case:
|
|
2903
|
+
*
|
|
2904
|
+
* 1. **`any` leaf (`z.any()`)**: the schema input type is `any`, and
|
|
2905
|
+
* so is the whole form API surface at this path (read, register,
|
|
2906
|
+
* fields). This branch returns raw `any` to stay consistent with
|
|
2907
|
+
* them. A call site passing an unannotated `(prev) => ...` may hit
|
|
2908
|
+
* `noImplicitAny` under the consumer's tsconfig; annotating
|
|
2909
|
+
* `(prev: any) => ...` opts into the looser shape explicitly.
|
|
2910
|
+
*
|
|
2911
|
+
* 2. **`unknown` leaf (`z.unknown()`, `z.preprocess()` input)**: the
|
|
2912
|
+
* schema input is unconstrained and consumers narrow before use.
|
|
2913
|
+
* The branch returns
|
|
2914
|
+
* `({} | null | undefined) | ((prev: unknown) => unknown)` rather
|
|
2915
|
+
* than a `SetValuePayload<unknown, ...>`-style union, for three
|
|
2916
|
+
* reasons:
|
|
2917
|
+
*
|
|
2918
|
+
* a. **Union absorption.** `unknown | X` collapses to `unknown`,
|
|
2919
|
+
* erasing the callback union member. With the callback shape
|
|
2920
|
+
* gone, TS has no contextual type for `prev` and decays it to
|
|
2921
|
+
* implicit `any` under `noImplicitAny`. The triple
|
|
2922
|
+
* `{} | null | undefined` covers the same value space as
|
|
2923
|
+
* `unknown` but is NOT subject to absorption, so the callback
|
|
2924
|
+
* branch survives and `prev` infers cleanly to `unknown`.
|
|
2925
|
+
*
|
|
2926
|
+
* b. **`NonNullable<unknown> = {}`.** Applying `NonNullable` to
|
|
2927
|
+
* the read slot for an unknown leaf narrows `prev` to `{}`,
|
|
2928
|
+
* which is looser than `unknown` and allows ad-hoc property
|
|
2929
|
+
* access. This branch keeps the read slot as `unknown` so the
|
|
2930
|
+
* consumer has to narrow.
|
|
2931
|
+
*
|
|
2932
|
+
* c. **`Unset`-widening does not apply.** `DefaultValuesShape`
|
|
2933
|
+
* widens a primitive leaf to admit `unset`, and an unknown leaf
|
|
2934
|
+
* has no primitive to widen. The open-form triple covers the
|
|
2935
|
+
* same value space the runtime accepts, `unset` included, since
|
|
2936
|
+
* a symbol is a `{}`.
|
|
2937
|
+
*
|
|
2938
|
+
* 3. **Every other leaf** flows through unchanged via
|
|
2939
|
+
* `SetValuePayload<DefaultValuesShape<Leaf>, NonNullable<WriteShape<Leaf>>>`.
|
|
2940
|
+
*/
|
|
2941
|
+
type PathSetValuePayload<Leaf> = IsAny<Leaf> extends true ? any : unknown extends Leaf ? // eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
2942
|
+
({} | null | undefined) | ((prev: unknown) => unknown) : SetValuePayload<DefaultValuesShape<Leaf>, NonNullable<WriteShape<Leaf>>>;
|
|
2943
|
+
/**
|
|
2944
|
+
* Per-field reactive shape returned by `form.fields.<leaf-path>` and
|
|
2945
|
+
* `form.fields(path)`. Slim, readonly across the board. One type lives at
|
|
2946
|
+
* every path, with aggregations rolled up at containers.
|
|
2947
|
+
*
|
|
2948
|
+
* Leaf-aware: dot-access injects these keys at LEAF paths only. At a
|
|
2949
|
+
* container path the proxy descends without injecting, so a schema field
|
|
2950
|
+
* literally named `dirty` at depth 2 or deeper stays reachable as a
|
|
2951
|
+
* descent target and nothing is shadowed. The container call form
|
|
2952
|
+
* (`form.fields('address')`) returns a `FieldState` whose keys aggregate
|
|
2953
|
+
* the descendant leaves.
|
|
2954
|
+
*/
|
|
2955
|
+
type FieldState<Value = unknown> = {
|
|
2956
|
+
readonly value: Value;
|
|
2957
|
+
readonly original: Value;
|
|
2958
|
+
readonly pristine: boolean;
|
|
2959
|
+
readonly dirty: boolean;
|
|
2960
|
+
readonly focused: boolean | null;
|
|
2961
|
+
readonly blurred: boolean | null;
|
|
2962
|
+
readonly touched: boolean;
|
|
2963
|
+
/**
|
|
2964
|
+
* `true` once the user has issued at least one value edit on this
|
|
2965
|
+
* field through `v-register` (an insert or a delete), sticky
|
|
2966
|
+
* thereafter and preserved across disconnects. Distinct from
|
|
2967
|
+
* `dirty`: typing `"a"` then deleting it back to empty leaves the
|
|
2968
|
+
* field net-unchanged (`dirty: false`) yet `interacted: true`.
|
|
2969
|
+
* Distinct from `touched`: tabbing through a field without editing
|
|
2970
|
+
* flips `touched` but never `interacted`. Set only by user input,
|
|
2971
|
+
* never by hydration or programmatic `setValue`; cleared by
|
|
2972
|
+
* `form.reset()` / `form.resetField(path)`. Containers roll it up as
|
|
2973
|
+
* a disjunction (any descendant interacted).
|
|
2974
|
+
*/
|
|
2975
|
+
readonly interacted: boolean;
|
|
2976
|
+
/**
|
|
2977
|
+
* `true` once the user has blurred this field after editing it: the
|
|
2978
|
+
* first time they edit a value and then leave. Sticky thereafter and
|
|
2979
|
+
* preserved across disconnects; a tab-through with no edit never sets
|
|
2980
|
+
* it (`interacted` is still false at that blur). It composes
|
|
2981
|
+
* `interacted` with the departure and drives the default display gate,
|
|
2982
|
+
* so errors reveal once the user finishes a pass and leaves, then stay
|
|
2983
|
+
* visible through a re-focus to be fixed live. Set only by user
|
|
2984
|
+
* input/blur, never by hydration or programmatic writes; cleared by
|
|
2985
|
+
* `form.reset()` / `form.resetField(path)`. Containers roll it up as a
|
|
2986
|
+
* disjunction.
|
|
2987
|
+
*/
|
|
2988
|
+
readonly blurredAfterInteraction: boolean;
|
|
2989
|
+
readonly connected: boolean;
|
|
2990
|
+
/**
|
|
2991
|
+
* The first DOM element bound to this path via `v-register`, or
|
|
2992
|
+
* `null` when none is registered (initial mount, post-unmount,
|
|
2993
|
+
* SSR). "First" means first by registration order. Reach for it to
|
|
2994
|
+
* call a native DOM method on a field's input (`focus()`,
|
|
2995
|
+
* `scrollIntoView()`, `select()`, `setSelectionRange()`) without
|
|
2996
|
+
* Attaform having to verb every imperative:
|
|
2997
|
+
*
|
|
2998
|
+
* ```ts
|
|
2999
|
+
* form.fields.email.element?.focus()
|
|
3000
|
+
* form.fields.email.element?.scrollIntoView({ block: 'center' })
|
|
3001
|
+
* ```
|
|
3002
|
+
*
|
|
3003
|
+
* On a path with multiple bindings (input syncing, mirrored shadow
|
|
3004
|
+
* inputs), prefer `elements` and pick the target yourself. Reactive:
|
|
3005
|
+
* a register or deregister triggers re-evaluation.
|
|
3006
|
+
*/
|
|
3007
|
+
readonly element: HTMLElement | null;
|
|
3008
|
+
/**
|
|
3009
|
+
* Every DOM element currently bound to this path via `v-register`,
|
|
3010
|
+
* in registration order, or an empty array when none is. Two
|
|
3011
|
+
* bindings on one path is a supported shape, for input syncing or
|
|
3012
|
+
* mirrored shadow inputs:
|
|
3013
|
+
*
|
|
3014
|
+
* ```ts
|
|
3015
|
+
* for (const el of form.fields.email.elements) el.blur()
|
|
3016
|
+
* ```
|
|
3017
|
+
*
|
|
3018
|
+
* For the common single-binding case, `element` is sugar over
|
|
3019
|
+
* `elements[0] ?? null`.
|
|
3020
|
+
*/
|
|
3021
|
+
readonly elements: readonly HTMLElement[];
|
|
3022
|
+
readonly updatedAt: string | null;
|
|
3023
|
+
/**
|
|
3024
|
+
* Every `ValidationError` in this path's SUBTREE: the node's own
|
|
3025
|
+
* bucket plus every descendant's, sorted by schema-declaration order.
|
|
3026
|
+
* On a leaf (no descendants) this equals `ownErrors`. On a container
|
|
3027
|
+
* it rolls up the whole fieldset, so a parent's `errors` is non-empty
|
|
3028
|
+
* whenever any child is invalid.
|
|
3029
|
+
*
|
|
3030
|
+
* For the errors pinned at THIS path alone (a container's own
|
|
3031
|
+
* cross-field `.refine()`, or `setErrors` at this path), read
|
|
3032
|
+
* `ownErrors`.
|
|
3033
|
+
*/
|
|
3034
|
+
readonly errors: readonly ValidationError[];
|
|
3035
|
+
/**
|
|
3036
|
+
* Every `ValidationError` at THIS exact path's own bucket, excluding
|
|
3037
|
+
* descendants: schema errors, blank-required errors, and `setErrors`
|
|
3038
|
+
* pinned here, in `schema`, `blank`, `user` order. The exact-path
|
|
3039
|
+
* counterpart to the subtree-scoped `errors`.
|
|
3040
|
+
*
|
|
3041
|
+
* On a leaf this is the same array as `errors` (a leaf has no
|
|
3042
|
+
* descendants). On a container it holds only the container's OWN
|
|
3043
|
+
* errors, a cross-field `.refine()` on the fieldset or a path-less
|
|
3044
|
+
* `setErrors` at this node, without dragging in any child error:
|
|
3045
|
+
*
|
|
3046
|
+
* ```vue
|
|
3047
|
+
* <p v-if="form.fields.address.firstOwnError">
|
|
3048
|
+
* {{ form.fields.address.firstOwnError.message }}
|
|
3049
|
+
* </p>
|
|
3050
|
+
* ```
|
|
3051
|
+
*
|
|
3052
|
+
* On `form.meta` this is the root `[]` bucket: form-level errors from
|
|
3053
|
+
* a root `.refine()` or a path-less `setErrors`. `form.meta.ownErrors`
|
|
3054
|
+
* (with `firstOwnError`) is the banner accessor for those.
|
|
3055
|
+
*/
|
|
3056
|
+
readonly ownErrors: readonly ValidationError[];
|
|
3057
|
+
/**
|
|
3058
|
+
* `true` while a per-field validation run is in flight at this path.
|
|
3059
|
+
* Reflects field-level debounced runs (`validate-on-change`) and
|
|
3060
|
+
* cross-field re-validations targeting this path. A whole-form
|
|
3061
|
+
* `validate()` or `parse()` drives `form.meta.validating` alone and
|
|
3062
|
+
* never flips a per-field flag.
|
|
3063
|
+
*
|
|
3064
|
+
* Per-field analogue of `form.meta.validating`. Use for a tight
|
|
3065
|
+
* "Checking…" indicator next to a single async-validated input
|
|
3066
|
+
* without commandeering the whole-form spinner.
|
|
3067
|
+
*/
|
|
3068
|
+
readonly validating: boolean;
|
|
3069
|
+
/**
|
|
3070
|
+
* `true` when this field has no errors AND no per-field validation
|
|
3071
|
+
* is in flight (`errors.length === 0 && !validating`). Confidence
|
|
3072
|
+
* that "we've checked, and we have no problems right now." Use for
|
|
3073
|
+
* green-checkmark / `aria-invalid` UX.
|
|
3074
|
+
*
|
|
3075
|
+
* Validation-only: an in-flight async transform does NOT clamp
|
|
3076
|
+
* `valid` to `false`. `valid` is the verdict on the last committed
|
|
3077
|
+
* value; `busy` is the union "work in flight" signal.
|
|
3078
|
+
*/
|
|
3079
|
+
readonly valid: boolean;
|
|
3080
|
+
/**
|
|
3081
|
+
* `true` while an async `register` transform is in flight at this
|
|
3082
|
+
* path: a transform returned a thenable and the resolved value has
|
|
3083
|
+
* not yet committed to form state. Always `false` for a sync-only
|
|
3084
|
+
* chain, which reaches form state in the same tick with no deferral.
|
|
3085
|
+
* Containers roll it up as a disjunction (any descendant transforming).
|
|
3086
|
+
*/
|
|
3087
|
+
readonly transforming: boolean;
|
|
3088
|
+
/**
|
|
3089
|
+
* `transforming || validating`: the "work is in flight at this path"
|
|
3090
|
+
* signal. Drives `aria-busy` through `displayState` on a
|
|
3091
|
+
* revealed field, and is the surface to bind for a busy indicator on a
|
|
3092
|
+
* field not yet revealed (where `displayState` stays idle by the
|
|
3093
|
+
* reveal gate). Containers roll it up as a disjunction.
|
|
3094
|
+
*/
|
|
3095
|
+
readonly busy: boolean;
|
|
3096
|
+
/**
|
|
3097
|
+
* The `Error` from the most recent async transform that rejected at
|
|
3098
|
+
* this path, else `null`. A per-field normalization-failure channel
|
|
3099
|
+
* separate from validation `errors`: a transform that rejects (a
|
|
3100
|
+
* failed fetch, a parse error) surfaces here instead of crashing the
|
|
3101
|
+
* host app or logging. Cleared when a fresh transform starts or a
|
|
3102
|
+
* write supersedes it. Leaf-only: a container never rolls it up, and
|
|
3103
|
+
* reads `null`.
|
|
3104
|
+
*/
|
|
3105
|
+
readonly transformError: Error | null;
|
|
3106
|
+
/**
|
|
3107
|
+
* The single display-state verdict at this path: `'idle'`,
|
|
3108
|
+
* `'pending'`, `'error'`, or `'success'`. The source of truth the
|
|
3109
|
+
* four `show*` booleans below derive from. Bind it directly when one
|
|
3110
|
+
* branch over the set reads cleaner than four flags:
|
|
3111
|
+
*
|
|
3112
|
+
* ```vue
|
|
3113
|
+
* <FieldStatusIcon :state="form.fields.email.displayState" />
|
|
3114
|
+
* ```
|
|
3115
|
+
*
|
|
3116
|
+
* Resolved by the display heuristic: gate closed → `'idle'`; a run
|
|
3117
|
+
* in flight → a delayed `'pending'`; an own-path error → `'error'`;
|
|
3118
|
+
* otherwise earned `valid` → `'success'`, else `'idle'`. The gate
|
|
3119
|
+
* opens after the first submit attempt OR once the field is edited
|
|
3120
|
+
* and left.
|
|
3121
|
+
*
|
|
3122
|
+
* Available on container paths too: `form.fields.users[0].displayState`
|
|
3123
|
+
* rolls up over the row's descendants.
|
|
3124
|
+
*/
|
|
3125
|
+
readonly displayState: DisplayState;
|
|
3126
|
+
/**
|
|
3127
|
+
* `displayState === 'error'`. The centralised "render this field's
|
|
3128
|
+
* errors right now?" gate, so templates avoid re-spelling the
|
|
3129
|
+
* heuristic at every error site:
|
|
3130
|
+
*
|
|
3131
|
+
* ```vue
|
|
3132
|
+
* <span v-if="form.fields.email.showErrors">
|
|
3133
|
+
* {{ form.fields.email.firstError?.message }}
|
|
3134
|
+
* </span>
|
|
3135
|
+
* ```
|
|
3136
|
+
*
|
|
3137
|
+
* Kept plural to match `errors` / `firstError`. On container paths it
|
|
3138
|
+
* rolls up over descendants (any descendant resolving to `'error'`
|
|
3139
|
+
* flips the container on).
|
|
3140
|
+
*/
|
|
3141
|
+
readonly showErrors: boolean;
|
|
3142
|
+
/**
|
|
3143
|
+
* `displayState === 'pending'`. A per-field validation run is in
|
|
3144
|
+
* flight at this path and the prior verdict is stale; drive a spinner
|
|
3145
|
+
* or a "Checking…" affordance.
|
|
3146
|
+
*/
|
|
3147
|
+
readonly showPending: boolean;
|
|
3148
|
+
/**
|
|
3149
|
+
* `displayState === 'success'`. Validation has passed and the timing
|
|
3150
|
+
* gate has cleared a positive confirmation; drive the green-check
|
|
3151
|
+
* pattern.
|
|
3152
|
+
*/
|
|
3153
|
+
readonly showSuccess: boolean;
|
|
3154
|
+
/**
|
|
3155
|
+
* `displayState === 'idle'`. Nothing to surface yet, either
|
|
3156
|
+
* pre-interaction or gate-open with no verdict worth showing. Read it
|
|
3157
|
+
* to suppress helper text the moment another signal takes over.
|
|
3158
|
+
*/
|
|
3159
|
+
readonly showIdle: boolean;
|
|
3160
|
+
/**
|
|
3161
|
+
* The first `ValidationError` at this path in the deterministic
|
|
3162
|
+
* schema-declaration order, equivalent to `errors[0]` and exposed as
|
|
3163
|
+
* sugar for "show the highest-priority error message and ignore the
|
|
3164
|
+
* rest":
|
|
3165
|
+
*
|
|
3166
|
+
* ```vue
|
|
3167
|
+
* <span v-if="form.fields.email.showErrors">
|
|
3168
|
+
* {{ form.fields.email.firstError?.message }}
|
|
3169
|
+
* </span>
|
|
3170
|
+
* ```
|
|
3171
|
+
*
|
|
3172
|
+
* `undefined` when no errors exist, and independent of `showErrors`:
|
|
3173
|
+
* the data primitive is always available, and the heuristic decides
|
|
3174
|
+
* only when to render it.
|
|
3175
|
+
*
|
|
3176
|
+
* On container paths, the first error in the aggregated subtree
|
|
3177
|
+
* (descendants sorted by `pathOrdinals`). For the container's OWN
|
|
3178
|
+
* first error only, read `firstOwnError`.
|
|
3179
|
+
*/
|
|
3180
|
+
readonly firstError: ValidationError | undefined;
|
|
3181
|
+
/**
|
|
3182
|
+
* The first `ValidationError` in `ownErrors` (this exact path's own
|
|
3183
|
+
* bucket), or `undefined` when that bucket is empty. Equivalent to
|
|
3184
|
+
* `ownErrors[0]`, the exact-path counterpart to `firstError`.
|
|
3185
|
+
*
|
|
3186
|
+
* On a leaf this equals `firstError`. On a container it is the
|
|
3187
|
+
* container's own cross-field error (a fieldset `.refine()`), never a
|
|
3188
|
+
* child's. `form.meta.firstOwnError` is the form-level banner: the
|
|
3189
|
+
* first root `[]` error from a root `.refine()` or a path-less
|
|
3190
|
+
* `setErrors`:
|
|
3191
|
+
*
|
|
3192
|
+
* ```vue
|
|
3193
|
+
* <p v-if="form.meta.firstOwnError" role="alert">
|
|
3194
|
+
* {{ form.meta.firstOwnError.message }}
|
|
3195
|
+
* </p>
|
|
3196
|
+
* ```
|
|
3197
|
+
*/
|
|
3198
|
+
readonly firstOwnError: ValidationError | undefined;
|
|
3199
|
+
readonly path: ReadonlyArray<string | number>;
|
|
3200
|
+
/**
|
|
3201
|
+
* Stable, SSR-safe DOM id for this field, unique across every mount
|
|
3202
|
+
* on the page. Derived from the form's key and this path, folded with
|
|
3203
|
+
* the form's per-mount `instanceId` so two simultaneous mounts of the
|
|
3204
|
+
* same keyed form never collide. Bind it to wire a label and its
|
|
3205
|
+
* input without inventing your own id:
|
|
3206
|
+
*
|
|
3207
|
+
* ```vue
|
|
3208
|
+
* <label :for="form.fields.email.id">Email</label>
|
|
3209
|
+
* <input :id="form.fields.email.id" v-register="form.register('email')" />
|
|
3210
|
+
* ```
|
|
3211
|
+
*
|
|
3212
|
+
* Treat as identity, not state: stable for the path across the form's
|
|
3213
|
+
* lifetime, opaque, not meant to be parsed.
|
|
3214
|
+
*/
|
|
3215
|
+
readonly id: string;
|
|
3216
|
+
/**
|
|
3217
|
+
* Satellite ids derived from {@link id} for the elements that
|
|
3218
|
+
* describe this field. Wire them to an error node and a description
|
|
3219
|
+
* node so assistive tech announces them with the input. The
|
|
3220
|
+
* `v-register` directive points `aria-describedby` at `errorId`
|
|
3221
|
+
* automatically while the field is in its error state; you render the
|
|
3222
|
+
* matching element and id it:
|
|
3223
|
+
*
|
|
3224
|
+
* ```vue
|
|
3225
|
+
* <input v-register="form.register('email')" />
|
|
3226
|
+
* <span :id="form.fields.email.aria.errorId" v-if="form.fields.email.showErrors">
|
|
3227
|
+
* {{ form.fields.email.firstError?.message }}
|
|
3228
|
+
* </span>
|
|
3229
|
+
* ```
|
|
3230
|
+
*
|
|
3231
|
+
* `descriptionId` is for opt-in help text; chain it into your own
|
|
3232
|
+
* `aria-describedby` when you render a persistent description element.
|
|
3233
|
+
*/
|
|
3234
|
+
readonly aria: {
|
|
3235
|
+
readonly errorId: string;
|
|
3236
|
+
readonly descriptionId: string;
|
|
3237
|
+
};
|
|
3238
|
+
/**
|
|
3239
|
+
* Stable identity for this field as an element of its parent array,
|
|
3240
|
+
* suitable as a Vue `:key` when iterating array elements. An allocated
|
|
3241
|
+
* token (not derived from the element's value) that follows the
|
|
3242
|
+
* element across inserts, removals, moves, and swaps, so a row keeps
|
|
3243
|
+
* its component instance across a reorder. Empty for fields that are
|
|
3244
|
+
* not array elements. Treat as opaque identity, not state.
|
|
3245
|
+
*/
|
|
3246
|
+
readonly key: string;
|
|
3247
|
+
readonly blank: boolean;
|
|
3248
|
+
/**
|
|
3249
|
+
* `true` when this field's form is frozen via `useForm({ disabled })`.
|
|
3250
|
+
* While disabled, value writes no-op and this field's `displayState`
|
|
3251
|
+
* is forced to `'idle'` so error, pending, and success signals stand
|
|
3252
|
+
* down. A form-level flag: every field of a disabled form reads
|
|
3253
|
+
* `true`, and the form root exposes it as `form.meta.disabled`. Bind
|
|
3254
|
+
* read-only or styling affordances off this.
|
|
3255
|
+
*/
|
|
3256
|
+
readonly disabled: boolean;
|
|
3257
|
+
/**
|
|
3258
|
+
* Presentational label for this field. It resolves through the
|
|
3259
|
+
* shared cross-adapter field-meta store, written with
|
|
3260
|
+
* `schema.register(fieldMeta, {...})` on Zod 4 or the `withMeta()`
|
|
3261
|
+
* helper on either major, and falls back to a humanized form of the
|
|
3262
|
+
* path's last segment when nothing is registered. Always a string.
|
|
3263
|
+
*
|
|
3264
|
+
* ```ts
|
|
3265
|
+
* z.string().register(fieldMeta, { label: 'Reference' })
|
|
3266
|
+
* // template: <label>{{ form.fields.reference.label }}</label>
|
|
3267
|
+
* ```
|
|
3268
|
+
*
|
|
3269
|
+
* A numeric segment (an array index) collapses to the empty string,
|
|
3270
|
+
* so a consumer wanting "Item 3" substitutes their own format.
|
|
3271
|
+
*/
|
|
3272
|
+
readonly label: string;
|
|
3273
|
+
/**
|
|
3274
|
+
* Helper-text description for this field. It reads the registered
|
|
3275
|
+
* `description` first, falls back to the schema's own
|
|
3276
|
+
* `.describe('...')` value (both Zod majors expose that as
|
|
3277
|
+
* `schema.description`), and is `undefined` when neither is set.
|
|
3278
|
+
*
|
|
3279
|
+
* Useful for `aria-describedby`-linked help text, and distinct from
|
|
3280
|
+
* `label`: a description is longer prose, a label a short
|
|
3281
|
+
* presentational noun.
|
|
3282
|
+
*/
|
|
3283
|
+
readonly description: string | undefined;
|
|
3284
|
+
/**
|
|
3285
|
+
* Placeholder hint for input affordance. Reads from the
|
|
3286
|
+
* registered `placeholder`; `undefined` otherwise.
|
|
3287
|
+
*/
|
|
3288
|
+
readonly placeholder: string | undefined;
|
|
3289
|
+
/**
|
|
3290
|
+
* The full registered metadata payload, frozen, or an empty object
|
|
3291
|
+
* when nothing is registered. It is the escape hatch for
|
|
3292
|
+
* consumer-augmented keys, declared through module augmentation on
|
|
3293
|
+
* `FieldMetaPayload`:
|
|
3294
|
+
*
|
|
3295
|
+
* ```ts
|
|
3296
|
+
* declare module 'attaform/zod' {
|
|
3297
|
+
* interface FieldMetaPayload { tooltip?: string }
|
|
3298
|
+
* }
|
|
3299
|
+
* // template: {{ form.fields.email.meta.tooltip }}
|
|
3300
|
+
* ```
|
|
3301
|
+
*/
|
|
3302
|
+
readonly meta: Readonly<FieldMetaPayload>;
|
|
3303
|
+
};
|
|
3304
|
+
/** `true` only for bare `unknown`, the storage shape of a preprocess or
|
|
3305
|
+
* coerce leaf. `any` is excluded, since it absorbs every test. */
|
|
3306
|
+
type IsUnknown<T> = IsAny<T> extends true ? false : unknown extends T ? true : false;
|
|
3307
|
+
/**
|
|
3308
|
+
* Leaf-shape dispatch table for `LeafWalker`. Each entry maps a walker
|
|
3309
|
+
* kind to the leaf type that walker produces at a primitive, Date or
|
|
3310
|
+
* non-recursable position. The lookup `LeafSchemeFor<T>[Kind]` threads
|
|
3311
|
+
* `T` through the leaf type when the kind needs it, as `field` does for
|
|
3312
|
+
* `FieldState<T>`; a kind that does not depend on `T` ignores it, as
|
|
3313
|
+
* `errors` does in always producing
|
|
3314
|
+
* `readonly ValidationError[] | undefined`.
|
|
3315
|
+
*
|
|
3316
|
+
* Adding a walker is one entry here plus a one-line wrapper alias
|
|
3317
|
+
* (`type FooShape<T> = LeafWalker<T, 'foo'>`), because the topology is
|
|
3318
|
+
* shared and only the leaf changes.
|
|
3319
|
+
*
|
|
3320
|
+
* The `errors` entry threads `T` to preserve `| undefined` when the
|
|
3321
|
+
* value type itself includes undefined (DU variant-only fields whose
|
|
3322
|
+
* lifted shape resolves to `X | undefined`). A statically-known leaf
|
|
3323
|
+
* collapses to `readonly ValidationError[]`
|
|
3324
|
+
* with no undefined, while a dynamic-key boundary (an array index, a
|
|
3325
|
+
* record key) re-introduces `| undefined` through the structural
|
|
3326
|
+
* index-signature channels.
|
|
3327
|
+
*
|
|
3328
|
+
* A preprocess or coerce leaf (StorageShape `unknown`) is statically
|
|
3329
|
+
* known too, and the IsUnknown filter keeps it on the non-optional
|
|
3330
|
+
* branch rather than letting `undefined extends unknown` sweep it into
|
|
3331
|
+
* the dynamic arm.
|
|
3332
|
+
*
|
|
3333
|
+
* An implementation-detail surface: consumers reach for `FieldStateMap`
|
|
3334
|
+
* or `FormErrorsSurface` instead.
|
|
3335
|
+
*/
|
|
3336
|
+
interface LeafSchemeFor<T> {
|
|
3337
|
+
field: FieldState<T>;
|
|
3338
|
+
errors: IsUnknown<T> extends true ? readonly ValidationError[] : undefined extends T ? readonly ValidationError[] | undefined : readonly ValidationError[];
|
|
3339
|
+
}
|
|
3340
|
+
/**
|
|
3341
|
+
* Generic walk that produces a proxy shape over `T` with leaves
|
|
3342
|
+
* dispatched through `LeafSchemeFor<T>[Kind]`. The walk topology is
|
|
3343
|
+
* identical across walker kinds and only the leaf type differs: an
|
|
3344
|
+
* object maps homomorphically, an object-union merges by KeyofUnion, an
|
|
3345
|
+
* array indexes, and a primitive or Date terminates.
|
|
3346
|
+
*
|
|
3347
|
+
* `FieldStateMapEntry` and `ErrorsProxyShape` share it rather than
|
|
3348
|
+
* walking the same shape twice per useForm return type, once for the
|
|
3349
|
+
* fields proxy and once for the errors proxy. Sharing lets the bundled
|
|
3350
|
+
* `.d.ts` carry one walker body plus per-kind one-line wrappers,
|
|
3351
|
+
* halving what these two proxies contribute to recursive depth at a
|
|
3352
|
+
* consumer call site.
|
|
3353
|
+
*
|
|
3354
|
+
* `StripOptional` controls whether optional modifiers on input
|
|
3355
|
+
* properties are stripped at every recursion level. `true`, the
|
|
3356
|
+
* default, matches the fields proxy, where every known leaf carries a
|
|
3357
|
+
* `FieldState` wrapper whatever the source `?` says. `false` matches
|
|
3358
|
+
* the errors proxy, whose shape stays structurally identical to the
|
|
3359
|
+
* input form, optional keys included.
|
|
3360
|
+
*
|
|
3361
|
+
* An implementation-detail surface: consumers reach for `FieldStateMap`
|
|
3362
|
+
* or `FormErrorsSurface` instead.
|
|
3363
|
+
*/
|
|
3364
|
+
type LeafWalker<T, Kind extends keyof LeafSchemeFor<unknown>, StripOptional extends boolean = true> = [T] extends [string | number | boolean | bigint | symbol | null | undefined | Date | File] ? LeafSchemeFor<T>[Kind] : [T] extends [ReadonlyArray<infer U>] ? {
|
|
3365
|
+
readonly [K: number]: LeafWalker<U, Kind, StripOptional>;
|
|
3366
|
+
} & ContainerSelfErrorsSlot<T, Kind> : [T] extends [object] ? string extends keyof T ? {
|
|
3367
|
+
readonly [K in keyof T]: LeafWalker<T[K], Kind, StripOptional>;
|
|
3368
|
+
} & ContainerSelfErrorsSlot<T, Kind> : [IsUnion<T>] extends [true] ? StripOptional extends true ? {
|
|
3369
|
+
readonly [K in KeyofUnion<T>]-?: DiscriminatedLeaf<T, K, Kind, StripOptional>;
|
|
3370
|
+
} & ContainerSelfErrorsSlot<T, Kind> : {
|
|
3371
|
+
readonly [K in KeyofUnion<T>]: DiscriminatedLeaf<T, K, Kind, StripOptional>;
|
|
3372
|
+
} & ContainerSelfErrorsSlot<T, Kind> : StripOptional extends true ? {
|
|
3373
|
+
readonly [K in keyof T]-?: LeafWalker<T[K], Kind, StripOptional>;
|
|
3374
|
+
} & ContainerSelfErrorsSlot<T, Kind> : {
|
|
3375
|
+
readonly [K in keyof T]: LeafWalker<T[K], Kind, StripOptional>;
|
|
3376
|
+
} & ContainerSelfErrorsSlot<T, Kind> : LeafSchemeFor<T>[Kind];
|
|
3377
|
+
/**
|
|
3378
|
+
* One key of a discriminated-union container in `LeafWalker`. A key
|
|
3379
|
+
* present and required in EVERY variant is universal, and its node is
|
|
3380
|
+
* always reachable. A variant-only or optional-in-some key is a dynamic
|
|
3381
|
+
* hop: its node is `undefined` while its variant is inactive, so it
|
|
3382
|
+
* carries NODE-optionality (`LeafWalker<...> | undefined`), not
|
|
3383
|
+
* VALUE-optionality (`LeafWalker<... | undefined>`).
|
|
3384
|
+
* `PresentValueOfUnion` strips the synthetic absent-variant `undefined`
|
|
3385
|
+
* so the present node resolves to the precise value type, while a
|
|
3386
|
+
* genuine `undefined` from an `optional` declaration survives.
|
|
3387
|
+
*
|
|
3388
|
+
* Universality is `[T] extends [Record<K, unknown>]`, true exactly when
|
|
3389
|
+
* the whole union satisfies "has K, required", which holds only when
|
|
3390
|
+
* every variant declares K as a required property.
|
|
3391
|
+
*/
|
|
3392
|
+
type DiscriminatedLeaf<T, K extends PropertyKey, Kind extends keyof LeafSchemeFor<unknown>, StripOptional extends boolean> = [T] extends [Record<K, unknown>] ? LeafWalker<PresentValueOfUnion<T, K>, Kind, StripOptional> : LeafWalker<PresentValueOfUnion<T, K>, Kind, StripOptional> | undefined;
|
|
3393
|
+
/**
|
|
3394
|
+
* Intersection augmenting every container in the `form.errors` walker
|
|
3395
|
+
* with a `''` slot. At a container of depth 1 or deeper it is the
|
|
3396
|
+
* container-self sentinel, home to cross-field refine errors and
|
|
3397
|
+
* server-side container marks; at the root it addresses the literal
|
|
3398
|
+
* empty-key field. Gated on `Kind extends 'errors'` so `form.values`
|
|
3399
|
+
* and `form.fields` surfaces stay untouched. Carve-out for schemas that
|
|
3400
|
+
* legitimately declare a `''` field: the declared field type wins; at
|
|
3401
|
+
* runtime the two collide harmlessly (errors at the literal leaf and
|
|
3402
|
+
* any container-self errors share the slot via array concat).
|
|
3403
|
+
*
|
|
3404
|
+
* Global (root) errors are NOT this slot: they live at the root `[]`
|
|
3405
|
+
* and are read via the `errors([])` call-form, never `errors['']`.
|
|
3406
|
+
*/
|
|
3407
|
+
type ContainerSelfErrorsSlot<T, Kind> = Kind extends 'errors' ? '' extends keyof T ? unknown : {
|
|
3408
|
+
readonly ['']: readonly ValidationError[];
|
|
3409
|
+
} : unknown;
|
|
3410
|
+
/**
|
|
3411
|
+
* Recursive type behind `form.fields`. Leaf-aware branching: at
|
|
3412
|
+
* a primitive path (string, number, boolean, bigint, Date) the proxy
|
|
3413
|
+
* returns a `FieldState`; at a container path (object, array) it
|
|
3414
|
+
* descends without injecting leaf keys.
|
|
3415
|
+
*
|
|
3416
|
+
* Field-name collisions at depth 2+ resolve unambiguously: a schema
|
|
3417
|
+
* field literally named `dirty` at depth 2 is reachable as a
|
|
3418
|
+
* descent target (`form.fields.address.dirty` returns the
|
|
3419
|
+
* FieldState for `address.dirty`). Reading `dirty` AT the
|
|
3420
|
+
* leaf-view (`form.fields.address.dirty.dirty`) reads the leaf's own
|
|
3421
|
+
* dirty boolean, because a path segment and a leaf prop occupy
|
|
3422
|
+
* different proxy depths.
|
|
3423
|
+
*
|
|
3424
|
+
* The runtime implementation queries `schema.isLeafAtPath(segments)`
|
|
3425
|
+
* at every step, and this type approximates that with
|
|
3426
|
+
* "T extends primitive". The two agree for typical schemas; an exotic
|
|
3427
|
+
* adapter-defined leaf kind, a custom `Date`-like, may need a runtime
|
|
3428
|
+
* check, and the runtime is authoritative.
|
|
3429
|
+
*
|
|
3430
|
+
* The mapped type strips optional flags (`-?:`) because the field-
|
|
3431
|
+
* state surface always exposes a record per known leaf, regardless
|
|
3432
|
+
* of whether the schema field is declared `.optional()`. An optional
|
|
3433
|
+
* schema means the VALUE can be undefined, which
|
|
3434
|
+
* `FieldState<string | undefined>` carries, while the wrapper itself
|
|
3435
|
+
* always exists. Without the strip, `form.fields.notes` on a
|
|
3436
|
+
* `notes?: string` would type as `FieldState<...> | undefined` and
|
|
3437
|
+
* force consumers to optional-chain through every reactive read.
|
|
3438
|
+
*
|
|
3439
|
+
* For discriminated-union containers the object branch uses
|
|
3440
|
+
* `[T] extends [object]` (non-distributive) plus
|
|
3441
|
+
* `KeyofUnion` / `ValueOfUnion` to merge variant key sets, so a
|
|
3442
|
+
* refrigerated-only `form.fields.cargo.tempMinC` is reachable whatever
|
|
3443
|
+
* the active variant, typed as `FieldState<number | undefined>`. That
|
|
3444
|
+
* matches the runtime's stub `FieldState` for an inactive-variant
|
|
3445
|
+
* path.
|
|
3446
|
+
*/
|
|
3447
|
+
type FieldStateMapEntry<T> = LeafWalker<T, 'field'>;
|
|
3448
|
+
/**
|
|
3449
|
+
* Result of the `form.fields(path)` string call form. A path the schema
|
|
3450
|
+
* declares resolves to its `FieldState`: a leaf's value type, or a
|
|
3451
|
+
* container's rolled-up aggregate, where every FieldState property
|
|
3452
|
+
* exists whatever the value type. A path the schema lacks resolves to
|
|
3453
|
+
* `undefined`, because a typo is not a field and the runtime hands back
|
|
3454
|
+
* `undefined` rather than a phantom stub. A non-literal `string` could
|
|
3455
|
+
* be either, so it widens to `FieldState<unknown> | undefined`.
|
|
3456
|
+
*
|
|
3457
|
+
* Named rather than inlined into the `FieldStateMap` call signature so
|
|
3458
|
+
* the conditional is one cached type. That keeps two
|
|
3459
|
+
* structurally-identical `FieldStateMap` instantiations, such as the
|
|
3460
|
+
* unified `attaform/zod` return and `UseFormReturnV4`, relatable rather
|
|
3461
|
+
* than collapsing into a nominal "two different types with this name"
|
|
3462
|
+
* mismatch.
|
|
3463
|
+
*/
|
|
3464
|
+
type FieldCallResult<Form, P extends string> = [P] extends [FlatPath<Form>] ? FieldState<NestedType<Form, P>> : string extends P ? FieldState<unknown> | undefined : undefined;
|
|
3465
|
+
/**
|
|
3466
|
+
* Type of `form.fields`, a leaf-aware drillable callable Proxy. At a
|
|
3467
|
+
* leaf path it resolves to a `FieldState<Value>`; at a container path
|
|
3468
|
+
* it returns a sub-proxy you can keep drilling.
|
|
3469
|
+
*
|
|
3470
|
+
* Augmented with the callable signatures so dot-access and function-
|
|
3471
|
+
* call coexist on the same identifier:
|
|
3472
|
+
*
|
|
3473
|
+
* ```ts
|
|
3474
|
+
* form.fields.email.value // string (leaf-prop on FieldState)
|
|
3475
|
+
* form.fields('email').value // function-call (dynamic / programmatic)
|
|
3476
|
+
* form.fields(['users', 0, 'name']) // path-array form
|
|
3477
|
+
* form.fields() // root proxy
|
|
3478
|
+
* ```
|
|
3479
|
+
*
|
|
3480
|
+
* Single-bracket dotted access (`form.fields['address.city']`) is
|
|
3481
|
+
* deliberately NOT supported, because JS object semantics read the
|
|
3482
|
+
* dotted string as a single key. Use chained dot or bracket access, or
|
|
3483
|
+
* the callable form.
|
|
3484
|
+
*/
|
|
3485
|
+
type FieldStateMap<Form extends GenericForm> = LeafWalker<Form, 'field'> & {
|
|
3486
|
+
/**
|
|
3487
|
+
* String-path form (dynamic / programmatic). See {@link FieldCallResult}:
|
|
3488
|
+
* a path the schema declares resolves to its precise `FieldState`, a
|
|
3489
|
+
* path it lacks to `undefined`, and a non-literal `string` to
|
|
3490
|
+
* `FieldState<unknown> | undefined`.
|
|
3491
|
+
*/
|
|
3492
|
+
<P extends string>(path: P): FieldCallResult<Form, P>;
|
|
3493
|
+
/**
|
|
3494
|
+
* Tuple-segment form. The segment-array spelling of the string
|
|
3495
|
+
* call form above, for a path built from variables. The two resolve
|
|
3496
|
+
* to the same proxy, so `form.fields(['users', i, 'email'])` and
|
|
3497
|
+
* `form.fields(\`users.${i}.email\`)` are interchangeable.
|
|
3498
|
+
*
|
|
3499
|
+
* Resolves to that path's `FieldState`, which at a container is its
|
|
3500
|
+
* rolled-up aggregate (`displayState`, `valid`, `dirty`, ... all
|
|
3501
|
+
* exist regardless of the value type), exactly as the string form
|
|
3502
|
+
* does. Note this is the CALL form: it hands back the path's state,
|
|
3503
|
+
* not a drillable sub-proxy. To walk into a container's children use
|
|
3504
|
+
* dot/bracket access (`form.fields.users[i].email`), which is what
|
|
3505
|
+
* descends.
|
|
3506
|
+
*/
|
|
3507
|
+
<const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : SegmentPathRejection<JoinSegments<S>>)): FieldState<NestedType<Form, JoinSegments<S>>>;
|
|
3508
|
+
/**
|
|
3509
|
+
* Dynamic-array fallback for a caller passing `Path`-typed runtime
|
|
3510
|
+
* segments, as forwarding `RegisterValue.segments` to resolve a field
|
|
3511
|
+
* view does. The path may not resolve, so the result widens with
|
|
3512
|
+
* `| undefined`; cast when the value type is known.
|
|
3513
|
+
*/
|
|
3514
|
+
(segments: ReadonlyArray<string | number>): FieldState<unknown> | undefined;
|
|
3515
|
+
/**
|
|
3516
|
+
* The no-arg call returns the root FieldState, the same as
|
|
3517
|
+
* `form.fields([])`, aggregating over the whole form as one
|
|
3518
|
+
* conjunction across every active-variant leaf.
|
|
3519
|
+
*/
|
|
3520
|
+
(): FieldState<Form>;
|
|
3521
|
+
};
|
|
3522
|
+
/**
|
|
3523
|
+
* Untyped error map keyed by dotted-string path: the same data
|
|
3524
|
+
* `form.errors` exposes, as a plain record. The case for it is routing
|
|
3525
|
+
* API errors that may land on paths the form's TypeScript type does not
|
|
3526
|
+
* know about.
|
|
3527
|
+
*/
|
|
3528
|
+
type FormErrorRecord = Record<string, ValidationError[]>;
|
|
3529
|
+
/**
|
|
3530
|
+
* Type of `form.errors`. Leaf-aware drillable callable Proxy. At a
|
|
3531
|
+
* statically-known leaf the proxy resolves to `readonly ValidationError[]`
|
|
3532
|
+
* (empty array when no errors land); at dynamic boundaries (array
|
|
3533
|
+
* indices, record keys, DU variant-only fields) it resolves to
|
|
3534
|
+
* `readonly ValidationError[] | undefined`. At a container path it
|
|
3535
|
+
* returns a sub-proxy you can keep drilling.
|
|
3536
|
+
*
|
|
3537
|
+
* Dot/bracket access mirrors the schema shape:
|
|
3538
|
+
*
|
|
3539
|
+
* ```ts
|
|
3540
|
+
* form.errors.email // readonly ValidationError[] (static leaf)
|
|
3541
|
+
* form.errors.user.profile.email // readonly ValidationError[] (chained static leaves)
|
|
3542
|
+
* form.errors.posts[3]?.title // readonly ValidationError[] | undefined (past array boundary)
|
|
3543
|
+
* form.errors.address // sub-proxy (a container, so descend)
|
|
3544
|
+
* ```
|
|
3545
|
+
*
|
|
3546
|
+
* Callable form for dynamic / programmatic paths:
|
|
3547
|
+
*
|
|
3548
|
+
* ```ts
|
|
3549
|
+
* form.errors('user.profile.email') // dotted-string
|
|
3550
|
+
* form.errors(['user', 'profile', 'email']) // path-array
|
|
3551
|
+
* form.errors() // root proxy
|
|
3552
|
+
* ```
|
|
3553
|
+
*
|
|
3554
|
+
* Single-bracket dotted access (`form.errors['user.profile.email']`) is
|
|
3555
|
+
* intentionally NOT supported: JS object semantics read the dotted string
|
|
3556
|
+
* as one key, which lands on a path that does not exist. Use chained
|
|
3557
|
+
* dot/bracket access, or the callable form.
|
|
3558
|
+
*
|
|
3559
|
+
* The underlying shape mirrors the schema. A statically-known primitive
|
|
3560
|
+
* leaf exposes `readonly ValidationError[]`, always an array and empty
|
|
3561
|
+
* when no errors land; a leaf whose value type itself includes `undefined`
|
|
3562
|
+
* (a DU variant-only field) keeps the `| undefined` branch. A container
|
|
3563
|
+
* exposes a sub-shape to keep drilling, and an array exposes
|
|
3564
|
+
* numeric-indexed sub-shapes where reading an index introduces
|
|
3565
|
+
* `| undefined` through noUncheckedIndexedAccess. The callable signatures
|
|
3566
|
+
* are intersected in, so dot access and a function call coexist on one
|
|
3567
|
+
* identifier.
|
|
3568
|
+
*/
|
|
3569
|
+
type FormErrorsSurface<Form> = ErrorsProxyShape<Form> & {
|
|
3570
|
+
(path: string): readonly ValidationError[];
|
|
3571
|
+
/**
|
|
3572
|
+
* Tuple-segment form. Validated against `FlatPath<Form>` so literal
|
|
3573
|
+
* tuples that don't resolve to a known path fail at the call site.
|
|
3574
|
+
* Dynamic `Path`-typed inputs hit the untyped fallback overload below.
|
|
3575
|
+
*/
|
|
3576
|
+
<const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : SegmentPathRejection<JoinSegments<S>>)): readonly ValidationError[];
|
|
3577
|
+
(segments: ReadonlyArray<string | number>): readonly ValidationError[];
|
|
3578
|
+
/**
|
|
3579
|
+
* No-arg call returns the whole-form error aggregate, the same as
|
|
3580
|
+
* `form.meta.errors` and `form.errors([])`: every field error plus
|
|
3581
|
+
* the root bucket. `form.errors(path)` is uniform at every depth,
|
|
3582
|
+
* including `[]`, so the root is no exception. For the root bucket
|
|
3583
|
+
* alone (a root `.refine()`, a path-less `setErrors`), read
|
|
3584
|
+
* `form.meta.ownErrors`. Always a readonly array; empty when the form
|
|
3585
|
+
* has no errors.
|
|
3586
|
+
*/
|
|
3587
|
+
(): readonly ValidationError[];
|
|
3588
|
+
};
|
|
3589
|
+
/**
|
|
3590
|
+
* Implementation-detail walker backing `form.errors` typed proxy.
|
|
3591
|
+
* A thin alias over `LeafWalker<T, 'errors', false>`, whose topology is
|
|
3592
|
+
* defined once at `LeafWalker` and parameterized through
|
|
3593
|
+
* `LeafSchemeFor`. The `false` preserves optional-key modifiers, since
|
|
3594
|
+
* the errors proxy mirrors the input shape including `?`, where the
|
|
3595
|
+
* fields proxy alias strips them under the default `true`.
|
|
3596
|
+
*
|
|
3597
|
+
* Exported so the bundled `.d.ts` references a single alias body
|
|
3598
|
+
* rather than re-emitting the full union-aware recursion at every
|
|
3599
|
+
* consumer call site that types `form.errors`. Multiple useForm
|
|
3600
|
+
* instances in one scope otherwise compound this into TS2589
|
|
3601
|
+
* territory. Consumers should reach for `FormErrorsSurface` instead.
|
|
3602
|
+
*/
|
|
3603
|
+
type ErrorsProxyShape<T> = LeafWalker<T, 'errors', false>;
|
|
3604
|
+
/**
|
|
3605
|
+
* Type of `form.values`: a drillable readonly callable proxy. Unlike
|
|
3606
|
+
* `form.errors` and `form.fields`, containers here are USEFUL
|
|
3607
|
+
* terminals, because every container in `values` carries real data
|
|
3608
|
+
* where in errors and fields it carries a derivation:
|
|
3609
|
+
* `form.values.address` returns the actual `{ city, ... }` subtree and
|
|
3610
|
+
* keeps drilling.
|
|
3611
|
+
*
|
|
3612
|
+
* ```ts
|
|
3613
|
+
* form.values.email // string (the value)
|
|
3614
|
+
* form.values.address // { city, ... } an object, drillable
|
|
3615
|
+
* form.values.address.city // string (chained descent)
|
|
3616
|
+
* form.values('address.city') // function-call (dynamic / programmatic)
|
|
3617
|
+
* form.values(['address', 'city']) // path-array form
|
|
3618
|
+
* form.values() // the whole form value (root)
|
|
3619
|
+
* ```
|
|
3620
|
+
*
|
|
3621
|
+
* The two shapes answer different questions. Dot access is the
|
|
3622
|
+
* REACTIVE view: per-key tracking, no copying, always the live value.
|
|
3623
|
+
* The call form returns a SNAPSHOT: a detached plain object that keeps
|
|
3624
|
+
* what it held at capture time, so it is the shape to hand an async
|
|
3625
|
+
* call, a serialiser, or a diff.
|
|
3626
|
+
*
|
|
3627
|
+
* ```ts
|
|
3628
|
+
* const before = form.values() // detached; survives later writes
|
|
3629
|
+
* await api.save(before) // cannot mutate underneath the call
|
|
3630
|
+
* form.values.email // reactive read; use this in computed
|
|
3631
|
+
* ```
|
|
3632
|
+
*
|
|
3633
|
+
* Prefer dot access inside a `computed`, `watchEffect`, or template:
|
|
3634
|
+
* it tracks the one key you read, where the call form depends on the
|
|
3635
|
+
* whole form and re-runs the consumer on any change.
|
|
3636
|
+
*
|
|
3637
|
+
* The copy is deep across plain objects and arrays. Non-plain
|
|
3638
|
+
* instances (Map, Set, File, Date) are shared by reference, matching
|
|
3639
|
+
* `JSON.stringify` behaviour; `structuredClone(form.values())` gives a
|
|
3640
|
+
* fully detached copy when one is needed.
|
|
3641
|
+
*
|
|
3642
|
+
* Single-bracket dotted access (`form.values['address.city']`) is
|
|
3643
|
+
* deliberately NOT supported, because JS object semantics read the
|
|
3644
|
+
* dotted string as a single key. Use chained dot or bracket access, or
|
|
3645
|
+
* the callable form.
|
|
3646
|
+
*
|
|
3647
|
+
* The chained shape applies the discriminated-union lift via
|
|
3648
|
+
* `LiftedValueShape<F>` so per-variant keys are reachable without
|
|
3649
|
+
* narrowing first: `form.values.cargo.permitNumber` types as
|
|
3650
|
+
* `string | undefined` whatever the active cargo variant, matching the
|
|
3651
|
+
* runtime, where plain JS object access on a missing variant key
|
|
3652
|
+
* returns `undefined`. The strict-variant shape is still required on
|
|
3653
|
+
* the WRITE side, where `setValue` and `defaultValues` use the unlifted
|
|
3654
|
+
* `WriteShape` so a partial or cross-variant object cannot reach the
|
|
3655
|
+
* form.
|
|
3656
|
+
*/
|
|
3657
|
+
type ValuesSurface<F> = Readonly<LiftedValueShape<F>> & {
|
|
3658
|
+
(path: string): unknown;
|
|
3659
|
+
(path: ReadonlyArray<string | number>): unknown;
|
|
3660
|
+
(): Readonly<F>;
|
|
3661
|
+
};
|
|
3662
|
+
/**
|
|
3663
|
+
* Reactive form-level flags, counters, and aggregates returned as
|
|
3664
|
+
* `form.meta`. "Meta" because every other surface (`form.values`,
|
|
3665
|
+
* `form.errors`, `form.fields`) is data-shaped, where `form.meta`
|
|
3666
|
+
* holds facts derived ABOUT the form.
|
|
3667
|
+
*
|
|
3668
|
+
* Read its fields directly, with no `.value`: they auto-unwrap inside
|
|
3669
|
+
* the reactive object:
|
|
3670
|
+
*
|
|
3671
|
+
* ```vue
|
|
3672
|
+
* <button :disabled="form.meta.submitting">Save</button>
|
|
3673
|
+
* ```
|
|
3674
|
+
*
|
|
3675
|
+
* Watch a single field via the getter form:
|
|
3676
|
+
*
|
|
3677
|
+
* ```ts
|
|
3678
|
+
* watch(() => form.meta.submitting, (value) => …)
|
|
3679
|
+
* ```
|
|
3680
|
+
*
|
|
3681
|
+
* Per-field state (touched, dirty, errors) lives behind
|
|
3682
|
+
* `form.fields.<path>`; this is the aggregate view across the
|
|
3683
|
+
* whole form.
|
|
3684
|
+
*
|
|
3685
|
+
* Read-only at runtime: an assignment is refused with a dev-console
|
|
3686
|
+
* warning and never lands, because Attaform does not throw from a read
|
|
3687
|
+
* surface. Destructuring snapshots the current values, so reach for
|
|
3688
|
+
* `toRefs()` when you need reactive handles to individual fields.
|
|
3689
|
+
*/
|
|
3690
|
+
type FormMeta<F = unknown> = FieldState<F> & {
|
|
3691
|
+
/**
|
|
3692
|
+
* `true` while a `handleSubmit`-produced submit handler is running.
|
|
3693
|
+
* Covers both the validation phase and your async submit callback.
|
|
3694
|
+
* Useful for disabling the submit button.
|
|
3695
|
+
*/
|
|
3696
|
+
readonly submitting: boolean;
|
|
3697
|
+
/**
|
|
3698
|
+
* How many times the submit handler has been invoked, regardless of
|
|
3699
|
+
* outcome (validation failure, callback success, callback throw).
|
|
3700
|
+
* Useful for "show errors after first submit attempt" UX.
|
|
3701
|
+
*/
|
|
3702
|
+
readonly submissionAttempts: number;
|
|
3703
|
+
/**
|
|
3704
|
+
* How many times wizard navigation (`wizard.next`, `wizard.back`,
|
|
3705
|
+
* `wizard.goTo`) has actually departed this form. Bumped on real
|
|
3706
|
+
* departures only: no-ops like `back()` from the first step, a
|
|
3707
|
+
* same-key `goTo`, or a `next()` blocked by failed activation leave
|
|
3708
|
+
* the counter at its prior value.
|
|
3709
|
+
*
|
|
3710
|
+
* A pure introspection counter: it serves "this form has been visited
|
|
3711
|
+
* and left" UX (analytics, prior-step badges) and drives no part of
|
|
3712
|
+
* the display heuristic. Reveal-on-submit runs entirely through
|
|
3713
|
+
* `submissionAttempts`, which `wizard.handleSubmit` bumps on every
|
|
3714
|
+
* form, since it always validates the whole step list.
|
|
3715
|
+
*
|
|
3716
|
+
* `submissionAttempts` counts `handleSubmit` passes alone, so a wizard
|
|
3717
|
+
* departure and a form submission stay separately introspectable.
|
|
3718
|
+
* `form.validate()` is a read-only inspection primitive and bumps no
|
|
3719
|
+
* counter at all.
|
|
3720
|
+
*
|
|
3721
|
+
* Cleared by `form.reset()`.
|
|
3722
|
+
*/
|
|
3723
|
+
readonly departAttempts: number;
|
|
3724
|
+
/**
|
|
3725
|
+
* The error thrown or rejected by the most recent submit callback (or
|
|
3726
|
+
* its `onError` handler), coerced to a real `Error` (a non-`Error`
|
|
3727
|
+
* throw keeps its origin on `.cause`). Cleared to `null` at the start
|
|
3728
|
+
* of each new submission attempt; stays `null` on success.
|
|
3729
|
+
*
|
|
3730
|
+
* The submit handler does NOT re-throw: its returned promise always
|
|
3731
|
+
* resolves, so binding it to `@submit` never manufactures a window
|
|
3732
|
+
* unhandledrejection. This is the channel for an UNEXPECTED submit
|
|
3733
|
+
* failure, a thrown exception or a rejected promise, read the same way
|
|
3734
|
+
* in a template and after an imperative `await submit()`. An EXPECTED
|
|
3735
|
+
* rejection handled the documented way (`setErrors(...)` then
|
|
3736
|
+
* `return`, no throw) surfaces through the error store and `onError`
|
|
3737
|
+
* instead, leaving `submitError` null. Like `hydrateError` it stays
|
|
3738
|
+
* distinct from the curated user-error store, so render it where you
|
|
3739
|
+
* choose:
|
|
3740
|
+
*
|
|
3741
|
+
* ```vue
|
|
3742
|
+
* <p v-if="form.meta.submitError">{{ form.meta.submitError.message }}</p>
|
|
3743
|
+
* ```
|
|
3744
|
+
*/
|
|
3745
|
+
readonly submitError: Error | null;
|
|
3746
|
+
/**
|
|
3747
|
+
* Scalar mirror of `meta.errors.length`. Read it from templates and
|
|
3748
|
+
* `watch()` without indexing the underlying array.
|
|
3749
|
+
*
|
|
3750
|
+
* It tracks `errors.length` exactly, through the same computed graph,
|
|
3751
|
+
* so a `watch(form.meta.errorCount, ...)` fires exactly when the
|
|
3752
|
+
* aggregate error count changes.
|
|
3753
|
+
*/
|
|
3754
|
+
readonly errorCount: number;
|
|
3755
|
+
/**
|
|
3756
|
+
* `true` once a `handleSubmit` callback has resolved without throwing
|
|
3757
|
+
* AND left no errors behind. It is independent of
|
|
3758
|
+
* `submissionAttempts`: a failed submit (a validation failure, a
|
|
3759
|
+
* callback rejection, or a callback calling `setErrors` and returning)
|
|
3760
|
+
* increments attempts and leaves `submitted` false. A template reads
|
|
3761
|
+
* it as "the form has been
|
|
3762
|
+
* submitted successfully at least once."
|
|
3763
|
+
*
|
|
3764
|
+
* The error check is scoped to the user-error layer (`setErrors` /
|
|
3765
|
+
* `clearErrors`): the documented server-rejection pattern
|
|
3766
|
+
* (`setErrors(response.errors); return`) is a failed submit, so it does
|
|
3767
|
+
* not flip `submitted`. A background async refinement that writes a
|
|
3768
|
+
* schema error mid-submit does not retroactively fail it.
|
|
3769
|
+
*
|
|
3770
|
+
* Cleared by `form.reset()` alongside `submissionAttempts` and
|
|
3771
|
+
* `submitError`. For "the user has attempted a submit," read
|
|
3772
|
+
* `submissionAttempts > 0` directly.
|
|
3773
|
+
*/
|
|
3774
|
+
readonly submitted: boolean;
|
|
3775
|
+
/**
|
|
3776
|
+
* Per-`useForm()`-call identity. Stable for the lifetime of one
|
|
3777
|
+
* `useForm()` call; new on every fresh mount. Orthogonal to
|
|
3778
|
+
* `form.key`: the key identifies a SHARED FormStore (so two
|
|
3779
|
+
* `useForm({ key: 'signup' })` calls return the same store and the
|
|
3780
|
+
* same key), while `instanceId` identifies THIS call site, which is
|
|
3781
|
+
* what disambiguates two forms sharing a key, a sidebar and a main
|
|
3782
|
+
* view rendering the same form.
|
|
3783
|
+
*
|
|
3784
|
+
* The format is opaque, derived from Vue 3.5+ `useId()`. Treat it as
|
|
3785
|
+
* identity rather than state: do not parse it, compare it ordinally,
|
|
3786
|
+
* or persist it.
|
|
3787
|
+
*
|
|
3788
|
+
* Common patterns:
|
|
3789
|
+
*
|
|
3790
|
+
* - **Devtools panels** disambiguating shared-key form mounts.
|
|
3791
|
+
* - **Telemetry / logging hooks** tagging events with which mount
|
|
3792
|
+
* triggered them.
|
|
3793
|
+
* - **E2E test selectors** stamping `data-form-id={form.meta.instanceId}`
|
|
3794
|
+
* onto a wrapper to assert which form was focused.
|
|
3795
|
+
* - **Vue `:key`** for a keyed list of dynamically-rendered forms, a
|
|
3796
|
+
* drag-reorder say, since identity is stable per useForm() call.
|
|
3797
|
+
*/
|
|
3798
|
+
readonly instanceId: string;
|
|
3799
|
+
};
|
|
3800
|
+
/**
|
|
3801
|
+
* Read-only view returned by `form.blankPaths.value`. Exposes lookup
|
|
3802
|
+
* (`.has`), aggregate (`.size`), and iteration over the form's
|
|
3803
|
+
* blank-marked paths.
|
|
3804
|
+
*
|
|
3805
|
+
* `.has(input)` and the iterator yield consistent results across both
|
|
3806
|
+
* input conventions the library accepts everywhere a path is named:
|
|
3807
|
+
*
|
|
3808
|
+
* - **Dotted string**: `'user.email'`, matching what `register('user.email')`
|
|
3809
|
+
* or `setValue('items.0.sku', …)` accept. Convenient when no segment
|
|
3810
|
+
* contains a literal dot.
|
|
3811
|
+
* - **Array form**: `['user', 'email']`, mirroring `register(['user', 'email'])`.
|
|
3812
|
+
* Required when a single segment contains literal dots, so
|
|
3813
|
+
* `['address.primary']` for a top-level key named `address.primary`,
|
|
3814
|
+
* which the dotted form would parse as two segments.
|
|
3815
|
+
*
|
|
3816
|
+
* Iteration yields `Path` arrays, so a consumer building debug UI or
|
|
3817
|
+
* persisting the set never has to guess whether a dot in a segment is a
|
|
3818
|
+
* separator or part of the name.
|
|
3819
|
+
*
|
|
3820
|
+
* Mutating the view does nothing. Writes go through
|
|
3821
|
+
* `setValue(path, unset)`, `markBlank()` on a register binding, or the
|
|
3822
|
+
* directive's input listener on a numeric clear.
|
|
3823
|
+
*/
|
|
3824
|
+
interface BlankPathsView {
|
|
3825
|
+
/** Number of blank-marked paths. */
|
|
3826
|
+
readonly size: number;
|
|
3827
|
+
/**
|
|
3828
|
+
* `true` when the path is in the blank set. Accepts dotted-string
|
|
3829
|
+
* form (parsed by [[parseDottedPath]]) or the array form.
|
|
3830
|
+
*/
|
|
3831
|
+
has(input: string | Path): boolean;
|
|
3832
|
+
/** Snapshot of all blank-marked paths as segment arrays. */
|
|
3833
|
+
values(): readonly Path[];
|
|
3834
|
+
/** Iterates the blank-marked paths as segment arrays. */
|
|
3835
|
+
[Symbol.iterator](): IterableIterator<Path>;
|
|
3836
|
+
}
|
|
3837
|
+
/**
|
|
3838
|
+
* The no-arg `form.record()` call form, present only when the form root
|
|
3839
|
+
* is itself an open record (`z.record(K, V)`). `string extends keyof
|
|
3840
|
+
* Form` is the open-keyset probe: true for `Record<string, V>`, false
|
|
3841
|
+
* for a fixed `z.object` shape. On a fixed object this resolves to
|
|
3842
|
+
* `unknown`, which contributes nothing to the intersection in `record`
|
|
3843
|
+
* below, so the no-arg call form simply does not exist there (and
|
|
3844
|
+
* `form.record()` stays a compile error, as it should when the root has
|
|
3845
|
+
* a closed key set). On a record root it mirrors the path-addressed
|
|
3846
|
+
* `record(path)` overload: one `FieldState` per entry, keyed by the
|
|
3847
|
+
* record's own runtime keys.
|
|
3848
|
+
*/
|
|
3849
|
+
type RootRecordView<Form> = string extends keyof Form ? Form extends Record<string, infer RootValue> ? () => Readonly<Record<string, FieldState<RootValue>>> : unknown : unknown;
|
|
3850
|
+
/**
|
|
3851
|
+
* The object returned by `useForm`. Holds every reactive ref, write
|
|
3852
|
+
* helper, and lifecycle method bound to one form.
|
|
3853
|
+
*
|
|
3854
|
+
* ```ts
|
|
3855
|
+
* const form = useForm({ schema })
|
|
3856
|
+
* form.register('email') // bind to <input v-register>
|
|
3857
|
+
* form.values.email // current value (proxy, no .value)
|
|
3858
|
+
* form.fields.email.dirty // per-field flags
|
|
3859
|
+
* form.errors.email // readonly ValidationError[]
|
|
3860
|
+
* form.setValue('email', 'a@b.c')
|
|
3861
|
+
* form.handleSubmit(onSubmit) // returns a submit handler
|
|
3862
|
+
* form.meta.submitting // form-level reactive flag
|
|
3863
|
+
* ```
|
|
3864
|
+
*
|
|
3865
|
+
* Three generic slots split the write view, parse view, and read view:
|
|
3866
|
+
*
|
|
3867
|
+
* - `Form` is the **input / write shape** (`z.input<Schema>`), used by
|
|
3868
|
+
* `setValue`, `defaultValues` and `register`'s write side. Loose: a
|
|
3869
|
+
* preprocess path accepts `unknown` at the write boundary, and a
|
|
3870
|
+
* defaulted field accepts its inner type optionally.
|
|
3871
|
+
*
|
|
3872
|
+
* - `GetValueFormType` is the **output / parsed shape**
|
|
3873
|
+
* (`z.output<Schema>`), used by `handleSubmit`'s `onSubmit` callback
|
|
3874
|
+
* and `form.parse()`'s success payload: the shape after refinements
|
|
3875
|
+
* have fired and transforms have run.
|
|
3876
|
+
*
|
|
3877
|
+
* - `ReadForm` is the **read / storage shape**, used by `values`,
|
|
3878
|
+
* `fields`, `register`'s read side and `toRef`. Precise per key: at a
|
|
3879
|
+
* write-boundary wrapper (`default`, `prefault`, `catch`, `readonly`,
|
|
3880
|
+
* `preprocess`) the value is `z.output<Inner>`, because the default
|
|
3881
|
+
* has fired and preprocess has normalized; at a transform or pipe it
|
|
3882
|
+
* stays `z.input<Inner>`, because transforms are deferred until
|
|
3883
|
+
* parse. A schema-agnostic call site defaults it to `Form`.
|
|
3884
|
+
*
|
|
3885
|
+
* For schemas without write-boundary wrappers or transforms the three
|
|
3886
|
+
* shapes coincide.
|
|
3887
|
+
*/
|
|
3888
|
+
type UseFormReturnType<Form extends GenericForm, GetValueFormType extends GenericForm = Form, ReadForm extends GenericForm = Form, K extends FormKey = FormKey> = {
|
|
3889
|
+
/**
|
|
3890
|
+
* Wraps your submit logic with validation and error routing.
|
|
3891
|
+
*
|
|
3892
|
+
* ```ts
|
|
3893
|
+
* const onSubmit = form.handleSubmit(
|
|
3894
|
+
* (data) => api.signup(data),
|
|
3895
|
+
* (errors) => console.log(errors),
|
|
3896
|
+
* )
|
|
3897
|
+
* // Bind the returned handler: <form @submit="onSubmit">
|
|
3898
|
+
* ```
|
|
3899
|
+
*
|
|
3900
|
+
* `data` is the strictly-typed parsed value: refinements have fired
|
|
3901
|
+
* and `.transform()`s have run, so the payload matches
|
|
3902
|
+
* `z.output<Schema>`. Where the input type differs from the output
|
|
3903
|
+
* type, as in `z.string().transform(v => v.length > 10)`, `data` is
|
|
3904
|
+
* the output shape while `form.values` stays the input shape.
|
|
3905
|
+
*/
|
|
3906
|
+
handleSubmit: HandleSubmit<GetValueFormType>;
|
|
3907
|
+
/**
|
|
3908
|
+
* Reactive readonly proxy over the form's storage value, read
|
|
3909
|
+
* identically in script and template: no `.value`, no auto-unwrap
|
|
3910
|
+
* rules, the Pinia setup-store pattern.
|
|
3911
|
+
*
|
|
3912
|
+
* ```vue
|
|
3913
|
+
* <script setup>
|
|
3914
|
+
* const form = useForm({ schema, key: 'login' })
|
|
3915
|
+
* </script>
|
|
3916
|
+
*
|
|
3917
|
+
* <template>
|
|
3918
|
+
* <p>{{ form.values.email }}</p>
|
|
3919
|
+
* <p>{{ form.values.address.city }}</p>
|
|
3920
|
+
* </template>
|
|
3921
|
+
* ```
|
|
3922
|
+
*
|
|
3923
|
+
* Writes are blocked at the proxy boundary: go through `setValue`,
|
|
3924
|
+
* the directive, or a field-array helper. The slim-primitive write
|
|
3925
|
+
* gate is the only path into storage.
|
|
3926
|
+
*
|
|
3927
|
+
* Reads reflect what's storable: enum-typed slots widen to their
|
|
3928
|
+
* primitive supertype (`string`), so refinement-invalid but
|
|
3929
|
+
* structurally-valid values are visible. Storage holds the
|
|
3930
|
+
* `z.input<Schema>` shape and `.transform()`s have NOT run, so on a
|
|
3931
|
+
* `z.string().transform(v => v.length > 10)` the value reads as
|
|
3932
|
+
* `string`, not `boolean`. Reach for `handleSubmit` or `form.parse()`
|
|
3933
|
+
* when you need the post-transform output shape.
|
|
3934
|
+
*/
|
|
3935
|
+
values: ValuesSurface<WriteShape<ReadForm>>;
|
|
3936
|
+
/**
|
|
3937
|
+
* Reactive per-field state proxy, a Pinia-style nested object: read
|
|
3938
|
+
* leaf properties (`value`, `dirty`, `touched`, `errors`, `blurred`,
|
|
3939
|
+
* `focused`, `blank`) directly off the field's path:
|
|
3940
|
+
*
|
|
3941
|
+
* ```vue
|
|
3942
|
+
* <p v-if="form.fields.email.touched && form.fields.email.errors.length">
|
|
3943
|
+
* {{ form.fields.email.errors[0].message }}
|
|
3944
|
+
* </p>
|
|
3945
|
+
* <p>City dirty? {{ form.fields.address.city.dirty }}</p>
|
|
3946
|
+
* ```
|
|
3947
|
+
*
|
|
3948
|
+
* The same proxy descends at every level: `address` reads the
|
|
3949
|
+
* FieldState for the address object, and `address.city` descends into
|
|
3950
|
+
* the nested leaf.
|
|
3951
|
+
*
|
|
3952
|
+
* Leaf values follow the slim WriteShape contract: enum-typed leaves
|
|
3953
|
+
* widen to their primitive supertype, and the leaf value reflects
|
|
3954
|
+
* the `z.input<Schema>` shape (transforms deferred until parse).
|
|
3955
|
+
* The errors array, dirty flag, focus state, etc. are unaffected.
|
|
3956
|
+
*
|
|
3957
|
+
* Shadowing: at depth 2+, FieldState keys (`dirty`, `touched`,
|
|
3958
|
+
* `errors`, `blank`, `focused`, `blurred`, `value`,
|
|
3959
|
+
* `original`, `pristine`, `connected`, `updatedAt`, `path`) win
|
|
3960
|
+
* over schema field names. Top-level fields are NOT shadowed.
|
|
3961
|
+
* Document edge case; rename the offending schema field if the
|
|
3962
|
+
* collision matters.
|
|
3963
|
+
*/
|
|
3964
|
+
fields: FieldStateMap<WriteShape<ReadForm>>;
|
|
3965
|
+
/**
|
|
3966
|
+
* Write to the form programmatically. Two forms:
|
|
3967
|
+
*
|
|
3968
|
+
* - `setValue(value)` replaces the whole form.
|
|
3969
|
+
* - `setValue(path, value)` writes at one path.
|
|
3970
|
+
*
|
|
3971
|
+
* Either takes a callback in place of `value` to derive the next
|
|
3972
|
+
* value from the previous one:
|
|
3973
|
+
*
|
|
3974
|
+
* ```ts
|
|
3975
|
+
* form.setValue('count', (prev) => prev + 1)
|
|
3976
|
+
* form.setValue((prev) => ({ ...prev, name: 'Ada' }))
|
|
3977
|
+
* ```
|
|
3978
|
+
*
|
|
3979
|
+
* Returns `true` when the write is accepted. A `false` means the
|
|
3980
|
+
* value did not match the slot's expected type, as writing a number
|
|
3981
|
+
* to a string field does not, and form state is unchanged. A
|
|
3982
|
+
* refinement-level mismatch (an out-of-enum value, a failing format
|
|
3983
|
+
* check) DOES succeed and surfaces as a field error instead.
|
|
3984
|
+
*/
|
|
3985
|
+
setValue: {
|
|
3986
|
+
/**
|
|
3987
|
+
* Replace the whole form. Pass a value or a callback receiving
|
|
3988
|
+
* the previous form.
|
|
3989
|
+
*
|
|
3990
|
+
* ```ts
|
|
3991
|
+
* form.setValue({ name: 'Ada', email: 'a@b.c' })
|
|
3992
|
+
* form.setValue((prev) => ({ ...prev, name: 'Ada' }))
|
|
3993
|
+
* ```
|
|
3994
|
+
*
|
|
3995
|
+
* Returns `true` when the write was accepted, `false` when the
|
|
3996
|
+
* value didn't match the expected shape (e.g. wrong primitive
|
|
3997
|
+
* type at a leaf). Refinement-level mismatches (out-of-enum
|
|
3998
|
+
* values, failing format checks, etc.) succeed and surface as
|
|
3999
|
+
* field errors instead.
|
|
4000
|
+
*/
|
|
4001
|
+
<Value extends SetValuePayload<DefaultValuesShape<Form>, WriteShape<Form>>>(value: Value): boolean;
|
|
4002
|
+
/**
|
|
4003
|
+
* Write at a specific path. Pass a value or a callback receiving
|
|
4004
|
+
* the previous value at that path.
|
|
4005
|
+
*
|
|
4006
|
+
* ```ts
|
|
4007
|
+
* form.setValue('email', 'a@b.c')
|
|
4008
|
+
* form.setValue('count', (prev) => prev + 1)
|
|
4009
|
+
* form.setValue('income', unset) // numeric leaf marked displayed-empty
|
|
4010
|
+
* ```
|
|
4011
|
+
*
|
|
4012
|
+
* Returns `true` when the write was accepted, `false` when the
|
|
4013
|
+
* value didn't match the slot's expected primitive type.
|
|
4014
|
+
* Refinement-level mismatches succeed and surface as field
|
|
4015
|
+
* errors. Pass the `unset` symbol at any primitive leaf to mark
|
|
4016
|
+
* it blank (storage holds the slim default; UI displays
|
|
4017
|
+
* empty; submit raises "No value supplied" for required schemas).
|
|
4018
|
+
*/
|
|
4019
|
+
<Path extends FlatPath<Form>, Value extends PathSetValuePayload<NestedType<Form, Path>>>(path: Path, value: Value): boolean;
|
|
4020
|
+
/**
|
|
4021
|
+
* Tuple-segment form, equivalent to the dotted-string overload and
|
|
4022
|
+
* the one to reach for when a path is built from variables or
|
|
4023
|
+
* arrays: `form.setValue([prefix, 'line1'], 'value')`. The resolved
|
|
4024
|
+
* leaf type is exact, matching the dotted-string form.
|
|
4025
|
+
*/
|
|
4026
|
+
<const S extends ReadonlyArray<string | number>, Value extends PathSetValuePayload<NestedType<Form, JoinSegments<S>>>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : SegmentPathRejection<JoinSegments<S>>), value: Value): boolean;
|
|
4027
|
+
};
|
|
4028
|
+
/**
|
|
4029
|
+
* Reactive validation status. Re-runs whenever the form (or the
|
|
4030
|
+
* subtree at `path`) mutates. The returned ref carries a `pending`
|
|
4031
|
+
* flag, so gate on `!status.value.pending` before reading `success`
|
|
4032
|
+
* or `errors`.
|
|
4033
|
+
*
|
|
4034
|
+
* ```ts
|
|
4035
|
+
* const status = form.validate()
|
|
4036
|
+
* watchEffect(() => {
|
|
4037
|
+
* if (status.value.pending) return
|
|
4038
|
+
* if (!status.value.success) console.log(status.value.errors)
|
|
4039
|
+
* })
|
|
4040
|
+
* ```
|
|
4041
|
+
*
|
|
4042
|
+
* A stale in-flight run is dropped automatically: the ref settles
|
|
4043
|
+
* only to results from the most recent call.
|
|
4044
|
+
*/
|
|
4045
|
+
validate: (path?: FlatPath<Form>) => Readonly<Ref<ReactiveValidationStatus<Form>>>;
|
|
4046
|
+
/**
|
|
4047
|
+
* Resolve once every in-flight async `register({ transforms })` run
|
|
4048
|
+
* has settled, either globally or, with `path`, at or under it.
|
|
4049
|
+
* Resolve-never-reject: a transform that throws still settles the
|
|
4050
|
+
* field (its failure lands on `field.transformError`), so the returned
|
|
4051
|
+
* promise always resolves.
|
|
4052
|
+
*
|
|
4053
|
+
* `handleSubmit` awaits this internally before parsing, so a submit
|
|
4054
|
+
* fired the instant after an async transform still validates the
|
|
4055
|
+
* resolved value. Reach for it directly when you need the same
|
|
4056
|
+
* guarantee outside submit, before reading `form.values` in an
|
|
4057
|
+
* imperative flow or a test:
|
|
4058
|
+
*
|
|
4059
|
+
* ```ts
|
|
4060
|
+
* input.value = ' a@b.com '
|
|
4061
|
+
* await form.settleTransforms('email')
|
|
4062
|
+
* // form.values.email is now the normalized value
|
|
4063
|
+
* ```
|
|
4064
|
+
*/
|
|
4065
|
+
settleTransforms: (path?: FlatPath<Form>) => Promise<void>;
|
|
4066
|
+
/**
|
|
4067
|
+
* Imperative one-shot parse. It runs the full pipeline, refinements,
|
|
4068
|
+
* `.transform()`s and blank-required composition, against the current
|
|
4069
|
+
* form snapshot and RETAINS the parsed data.
|
|
4070
|
+
*
|
|
4071
|
+
* Storage holds the honest input view: the values you wrote, with
|
|
4072
|
+
* preprocess normalization applied and `.transform()` deferred. Where
|
|
4073
|
+
* the input type differs from the output type, as on a
|
|
4074
|
+
* `z.string().transform(v => v.length > 10)`, `form.values.X` is the
|
|
4075
|
+
* input shape and `(await form.parse()).data?.X` the output shape.
|
|
4076
|
+
*
|
|
4077
|
+
* ```ts
|
|
4078
|
+
* const result = await form.parse()
|
|
4079
|
+
* if (result.success) {
|
|
4080
|
+
* // result.data matches z.output<typeof schema>
|
|
4081
|
+
* } else {
|
|
4082
|
+
* // result.errors is the validation failure list
|
|
4083
|
+
* }
|
|
4084
|
+
* ```
|
|
4085
|
+
*
|
|
4086
|
+
* By default the call is a PURE read: nothing is written to
|
|
4087
|
+
* `form.errors`, and in-flight per-field validation runs are left
|
|
4088
|
+
* alone. Pass `{ commit: true }` to make the run authoritative: the
|
|
4089
|
+
* verdict is committed to the error store at the parsed scope and any
|
|
4090
|
+
* in-flight per-field runs are cancelled first, mirroring
|
|
4091
|
+
* `handleSubmit`, so `await form.parse('email', { commit: true })`
|
|
4092
|
+
* lands a deterministic view of `form.errors.email`:
|
|
4093
|
+
*
|
|
4094
|
+
* ```ts
|
|
4095
|
+
* const result = await form.parse({ commit: true })
|
|
4096
|
+
* if (!result.success) showErrors(result.errors)
|
|
4097
|
+
* ```
|
|
4098
|
+
*
|
|
4099
|
+
* Always async, and there is no synchronous variant by design: a
|
|
4100
|
+
* schema can carry async refinements or transforms, so a sync parse
|
|
4101
|
+
* would silently miss them the moment one is added. One always-
|
|
4102
|
+
* awaited `parse` closes that category of bug entirely. The returned
|
|
4103
|
+
* promise never rejects, since a thrown adapter lands as a
|
|
4104
|
+
* `success: false` response. Pass a path to parse a subtree only;
|
|
4105
|
+
* `meta.validating` flips `true` while the promise is in flight.
|
|
4106
|
+
*/
|
|
4107
|
+
parse: {
|
|
4108
|
+
(path?: FlatPath<Form>, options?: ParseOptions): Promise<ValidationResponse<GetValueFormType>>;
|
|
4109
|
+
(options: ParseOptions): Promise<ValidationResponse<GetValueFormType>>;
|
|
4110
|
+
};
|
|
4111
|
+
/**
|
|
4112
|
+
* Bind a path to a native input via `v-register`. Returns a
|
|
4113
|
+
* `RegisterValue` carrying the live ref and event handlers the
|
|
4114
|
+
* directive needs.
|
|
4115
|
+
*
|
|
4116
|
+
* ```vue
|
|
4117
|
+
* <input v-register="form.register('email')" />
|
|
4118
|
+
* <input v-register="form.register('username', { transforms: [trim] })" />
|
|
4119
|
+
* ```
|
|
4120
|
+
*
|
|
4121
|
+
* It also accepts a segment-array form for a caller building paths
|
|
4122
|
+
* dynamically, above all inside a `v-for` over a prefix variable,
|
|
4123
|
+
* where dotted-string concatenation widens the prefix's literal union
|
|
4124
|
+
* to plain `string`:
|
|
4125
|
+
*
|
|
4126
|
+
* ```vue
|
|
4127
|
+
* <fieldset v-for="block in [{ prefix: 'pickup' }, { prefix: 'delivery' }] as const">
|
|
4128
|
+
* <input v-register="form.register([block.prefix, 'line1'])" />
|
|
4129
|
+
* </fieldset>
|
|
4130
|
+
* ```
|
|
4131
|
+
*
|
|
4132
|
+
* Pass `options.transforms` to run a sync normalisation pipeline over
|
|
4133
|
+
* user-typed values before they reach form state.
|
|
4134
|
+
*/
|
|
4135
|
+
register: {
|
|
4136
|
+
<Path extends RegisterFlatPath<Form>>(path: Path, options?: RegisterOptions): RegisterValue<NestedReadType<WriteShape<ReadForm>, Path>>;
|
|
4137
|
+
<const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [RegisterFlatPath<Form>] ? unknown : SegmentRegisterRejection<JoinSegments<S>>), options?: RegisterOptions): RegisterValue<NestedReadType<WriteShape<ReadForm>, JoinSegments<S>>>;
|
|
4138
|
+
};
|
|
4139
|
+
/**
|
|
4140
|
+
* The form's identifier: the explicit `key` passed to `useForm`, or
|
|
4141
|
+
* an auto-generated unique id when `key` was omitted. Every
|
|
4142
|
+
* `ValidationError` this form produces carries it as `formKey`, so a
|
|
4143
|
+
* shared error list routes back to the right form.
|
|
4144
|
+
*
|
|
4145
|
+
* Typed as the literal `K` when an explicit `key` was passed, and as
|
|
4146
|
+
* `FormKey` when it was omitted.
|
|
4147
|
+
*/
|
|
4148
|
+
key: K;
|
|
4149
|
+
/**
|
|
4150
|
+
* `true` while a function-form `defaultValues` factory is in flight,
|
|
4151
|
+
* between `useForm` construction and the moment the factory resolves:
|
|
4152
|
+
* the next microtask for a sync function, the promise settling for an
|
|
4153
|
+
* async one. `false` otherwise, a plain `defaultValues` included.
|
|
4154
|
+
*
|
|
4155
|
+
* The form is fully usable while `hydrating` is `true`, holding the
|
|
4156
|
+
* schema's slim defaults. The flag exists so a template can show a
|
|
4157
|
+
* spinner or dim the form while real data loads:
|
|
4158
|
+
*
|
|
4159
|
+
* ```vue
|
|
4160
|
+
* <div :aria-busy="form.hydrating">…</div>
|
|
4161
|
+
* ```
|
|
4162
|
+
*
|
|
4163
|
+
* Exposed as an auto-unwrapping `boolean` with no `.value`.
|
|
4164
|
+
* Reactivity survives through a getter that tracks the underlying ref
|
|
4165
|
+
* at the access site, so `watch(() => form.hydrating, ...)` and a
|
|
4166
|
+
* template read both fire on change. Reading it activates the form's
|
|
4167
|
+
* factory under the lazy-by-default rule.
|
|
4168
|
+
*/
|
|
4169
|
+
readonly hydrating: boolean;
|
|
4170
|
+
/**
|
|
4171
|
+
* The error from the most recent function-form `defaultValues` factory,
|
|
4172
|
+
* normalized to a `ValidationError` (code `atta:hydration-failed`) so the
|
|
4173
|
+
* shape matches every other surface in `form.errors` / `form.meta.errors`.
|
|
4174
|
+
* `null` on construction, on successful resolution, and whenever no
|
|
4175
|
+
* factory has fired. Updates with each `form.rehydrate()` call.
|
|
4176
|
+
*
|
|
4177
|
+
* Distinct from `meta.submitError` so retry buttons and recovery UX can
|
|
4178
|
+
* stay focused on the load-time failure without entangling the submit
|
|
4179
|
+
* pipeline. Read directly in templates and script (no `.value`);
|
|
4180
|
+
* reactivity is preserved via a getter:
|
|
4181
|
+
*
|
|
4182
|
+
* ```vue
|
|
4183
|
+
* <p v-if="form.hydrateError">{{ form.hydrateError.message }}</p>
|
|
4184
|
+
* ```
|
|
4185
|
+
*/
|
|
4186
|
+
readonly hydrateError: ValidationError | null;
|
|
4187
|
+
/**
|
|
4188
|
+
* `true` once the form's defaults have been applied, whether from a
|
|
4189
|
+
* plain `defaultValues` at construction or an async factory that
|
|
4190
|
+
* settled successfully. It stays `false` for a dormant lazy form
|
|
4191
|
+
* whose factory has not activated, and for a failed activation, which
|
|
4192
|
+
* sets `hydrateError`. Once `true` it stays `true` through refetches
|
|
4193
|
+
* so stale-while-revalidate UIs can keep rendering the prior values
|
|
4194
|
+
* while a `rehydrate()` is in flight.
|
|
4195
|
+
*
|
|
4196
|
+
* Composes with `hydrating` and `hydrateError`:
|
|
4197
|
+
*
|
|
4198
|
+
* ```vue
|
|
4199
|
+
* <Spinner v-if="!form.ready && form.hydrating" />
|
|
4200
|
+
* <ErrorBanner v-if="!form.ready && form.hydrateError" :error="form.hydrateError" />
|
|
4201
|
+
* <form v-if="form.ready">…</form>
|
|
4202
|
+
* ```
|
|
4203
|
+
*
|
|
4204
|
+
* Exposed as a reactive `boolean` with no `.value`. Reading it
|
|
4205
|
+
* activates the factory under the lazy-by-default rule, since
|
|
4206
|
+
* observing readiness implies use.
|
|
4207
|
+
*/
|
|
4208
|
+
readonly ready: boolean;
|
|
4209
|
+
/**
|
|
4210
|
+
* Re-fire the captured `defaultValues` factory and re-apply its
|
|
4211
|
+
* payload over the current form values. Useful when the upstream
|
|
4212
|
+
* source changes (the user picks a different draft, a background
|
|
4213
|
+
* sync indicates fresh server data, etc.).
|
|
4214
|
+
*
|
|
4215
|
+
* Resolves after `hydrating` flips back to `false`. Throws
|
|
4216
|
+
* synchronously when the form was constructed with a plain-value
|
|
4217
|
+
* `defaultValues` (nothing to re-fire).
|
|
4218
|
+
*
|
|
4219
|
+
* The payload becomes the form's defaults, the same way a
|
|
4220
|
+
* `reset(next)` argument does, so `form.reset()` afterwards lands on
|
|
4221
|
+
* the values just fetched. Does NOT clear touched / submit state;
|
|
4222
|
+
* chain `form.reset()` for that.
|
|
4223
|
+
*/
|
|
4224
|
+
rehydrate(): Promise<void>;
|
|
4225
|
+
/**
|
|
4226
|
+
* Idempotent activation. Forms are lazy-by-default: a function-form
|
|
4227
|
+
* `defaultValues` factory fires on the first reactive interaction
|
|
4228
|
+
* (reading `form.values`, calling `form.setValue`, etc.). Call
|
|
4229
|
+
* `form.activate()` to kick the factory explicitly, usually from
|
|
4230
|
+
* `setup`, so SSR's `onServerPrefetch` hook awaits the resolution
|
|
4231
|
+
* before the page renders. A later call returns the in-flight promise
|
|
4232
|
+
* until the factory settles, and resolves immediately after. A
|
|
4233
|
+
* plain-value form, having captured no factory, always returns a
|
|
4234
|
+
* resolved promise.
|
|
4235
|
+
*/
|
|
4236
|
+
activate(): Promise<void>;
|
|
4237
|
+
/**
|
|
4238
|
+
* Reactive map of field errors, keyed by dotted path. Populated
|
|
4239
|
+
* automatically by `handleSubmit` and per-field validation; cleared
|
|
4240
|
+
* on validation success.
|
|
4241
|
+
*
|
|
4242
|
+
* Read in templates with no `.value`:
|
|
4243
|
+
*
|
|
4244
|
+
* ```vue
|
|
4245
|
+
* <p v-if="form.errors.email">{{ form.errors.email[0].message }}</p>
|
|
4246
|
+
* ```
|
|
4247
|
+
*
|
|
4248
|
+
* Watch from script via the getter form:
|
|
4249
|
+
*
|
|
4250
|
+
* ```ts
|
|
4251
|
+
* watch(() => form.errors.email, (errors) => …)
|
|
4252
|
+
* ```
|
|
4253
|
+
*
|
|
4254
|
+
* Use bracket access for a nested dotted key
|
|
4255
|
+
* (`form.errors['user.profile.email']`), since JS dot notation splits
|
|
4256
|
+
* on a literal dot.
|
|
4257
|
+
*
|
|
4258
|
+
* Read-only: populate it through `setErrors` and `clearErrors`. A
|
|
4259
|
+
* server response already emitting `ValidationError[]` pipes straight
|
|
4260
|
+
* into `setErrors` with no adapter.
|
|
4261
|
+
*/
|
|
4262
|
+
errors: FormErrorsSurface<Form>;
|
|
4263
|
+
/**
|
|
4264
|
+
* The escape hatch for the rare case a consumer needs a `Ref<T>`:
|
|
4265
|
+
* handing the value to an external composable that expects a Vue ref,
|
|
4266
|
+
* or watching a single path with `watch(formRef, ...)`.
|
|
4267
|
+
*
|
|
4268
|
+
* ```ts
|
|
4269
|
+
* const emailRef = form.toRef('email') // Readonly<Ref<string>>
|
|
4270
|
+
* watch(emailRef, (next) => console.log(next))
|
|
4271
|
+
* ```
|
|
4272
|
+
*
|
|
4273
|
+
* Returns `Readonly<Ref<...>>`: writes go through `setValue`,
|
|
4274
|
+
* `register()` or a field-array helper, never the ref. Prefer
|
|
4275
|
+
* `form.values.email` for a direct read in a template or script, and
|
|
4276
|
+
* keep `toRef` for ref-shaped interop.
|
|
4277
|
+
*/
|
|
4278
|
+
toRef: {
|
|
4279
|
+
<Path extends FlatPath<Form>>(path: Path): Readonly<Ref<NestedReadType<WriteShape<ReadForm>, Path>>>;
|
|
4280
|
+
<const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : SegmentPathRejection<JoinSegments<S>>)): Readonly<Ref<NestedReadType<WriteShape<ReadForm>, JoinSegments<S>>>>;
|
|
4281
|
+
};
|
|
4282
|
+
/**
|
|
4283
|
+
* Set the form's manual error layer. One surface for server-side
|
|
4284
|
+
* errors, optimistic-UI errors, and form-level banners: a field error
|
|
4285
|
+
* and a global (form-level) error are the same thing at different
|
|
4286
|
+
* paths, so there is no separate field/form split.
|
|
4287
|
+
*
|
|
4288
|
+
* Input is lenient ({@link ErrorInput}): a real `Error`, a partial
|
|
4289
|
+
* `{ message?, path?, code?, data? }`, or an array of either. The form
|
|
4290
|
+
* stamps its own `formKey`, defaults a missing `code` to
|
|
4291
|
+
* `atta:user-error`, and coerces a missing or empty `message` to
|
|
4292
|
+
* `"Unknown error"` instead of throwing. A server that already emits
|
|
4293
|
+
* `ValidationError[]` satisfies the input shape directly, so
|
|
4294
|
+
* `form.setErrors(response.errors)` needs no adapter.
|
|
4295
|
+
*
|
|
4296
|
+
* Three call forms, mirroring `setValue`:
|
|
4297
|
+
*
|
|
4298
|
+
* ```ts
|
|
4299
|
+
* // Whole-layer replace. An entry with no `path` is a global,
|
|
4300
|
+
* // form-level error (path `[]`); add a `path` to target a field.
|
|
4301
|
+
* form.setErrors([{ path: ['email'], message: 'Already taken' }])
|
|
4302
|
+
* form.setErrors({ message: 'Capacity exceeded' }) // global banner
|
|
4303
|
+
* form.setErrors(new Error('Network unreachable')) // message coerced
|
|
4304
|
+
*
|
|
4305
|
+
* // Functional update. `prev` is the current manual layer, flat.
|
|
4306
|
+
* form.setErrors((prev) => [...prev, { message: 'And one more' }])
|
|
4307
|
+
*
|
|
4308
|
+
* // Path-scoped. The path is stamped onto every entry, and only that
|
|
4309
|
+
* // path's bucket is replaced. `prev` is that path's manual errors.
|
|
4310
|
+
* form.setErrors('email', [{ message: 'Already taken' }])
|
|
4311
|
+
* form.setErrors(['profile', 'handle'], { message: 'Reserved' })
|
|
4312
|
+
* form.setErrors('email', (prev) => prev.slice(0, 1))
|
|
4313
|
+
* ```
|
|
4314
|
+
*
|
|
4315
|
+
* Replaces only the manual layer; schema/validation errors live in a
|
|
4316
|
+
* separate store and merge on read. Pass `[]` to clear the manual
|
|
4317
|
+
* layer (or use `clearErrors`, which also clears the schema layer).
|
|
4318
|
+
*/
|
|
4319
|
+
setErrors: {
|
|
4320
|
+
(update: (prev: ValidationError[]) => ErrorInput | ErrorInput[]): void;
|
|
4321
|
+
(errors: ErrorInput | ErrorInput[]): void;
|
|
4322
|
+
(path: string | (string | number)[], errors: ErrorInput | ErrorInput[] | ((prev: ValidationError[]) => ErrorInput | ErrorInput[])): void;
|
|
4323
|
+
};
|
|
4324
|
+
/**
|
|
4325
|
+
* Clear errors at one path, or everywhere. Clears BOTH the manual
|
|
4326
|
+
* layer (set through `setErrors`) and the schema/validation layer at
|
|
4327
|
+
* the target. With always-on validation the schema half re-populates
|
|
4328
|
+
* on the next mutation if the value is still invalid, so the cleared
|
|
4329
|
+
* state is short-lived for a field that is still wrong.
|
|
4330
|
+
*
|
|
4331
|
+
* ```ts
|
|
4332
|
+
* form.clearErrors('email') // one field
|
|
4333
|
+
* form.clearErrors([]) // the global, form-level bucket
|
|
4334
|
+
* form.clearErrors() // every error on the form
|
|
4335
|
+
* ```
|
|
4336
|
+
*/
|
|
4337
|
+
clearErrors: (path?: string | (string | number)[]) => void;
|
|
4338
|
+
/**
|
|
4339
|
+
* Form-level reactive flags, counters, and aggregates (`dirty`,
|
|
4340
|
+
* `valid`, `submitting`, `submissionAttempts`, and the flat `errors`
|
|
4341
|
+
* array). See `FormMeta` for the full shape. Read leaves directly
|
|
4342
|
+
* with no `.value`.
|
|
4343
|
+
*
|
|
4344
|
+
* For per-field state (touched, focused, blurred, errors at one
|
|
4345
|
+
* path), use `form.fields.<path>` instead. Undo/redo state lives at
|
|
4346
|
+
* `form.history` (see `FormHistoryNamespace`).
|
|
4347
|
+
*/
|
|
4348
|
+
meta: FormMeta<Form>;
|
|
4349
|
+
/**
|
|
4350
|
+
* Restore the form to its defaults. Without arguments, re-applies
|
|
4351
|
+
* whatever the defaults currently are: `useForm({ defaultValues })`
|
|
4352
|
+
* on a fresh form, or the last values a `reset(next)` or an async
|
|
4353
|
+
* `defaultValues` factory settled on.
|
|
4354
|
+
*
|
|
4355
|
+
* Resets:
|
|
4356
|
+
* - the form value back to defaults;
|
|
4357
|
+
* - the dirty baseline (so the next edit flips `dirty` correctly);
|
|
4358
|
+
* - field errors;
|
|
4359
|
+
* - touched / focused / blurred per-field flags;
|
|
4360
|
+
* - submission state (`submitting` / `submissionAttempts` /
|
|
4361
|
+
* `submitted` / `submitError`).
|
|
4362
|
+
*
|
|
4363
|
+
* `nextDefaultValues` RE-SEATS the defaults rather than applying for
|
|
4364
|
+
* one call, so the form has a single set of defaults that `reset()`,
|
|
4365
|
+
* `resetField(path)`, and `dirty` all read. That is what makes the
|
|
4366
|
+
* save-then-discard shape work: reset from the resource the server
|
|
4367
|
+
* returned, and a later Discard returns to the save instead of
|
|
4368
|
+
* rolling back across it.
|
|
4369
|
+
*
|
|
4370
|
+
* ```ts
|
|
4371
|
+
* const saved = await api.save(form.values())
|
|
4372
|
+
* form.reset(toValues(saved)) // the saved resource is now the baseline
|
|
4373
|
+
* // …later…
|
|
4374
|
+
* form.reset() // back to the saved resource
|
|
4375
|
+
* ```
|
|
4376
|
+
*
|
|
4377
|
+
* The argument is sparse: it folds over the defaults already in
|
|
4378
|
+
* force, so paths it does not name keep the value they had, and
|
|
4379
|
+
* `reset({})` changes nothing. Arrays are replaced wholesale rather
|
|
4380
|
+
* than merged element-wise. Pass `unset` at a path to withdraw its
|
|
4381
|
+
* value and mark it blank.
|
|
4382
|
+
*/
|
|
4383
|
+
reset: (nextDefaultValues?: DefaultValuesInput<Form>) => void;
|
|
4384
|
+
/**
|
|
4385
|
+
* Restore a single field (or a sub-tree like `'user'`) to its
|
|
4386
|
+
* initial value. Clears errors and touched flags for the field
|
|
4387
|
+
* and its descendants; leaves siblings and submission state alone.
|
|
4388
|
+
*
|
|
4389
|
+
* No-op when the path doesn't exist on the form (e.g. a typo'd
|
|
4390
|
+
* dynamic key).
|
|
4391
|
+
*/
|
|
4392
|
+
resetField: (path: FlatPath<Form>) => void;
|
|
4393
|
+
/**
|
|
4394
|
+
* Wipe a field, or the whole form, to the appropriate nullish value
|
|
4395
|
+
* for its declared type: the underlying type's empty or falsy
|
|
4396
|
+
* concrete, with any `.default(x)` wrapper DELIBERATELY skipped.
|
|
4397
|
+
* Orthogonal to `reset` and `resetField` by design.
|
|
4398
|
+
*
|
|
4399
|
+
* ```ts
|
|
4400
|
+
* const schema = z.object({
|
|
4401
|
+
* notify: z.boolean().default(true),
|
|
4402
|
+
* count: z.number().default(5),
|
|
4403
|
+
* })
|
|
4404
|
+
* const form = useForm({ schema })
|
|
4405
|
+
*
|
|
4406
|
+
* form.reset() // notify → true, count → 5 (defaults)
|
|
4407
|
+
* form.clear() // notify → false, count → 0 (falsy-for-type)
|
|
4408
|
+
* form.clear('notify') // → false (NOT the declared default true)
|
|
4409
|
+
* ```
|
|
4410
|
+
*
|
|
4411
|
+
* Per-wrapper semantics:
|
|
4412
|
+
*
|
|
4413
|
+
* - `.default(x)`, `.prefault(x)` and `.catch(x)` give the inner
|
|
4414
|
+
* schema's empty; the default is DELIBERATELY skipped.
|
|
4415
|
+
* - `.optional()` gives `undefined`, the wrapper's absent marker.
|
|
4416
|
+
* - `.nullable()` gives `null`, its explicit empty.
|
|
4417
|
+
* - An object recurses, so every property gets its own empty.
|
|
4418
|
+
* - An array, Set or Record gives the empty one.
|
|
4419
|
+
*
|
|
4420
|
+
* Returns `true` when the write was accepted and `false` when the
|
|
4421
|
+
* adapter could not resolve an empty value at the path, as happens
|
|
4422
|
+
* when the path is not in the schema. Form state is unchanged on a
|
|
4423
|
+
* `false`.
|
|
4424
|
+
*
|
|
4425
|
+
* Sugar over `setValue(path, schema.getEmptyValueAtPath(path))` with
|
|
4426
|
+
* no separate bookkeeping, so variant memory, history and listeners
|
|
4427
|
+
* all see a regular write at the path.
|
|
4428
|
+
*
|
|
4429
|
+
* `clear()` with no argument targets the whole form, while
|
|
4430
|
+
* `clear('')` targets the empty-string path slot SPECIFICALLY. The
|
|
4431
|
+
* two are not interchangeable, matching `touch()` and `touch('')`
|
|
4432
|
+
* from #184.
|
|
4433
|
+
*/
|
|
4434
|
+
clear: {
|
|
4435
|
+
(): boolean;
|
|
4436
|
+
<Path extends FlatPath<Form> | ''>(path: Path): boolean;
|
|
4437
|
+
<const S extends ReadonlyArray<string | number>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form> | ''] ? unknown : SegmentPathRejection<JoinSegments<S>>)): boolean;
|
|
4438
|
+
};
|
|
4439
|
+
/**
|
|
4440
|
+
* The undo/redo namespace: `form.history.{undo, redo, clear, canUndo,
|
|
4441
|
+
* canRedo, size}`. Always present, and inert when `useForm({ history })`
|
|
4442
|
+
* was not configured. `FormHistoryNamespace` carries the
|
|
4443
|
+
* field-by-field semantics.
|
|
4444
|
+
*/
|
|
4445
|
+
history: FormHistoryNamespace;
|
|
4446
|
+
/**
|
|
4447
|
+
* Focus the first errored field's first visible element. Returns
|
|
4448
|
+
* `true` when an element was focused, `false` when no candidate
|
|
4449
|
+
* element exists (no errors, or every errored field is unmounted
|
|
4450
|
+
* or hidden).
|
|
4451
|
+
*
|
|
4452
|
+
* Pass `{ preventScroll: true }` if you're scrolling separately
|
|
4453
|
+
* (e.g. via `scrollToFirstError`) and don't want the browser to
|
|
4454
|
+
* fight the explicit scroll.
|
|
4455
|
+
*
|
|
4456
|
+
* Requests `focusVisible: true` so the UA paints a focus ring even
|
|
4457
|
+
* though the move is programmatic. Honored where supported; on
|
|
4458
|
+
* browsers without it, non-text controls focused after a pointer
|
|
4459
|
+
* submit may show no ring, so you may still want an app-side
|
|
4460
|
+
* indicator.
|
|
4461
|
+
*/
|
|
4462
|
+
focusFirstError: (options?: {
|
|
4463
|
+
preventScroll?: boolean;
|
|
4464
|
+
}) => boolean;
|
|
4465
|
+
/**
|
|
4466
|
+
* Scroll the first errored field's first visible element into
|
|
4467
|
+
* view. Returns `true` when the call ran, `false` when no
|
|
4468
|
+
* candidate element exists.
|
|
4469
|
+
*
|
|
4470
|
+
* `options` is forwarded to `Element.scrollIntoView` unchanged.
|
|
4471
|
+
*/
|
|
4472
|
+
scrollToFirstError: (options?: ScrollIntoViewOptions) => boolean;
|
|
4473
|
+
/**
|
|
4474
|
+
* Run the form's own invalid-submit nudge imperatively: exactly what
|
|
4475
|
+
* `handleSubmit` does after a failed submit, but available standalone
|
|
4476
|
+
* and honoring this form's `useForm({ focusOnInvalidSubmit })` choice.
|
|
4477
|
+
*
|
|
4478
|
+
* Used by `useWizard` after navigating to the first failing form
|
|
4479
|
+
* during `wizard.handleSubmit`, so the failing form's own configured
|
|
4480
|
+
* behavior fires once its DOM is in view.
|
|
4481
|
+
*
|
|
4482
|
+
* No-op when this form opted out, or when no errored field is
|
|
4483
|
+
* currently registered. Use `focusFirstError()` to focus regardless
|
|
4484
|
+
* of the configured choice.
|
|
4485
|
+
*/
|
|
4486
|
+
applyInvalidSubmitPolicy: () => void;
|
|
4487
|
+
/**
|
|
4488
|
+
* Programmatically mark fields as `touched`, the descriptive "this
|
|
4489
|
+
* field was visited" flag.
|
|
4490
|
+
*
|
|
4491
|
+
* ```ts
|
|
4492
|
+
* form.touch('email') // one leaf
|
|
4493
|
+
* form.touch('profile') // every leaf under profile
|
|
4494
|
+
* form.touch(['profile', 'name']) // segment-array form
|
|
4495
|
+
* form.touch() // every leaf in the form
|
|
4496
|
+
* ```
|
|
4497
|
+
*
|
|
4498
|
+
* **This does not reveal errors under the default display
|
|
4499
|
+
* heuristic.** `touched` also flips on a bare focus and blur with no
|
|
4500
|
+
* edit, so the library-default gate ignores it and reads
|
|
4501
|
+
* `blurredAfterInteraction` instead, the stricter bit that only a
|
|
4502
|
+
* blur *following* an edit sets. Reach for `touch()` when your own
|
|
4503
|
+
* analytics reads `touched`, and for
|
|
4504
|
+
* {@link UseFormReturnType.interact} to make a seeded or imported
|
|
4505
|
+
* value surface its errors.
|
|
4506
|
+
*
|
|
4507
|
+
* A pure flag write: it mutates no value, focused or blurred state
|
|
4508
|
+
* and triggers no validation. Idempotent, so re-calling on an
|
|
4509
|
+
* already-touched field is a no-op. Touched is sticky-true; pair it
|
|
4510
|
+
* with `form.reset()` or `form.resetField()` to clear.
|
|
4511
|
+
*/
|
|
4512
|
+
touch: (path?: FlatPath<Form> | (string | number)[]) => void;
|
|
4513
|
+
/**
|
|
4514
|
+
* Simulate a complete focus, edit and blur over every leaf under
|
|
4515
|
+
* `path`, or the whole form when it is omitted, so a value that
|
|
4516
|
+
* arrived without a DOM gesture reveals its errors under the default
|
|
4517
|
+
* display heuristic.
|
|
4518
|
+
*
|
|
4519
|
+
* ```ts
|
|
4520
|
+
* form.interact('profile') // arm one subtree
|
|
4521
|
+
* await form.interact(['members', 2]) // errors committed on resolve
|
|
4522
|
+
* form.interact() // every leaf in the form
|
|
4523
|
+
* ```
|
|
4524
|
+
*
|
|
4525
|
+
* Flips the full interaction ladder (`touched`, `interacted`, and
|
|
4526
|
+
* `blurredAfterInteraction`) and runs the subtree's validation, so
|
|
4527
|
+
* the default gate (`submissionAttempts > 0 ||
|
|
4528
|
+
* blurredAfterInteraction`) opens through its front door. Use it to
|
|
4529
|
+
* arm one region's errors without a form-wide submit, which would
|
|
4530
|
+
* light up every other field on the page: a field-array row edited in
|
|
4531
|
+
* a modal, a server-seeded section, a pasted import.
|
|
4532
|
+
*
|
|
4533
|
+
* Walks schema leaves, so it reaches fields that are currently
|
|
4534
|
+
* `v-if`'d away or were never mounted; the flags are sticky, so
|
|
4535
|
+
* such a subtree stays revealed when it remounts.
|
|
4536
|
+
*
|
|
4537
|
+
* Flags land synchronously, and the returned promise resolves once
|
|
4538
|
+
* the subtree's validation has committed, so an awaiting caller reads
|
|
4539
|
+
* `showErrors` and `errors` immediately after. It never rejects, and
|
|
4540
|
+
* ignoring it is fine.
|
|
4541
|
+
*
|
|
4542
|
+
* No-op on a disabled form, matching every other interaction-
|
|
4543
|
+
* lifecycle write. Does not mutate value, `focused`, or `blurred`:
|
|
4544
|
+
* those are DOM-owned, and the display gate reads none of them.
|
|
4545
|
+
* Sticky like `touched`; pair with `form.reset()` /
|
|
4546
|
+
* `form.resetField()` to clear.
|
|
4547
|
+
*/
|
|
4548
|
+
interact: (path?: FlatPath<Form> | (string | number)[]) => Promise<void>;
|
|
4549
|
+
/**
|
|
4550
|
+
* Append `value` to the array at `path`.
|
|
4551
|
+
*
|
|
4552
|
+
* ```ts
|
|
4553
|
+
* form.append('items', { name: 'New' })
|
|
4554
|
+
* ```
|
|
4555
|
+
*/
|
|
4556
|
+
append: <Path extends ArrayPath<Form>>(path: Path, value: ArrayItem<Form, Path>) => void;
|
|
4557
|
+
/** Prepend `value` to the array at `path`. */
|
|
4558
|
+
prepend: <Path extends ArrayPath<Form>>(path: Path, value: ArrayItem<Form, Path>) => void;
|
|
4559
|
+
/**
|
|
4560
|
+
* Insert `value` into the array at `path` at the given `index`.
|
|
4561
|
+
* Behaves like `Array.prototype.splice`: `index` is clamped into
|
|
4562
|
+
* `[0, length]`, and negative indices count from the end.
|
|
4563
|
+
*/
|
|
4564
|
+
insert: <Path extends ArrayPath<Form>>(path: Path, index: number, value: ArrayItem<Form, Path>) => void;
|
|
4565
|
+
/** Remove the element at `index` from the array at `path`. No-op when out of range. */
|
|
4566
|
+
remove: <Path extends ArrayPath<Form>>(path: Path, index: number) => void;
|
|
4567
|
+
/** Swap the elements at indices `a` and `b`. No-op when either is out of range. */
|
|
4568
|
+
swap: <Path extends ArrayPath<Form>>(path: Path, a: number, b: number) => void;
|
|
4569
|
+
/**
|
|
4570
|
+
* Move the element at `from` to `to`. Useful for drag-and-drop
|
|
4571
|
+
* reordering. No-op when either index is out of range.
|
|
4572
|
+
*/
|
|
4573
|
+
move: <Path extends ArrayPath<Form>>(path: Path, from: number, to: number) => void;
|
|
4574
|
+
/** Replace the element at `index` with `value`. No-op when out of range. */
|
|
4575
|
+
replace: <Path extends ArrayPath<Form>>(path: Path, index: number, value: ArrayItem<Form, Path>) => void;
|
|
4576
|
+
/**
|
|
4577
|
+
* Read-only, reactive view of the array at `path` as one `FieldState`
|
|
4578
|
+
* per element, in index order. Each entry carries its element `key`,
|
|
4579
|
+
* an allocated identity token, so a `v-for` keyed by it keeps a row's
|
|
4580
|
+
* component instance across an insert, removal, move, or swap:
|
|
4581
|
+
*
|
|
4582
|
+
* ```vue
|
|
4583
|
+
* <div v-for="(row, i) in form.list('contacts')" :key="row.key">
|
|
4584
|
+
* <input v-register="form.register(`contacts.${i}.name`)" />
|
|
4585
|
+
* <p v-if="row.showErrors">{{ row.firstError?.message }}</p>
|
|
4586
|
+
* </div>
|
|
4587
|
+
* ```
|
|
4588
|
+
*
|
|
4589
|
+
* Entries are the same field states `form.fields` exposes, so reads
|
|
4590
|
+
* stay live. `form.fields(path)` remains the single aggregated
|
|
4591
|
+
* container for the whole array; `list` is the per-element view.
|
|
4592
|
+
* For a record, reach for `record`, which keys each entry by its own
|
|
4593
|
+
* key.
|
|
4594
|
+
*/
|
|
4595
|
+
list: <Path extends ArrayPath<Form>>(path: Path) => readonly FieldState<ArrayItem<Form, Path>>[];
|
|
4596
|
+
/**
|
|
4597
|
+
* Read-only, reactive view of the record at `path` as one `FieldState`
|
|
4598
|
+
* per entry, keyed by the entry's own key. Where `list` hands back an
|
|
4599
|
+
* ordered array for an array path, `record` hands back a keyed object
|
|
4600
|
+
* for a record path, so you iterate it by key:
|
|
4601
|
+
*
|
|
4602
|
+
* ```vue
|
|
4603
|
+
* <div v-for="(field, key) in form.record('scoresByTeam')" :key="key">
|
|
4604
|
+
* <label>{{ key }}</label>
|
|
4605
|
+
* <input v-register="form.register(`scoresByTeam.${key}`)" />
|
|
4606
|
+
* <p v-if="field.showErrors">{{ field.firstError?.message }}</p>
|
|
4607
|
+
* </div>
|
|
4608
|
+
* ```
|
|
4609
|
+
*
|
|
4610
|
+
* Entries are the same field states `form.fields` exposes, so reads
|
|
4611
|
+
* stay live, and the keyed shape mirrors the record's own keys: an
|
|
4612
|
+
* entry appears once you write its key (`form.setValue`) and drops
|
|
4613
|
+
* when the key leaves. `form.fields(path)` remains the single
|
|
4614
|
+
* aggregated container for the whole record; `record` is the
|
|
4615
|
+
* per-entry view.
|
|
4616
|
+
*
|
|
4617
|
+
* When the form root is itself a record (`useForm({ schema:
|
|
4618
|
+
* z.record(K, V) })`, a dictionary form), call `form.record()` with
|
|
4619
|
+
* no argument for the root entry view:
|
|
4620
|
+
*
|
|
4621
|
+
* ```vue
|
|
4622
|
+
* <div v-for="(member, id) in form.record()" :key="id">
|
|
4623
|
+
* <input v-register="form.register(id)" />
|
|
4624
|
+
* <p v-if="member.showErrors">{{ member.firstError?.message }}</p>
|
|
4625
|
+
* </div>
|
|
4626
|
+
* ```
|
|
4627
|
+
*/
|
|
4628
|
+
record: RootRecordView<Form> & (<Path extends RecordPath<Form>>(path: Path) => Readonly<Record<string, FieldState<RecordValue<Form, Path>>>>);
|
|
4629
|
+
/**
|
|
4630
|
+
* Read-only view of the form's blank path set, and reactive: Vue 3.5
|
|
4631
|
+
* tracks `.has()`, `for..of` and size accesses, so a consumer drives
|
|
4632
|
+
* conditional UI off it directly:
|
|
4633
|
+
*
|
|
4634
|
+
* ```ts
|
|
4635
|
+
* watchEffect(() => {
|
|
4636
|
+
* if (form.blankPaths.value.size > 0) {
|
|
4637
|
+
* const paths = [...form.blankPaths.value] // Path[][], segments per entry
|
|
4638
|
+
* console.warn('unanswered fields:', paths.map((p) => p.join('.')))
|
|
4639
|
+
* }
|
|
4640
|
+
* })
|
|
4641
|
+
* ```
|
|
4642
|
+
*
|
|
4643
|
+
* `.has(input)` accepts the dotted-string form (`'user.email'`) or
|
|
4644
|
+
* the array form (`['user', 'email']`), which disambiguates a key
|
|
4645
|
+
* carrying literal dots such as `['address.primary']`.
|
|
4646
|
+
* [[BlankPathsView]] carries the full surface.
|
|
4647
|
+
*
|
|
4648
|
+
* For per-path access, use `form.fields.<path>.blank`.
|
|
4649
|
+
* Writes happen through `setValue(path, unset)`,
|
|
4650
|
+
* `markBlank()` on a register binding, and the directive's
|
|
4651
|
+
* input listener on numeric clear.
|
|
4652
|
+
*/
|
|
4653
|
+
blankPaths: ComputedRef<BlankPathsView>;
|
|
4654
|
+
};
|
|
4655
|
+
|
|
4656
|
+
export { ROOT_PATH as R, canonicalizePath as ai, isPathPrefix as aj, isUnset as ak, parseDottedPath as al, unset as am, ROOT_PATH_KEY as z };
|
|
4657
|
+
export type { SchemaParseErrorWithData as $, AbstractSchema as A, ReactiveValidationStatus as B, CustomDirectiveRegisterAssignerFn as C, DefaultValuesInput as D, ErrorInput as E, FormKey as F, GenericForm as G, HandleSubmit as H, IsTuple as I, JoinSegments as J, KeyofUnion as K, LiftedValueShape as L, MetaTrackerValue as M, NestedReadType as N, OnError as O, ParseOptions as P, RecordPath as Q, RecordValue as S, RegisterDirective as T, UseFormConfiguration as U, RegisterFlatPath as V, RegisterOptions as W, RegisterSelectModifier as X, RegisterTextModifier as Y, RegisterTransform as Z, RegisterValue as _, UseFormReturnType as a, SchemaParseErrorWithoutData as a0, SchemaParseResult as a1, SchemaParseSuccess as a2, Segment as a3, SetValueCallback as a4, SetValuePayload as a5, SettledValidationStatus as a6, SlimPrimitiveKind as a7, SubmitHandler as a8, Unset as a9, ValidateOn as aa, ValidateOnConfig as ab, ValidationError as ac, ValidationResponse as ad, ValidationResponseWithoutValue as ae, ValueOfUnion as af, WriteMeta as ag, WriteShape as ah, RegisterModelDynamicCustomDirective as an, SchemaFactoryOptions as ao, AcceptableDefaults as ap, DisplayCtx as aq, GetDisplayState as ar, DisplayMachine as as, FieldRecord as at, ErrorCell as au, OriginalsRecord as av, AttaformDomBinding as aw, TransformAbortHolder as ax, ArrayItem as b, ArrayPath as c, DeepPartial as d, DefaultValuesShape as e, DisplayState as f, ErrorsProxyShape as g, FieldMetaPayload as h, FieldState as i, FieldStateMap as j, FieldStateMapEntry as k, FlatPath as l, FormErrorRecord as m, FormErrorsSurface as n, FormMeta as o, HistoryPlugin as p, IsUnion as q, Json as r, NestedType as s, OnSubmit as t, PartialFlatPath as u, Path as v, PathKey as w, PendingValidationStatus as x, Primitive as y };
|