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