attaform 0.17.2 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -36
- package/dist/chunks/devtools.cjs +10 -37
- package/dist/chunks/devtools.cjs.map +1 -1
- package/dist/chunks/devtools.mjs +10 -37
- package/dist/chunks/devtools.mjs.map +1 -1
- package/dist/chunks/indexeddb.cjs +4 -4
- package/dist/chunks/indexeddb.cjs.map +1 -1
- package/dist/chunks/indexeddb.mjs +1 -1
- package/dist/chunks/local-storage.cjs +2 -2
- package/dist/chunks/local-storage.cjs.map +1 -1
- package/dist/chunks/local-storage.mjs +1 -1
- package/dist/chunks/session-storage.cjs +2 -2
- package/dist/chunks/session-storage.cjs.map +1 -1
- package/dist/chunks/session-storage.mjs +1 -1
- package/dist/index.cjs +42 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +159 -196
- package/dist/index.d.mts +159 -196
- package/dist/index.d.ts +159 -196
- package/dist/index.mjs +5 -7
- package/dist/index.mjs.map +1 -1
- package/dist/nuxt.cjs +31 -40
- package/dist/nuxt.cjs.map +1 -1
- package/dist/nuxt.d.cts +8 -1
- package/dist/nuxt.d.mts +8 -1
- package/dist/nuxt.d.ts +8 -1
- package/dist/nuxt.mjs +32 -41
- package/dist/nuxt.mjs.map +1 -1
- package/dist/runtime/components/AttaformDevtoolsPanel.d.vue.ts +7 -0
- package/dist/runtime/components/AttaformDevtoolsPanel.vue +453 -0
- package/dist/runtime/components/AttaformDevtoolsPanel.vue.d.ts +7 -0
- package/dist/runtime/components/DevtoolsValueTree.d.vue.ts +37 -0
- package/dist/runtime/components/DevtoolsValueTree.vue +192 -0
- package/dist/runtime/components/DevtoolsValueTree.vue.d.ts +37 -0
- package/dist/runtime/plugins/attaform.cjs +17 -6
- package/dist/runtime/plugins/attaform.cjs.map +1 -1
- package/dist/runtime/plugins/attaform.mjs +15 -4
- package/dist/runtime/plugins/attaform.mjs.map +1 -1
- package/dist/shared/{attaform.C0iFnTN0.d.ts → attaform.2b7M2mww.d.mts} +57 -23
- package/dist/shared/attaform.5UhpSVFI.cjs +63 -0
- package/dist/shared/attaform.5UhpSVFI.cjs.map +1 -0
- package/dist/shared/attaform.BDdFdjeX.mjs +57 -0
- package/dist/shared/attaform.BDdFdjeX.mjs.map +1 -0
- package/dist/shared/{attaform.Dee2rU1P.cjs → attaform.BqK_L4gK.cjs} +310 -24
- package/dist/shared/attaform.BqK_L4gK.cjs.map +1 -0
- package/dist/shared/attaform.Bubm_slq.cjs.map +1 -1
- package/dist/shared/attaform.CXpzmj38.mjs.map +1 -1
- package/dist/shared/{attaform.Drt6fivF.mjs → attaform.CtNUB9nf.mjs} +74 -76
- package/dist/shared/attaform.CtNUB9nf.mjs.map +1 -0
- package/dist/shared/{attaform.C5MH4lNh.d.mts → attaform.DF8wo-ry.d.ts} +4 -4
- package/dist/shared/attaform.DK9aj0N8.d.ts +1651 -0
- package/dist/shared/{attaform.BPRHR3Zs.cjs → attaform.DUHru0OF.cjs} +83 -85
- package/dist/shared/attaform.DUHru0OF.cjs.map +1 -0
- package/dist/shared/{attaform.C6lbmMUe.d.ts → attaform.DVLB6CAn.d.mts} +4 -4
- package/dist/shared/{attaform.C_5aB6EQ.d.ts → attaform.Dj9mwbaV.d.cts} +756 -148
- package/dist/shared/{attaform.C_5aB6EQ.d.mts → attaform.Dj9mwbaV.d.mts} +756 -148
- package/dist/shared/{attaform.C_5aB6EQ.d.cts → attaform.Dj9mwbaV.d.ts} +756 -148
- package/dist/shared/{attaform.BV40t5y2.cjs → attaform.Dlk1jMuv.cjs} +245 -108
- package/dist/shared/attaform.Dlk1jMuv.cjs.map +1 -0
- package/dist/shared/attaform.DoSuaKMd.d.cts +1651 -0
- package/dist/shared/{attaform.B3ZaPIzS.mjs → attaform.DsC3rZHG.mjs} +1804 -219
- package/dist/shared/attaform.DsC3rZHG.mjs.map +1 -0
- package/dist/shared/{attaform.Cer8JO_P.cjs → attaform.II89Pcf4.cjs} +1860 -272
- package/dist/shared/attaform.II89Pcf4.cjs.map +1 -0
- package/dist/shared/{attaform.CHorcsIU.d.cts → attaform.M33WKVV4.d.cts} +57 -23
- package/dist/shared/{attaform.CIEQgJnM.mjs → attaform.Xhg0AYNa.mjs} +300 -26
- package/dist/shared/attaform.Xhg0AYNa.mjs.map +1 -0
- package/dist/shared/{attaform.CpERWz3u.mjs → attaform.Xt0A3QUd.mjs} +232 -95
- package/dist/shared/attaform.Xt0A3QUd.mjs.map +1 -0
- package/dist/shared/attaform.iTqxvl-P.d.mts +1651 -0
- package/dist/shared/{attaform.CuE-bS1C.d.mts → attaform.tsNFcEW7.d.ts} +57 -23
- package/dist/shared/{attaform.DtMN-MAm.d.cts → attaform.tts_OM7j.d.cts} +4 -4
- package/dist/vite.cjs +288 -2
- package/dist/vite.cjs.map +1 -1
- package/dist/vite.mjs +288 -2
- package/dist/vite.mjs.map +1 -1
- package/dist/zod-v3.cjs +11 -8
- package/dist/zod-v3.cjs.map +1 -1
- package/dist/zod-v3.d.cts +6 -6
- package/dist/zod-v3.d.mts +6 -6
- package/dist/zod-v3.d.ts +6 -6
- package/dist/zod-v3.mjs +3 -3
- package/dist/zod-v4.cjs +11 -8
- package/dist/zod-v4.cjs.map +1 -1
- package/dist/zod-v4.d.cts +5 -5
- package/dist/zod-v4.d.mts +5 -5
- package/dist/zod-v4.d.ts +5 -5
- package/dist/zod-v4.mjs +3 -3
- package/dist/zod.cjs +15 -16
- package/dist/zod.cjs.map +1 -1
- package/dist/zod.d.cts +127 -40
- package/dist/zod.d.mts +127 -40
- package/dist/zod.d.ts +127 -40
- package/dist/zod.mjs +7 -11
- package/dist/zod.mjs.map +1 -1
- package/package.json +18 -7
- package/dist/shared/attaform.B1jvxsOF.d.mts +0 -156
- package/dist/shared/attaform.B3ZaPIzS.mjs.map +0 -1
- package/dist/shared/attaform.BBM2muQ9.cjs +0 -101
- package/dist/shared/attaform.BBM2muQ9.cjs.map +0 -1
- package/dist/shared/attaform.BPRHR3Zs.cjs.map +0 -1
- package/dist/shared/attaform.BV40t5y2.cjs.map +0 -1
- package/dist/shared/attaform.C6qzEdIM.d.cts +0 -156
- package/dist/shared/attaform.C8LVFVVe.cjs +0 -32
- package/dist/shared/attaform.C8LVFVVe.cjs.map +0 -1
- package/dist/shared/attaform.CIEQgJnM.mjs.map +0 -1
- package/dist/shared/attaform.CTwNcpLE.d.ts +0 -156
- package/dist/shared/attaform.Cer8JO_P.cjs.map +0 -1
- package/dist/shared/attaform.CpERWz3u.mjs.map +0 -1
- package/dist/shared/attaform.Dee2rU1P.cjs.map +0 -1
- package/dist/shared/attaform.Drt6fivF.mjs.map +0 -1
- package/dist/shared/attaform.Vo-Kft0t.mjs +0 -29
- package/dist/shared/attaform.Vo-Kft0t.mjs.map +0 -1
- package/dist/shared/attaform.h1sq3BFu.mjs +0 -92
- package/dist/shared/attaform.h1sq3BFu.mjs.map +0 -1
|
@@ -0,0 +1,1651 @@
|
|
|
1
|
+
import { F as FormKey, af as SlimPrimitiveKind, C as CoercionEntry, j as CoercionRegistry, G as GenericForm, Y as PathKey, X as Path, V as ValidationError, a as AbstractSchema, d as ShouldShowErrors, ao as WriteMeta, m as DeepPartial, ap as WriteShape, aj as ValidateOn, aw as PersistOptInRegistry, S as Segment, A as AttaformDefaults, b as UseFormReturnType, R as RegisterValue } from './attaform.Dj9mwbaV.mjs';
|
|
2
|
+
import { Ref, ComputedRef, App, InjectionKey } from 'vue';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Public types for `useWizard` — the multistep-form orchestrator.
|
|
6
|
+
*
|
|
7
|
+
* The wizard is built around an ordered list of step slots. Each slot
|
|
8
|
+
* resolves to a participating form: an existing `useForm` reference, a
|
|
9
|
+
* bare string key (desugared to a noop form so affordance steps
|
|
10
|
+
* participate uniformly), an eagerly-evaluated function slot for
|
|
11
|
+
* runtime branching, or a `lazy()`-wrapped function slot that caches
|
|
12
|
+
* its resolution and re-fires only on its own tracked deps.
|
|
13
|
+
*
|
|
14
|
+
* The wizard surface is loosely keyed (`Record<FormKey, …>`).
|
|
15
|
+
* Cross-component flows threaded through `injectWizard` lose lexical
|
|
16
|
+
* key knowledge anyway, so the public read surface is a string-keyed
|
|
17
|
+
* record. Typed per-form access flows back through the original form
|
|
18
|
+
* refs and through `wizard.handleSubmit`'s `ctx.get(formRef)` accessor.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Minimum structural shape the wizard requires from a participating
|
|
23
|
+
* form. Constraining to the full `UseFormReturnType` would force
|
|
24
|
+
* contravariant unification of the storage / read shapes across all
|
|
25
|
+
* steps; the wizard does not care about those — it routes by `key` at
|
|
26
|
+
* runtime and exposes the original form objects untouched.
|
|
27
|
+
*/
|
|
28
|
+
type AnyForm = {
|
|
29
|
+
readonly key: FormKey;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Per-form summary surface — what `wizard.statuses[key]` exposes (and
|
|
33
|
+
* what `defaultStatuses` seeds). Distinct from `form.meta`: `FormStatus`
|
|
34
|
+
* is the cross-step rollup optimized for template ergonomics
|
|
35
|
+
* (`{{ wizard.statuses.cargo.valid }}`), while `form.meta` carries the
|
|
36
|
+
* full per-form lifecycle surface.
|
|
37
|
+
*
|
|
38
|
+
* Field semantics:
|
|
39
|
+
* - `valid` — `form.meta.valid`. `false` while errors exist or while
|
|
40
|
+
* the first-validation-done gate has not flipped.
|
|
41
|
+
* - `dirty` — `form.meta.dirty`. `true` once any value differs from
|
|
42
|
+
* the original defaults.
|
|
43
|
+
* - `submitted` — `form.meta.submitted`. `true` once a `handleSubmit`
|
|
44
|
+
* callback has resolved without throwing. A failed submit
|
|
45
|
+
* (validation or callback rejection) leaves this `false`;
|
|
46
|
+
* `submissionAttempts > 0` is the "user has tried" signal.
|
|
47
|
+
* - `errorCount` — `form.meta.errorCount`. Count of active validation
|
|
48
|
+
* errors (zero when valid).
|
|
49
|
+
*
|
|
50
|
+
* Noop forms generated for string slots surface as default-valid
|
|
51
|
+
* (`{ valid: true, dirty: false, submitted: false, errorCount: 0 }`).
|
|
52
|
+
*/
|
|
53
|
+
type FormStatus = {
|
|
54
|
+
readonly valid: boolean;
|
|
55
|
+
readonly dirty: boolean;
|
|
56
|
+
readonly submitted: boolean;
|
|
57
|
+
readonly errorCount: number;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Flat error shape returned per form by `wizard.allErrors[key]`. Each
|
|
61
|
+
* entry carries the formKey + path tuple so consumers can route to the
|
|
62
|
+
* offending field from a wizard-wide error summary.
|
|
63
|
+
*/
|
|
64
|
+
type AggregateError = {
|
|
65
|
+
readonly formKey: FormKey;
|
|
66
|
+
readonly path: ReadonlyArray<string | number>;
|
|
67
|
+
readonly message: string;
|
|
68
|
+
readonly code?: string;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Mirror of `form.values`' call-or-read pattern, one level deep.
|
|
72
|
+
* Drillable as `wizard.statuses.cargo.valid` (readable), as
|
|
73
|
+
* `wizard.statuses('cargo')` (callable single-key), or as
|
|
74
|
+
* `wizard.statuses()` (callable no-arg returns the whole record).
|
|
75
|
+
*/
|
|
76
|
+
type WizardStatusesProxy<S extends Record<string, FormStatus>> = ((key?: keyof S) => FormStatus | S) & Readonly<S>;
|
|
77
|
+
/**
|
|
78
|
+
* One compiled position in the wizard's flow. The wizard surface
|
|
79
|
+
* exposes an ordered array of these as `wizard.steps`, plus a
|
|
80
|
+
* `wizard.forms` record keyed by `step.key` for direct lookup.
|
|
81
|
+
*
|
|
82
|
+
* String slots in the source `steps` array desugar to noop forms
|
|
83
|
+
* before compilation, so every compiled step carries a `form`
|
|
84
|
+
* regardless of source kind.
|
|
85
|
+
*/
|
|
86
|
+
type CompiledStep = {
|
|
87
|
+
readonly key: FormKey;
|
|
88
|
+
readonly form: AnyForm;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Shape of a participating form as seen from inside a function slot's
|
|
92
|
+
* `ctx.forms[key]` lookup. Adds `values` to the structural `AnyForm`
|
|
93
|
+
* minimum so routing decisions can read live form state.
|
|
94
|
+
*
|
|
95
|
+
* Values are typed loose because the wizard does not generically thread
|
|
96
|
+
* each step's schema through `ctx.forms`. For typed access inside slot
|
|
97
|
+
* bodies, close over the original form ref instead of routing through
|
|
98
|
+
* `ctx.forms`.
|
|
99
|
+
*/
|
|
100
|
+
type WizardCtxForm = AnyForm & {
|
|
101
|
+
readonly values: Readonly<Record<string, unknown>>;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Context object passed to function slots in the `steps` array. The
|
|
105
|
+
* `forms` record exposes the wizard's statically-known forms (every
|
|
106
|
+
* top-level `AnyForm` slot plus every noop form synthesized for a
|
|
107
|
+
* top-level string slot). `currentKey` mirrors the live wizard step.
|
|
108
|
+
*
|
|
109
|
+
* Function slots re-evaluate reactively when the values they read
|
|
110
|
+
* mutate (typically `ctx.forms.<key>.values.<path>`). The `forms`
|
|
111
|
+
* accumulator itself is stable across re-evaluations so the slot's
|
|
112
|
+
* lookup identity stays referentially equal. Effectful slot bodies
|
|
113
|
+
* should be avoided; routing decisions live here.
|
|
114
|
+
*/
|
|
115
|
+
type WizardCtx = {
|
|
116
|
+
readonly forms: Readonly<Record<FormKey, WizardCtxForm>>;
|
|
117
|
+
readonly currentKey: FormKey | undefined;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Internal phantom brand for `LazyMarker`. The runtime brand symbol
|
|
121
|
+
* lives in `core/wizard-lazy.ts`; this declaration keeps the marker
|
|
122
|
+
* type unforgeable without circular module imports.
|
|
123
|
+
*/
|
|
124
|
+
declare const _lazyBrand: unique symbol;
|
|
125
|
+
/**
|
|
126
|
+
* Brand-typed marker returned by `lazy((ctx) => …)`. Wrapping a
|
|
127
|
+
* function slot in `lazy()` gives that slot its own memoization cache:
|
|
128
|
+
* the resolver fires once on the first compile pass, and the result
|
|
129
|
+
* stays cached until one of the resolver's own tracked reactive reads
|
|
130
|
+
* changes (or `wizard.reset()` invalidates the cache). Heavy or
|
|
131
|
+
* one-shot lookups (network-backed factories, expensive derivations)
|
|
132
|
+
* do not re-fire because an unrelated slot's deps changed.
|
|
133
|
+
*
|
|
134
|
+
* Construct via the `lazy()` helper exported from the same entry as
|
|
135
|
+
* `useWizard`. The marker is opaque at the type level; consumers do
|
|
136
|
+
* not assemble it directly.
|
|
137
|
+
*/
|
|
138
|
+
type LazyMarker<Ctx = WizardCtx> = {
|
|
139
|
+
readonly [_lazyBrand]: true;
|
|
140
|
+
readonly resolve: (ctx: Ctx) => AnyForm | string | undefined;
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* One position in the source `useWizard({ steps })` array. Each slot
|
|
144
|
+
* resolves to a compiled `{ key, form }` step:
|
|
145
|
+
*
|
|
146
|
+
* - `AnyForm` — a form declared via `useForm`. Surfaced as-is.
|
|
147
|
+
* - `string` — bare key. The wizard generates a noop form
|
|
148
|
+
* under the hood so the external surface stays
|
|
149
|
+
* uniform across affordance positions (intro,
|
|
150
|
+
* terms, congratulations, review surfaces).
|
|
151
|
+
* - function — eager slot, re-evaluates reactively. Returns
|
|
152
|
+
* one of the above, or `undefined` to drop the
|
|
153
|
+
* slot from the compiled list.
|
|
154
|
+
* - `LazyMarker` — memoized function slot (see `lazy`).
|
|
155
|
+
*/
|
|
156
|
+
type StepSlot<Ctx = WizardCtx> = AnyForm | string | ((ctx: Ctx) => AnyForm | string | undefined) | LazyMarker<Ctx>;
|
|
157
|
+
/**
|
|
158
|
+
* Shape returned by the `restore` callback. Carries the active step's
|
|
159
|
+
* key; intentionally open-ended (object form) so future additions land
|
|
160
|
+
* without a callback-signature break.
|
|
161
|
+
*/
|
|
162
|
+
type WizardRestoreState = {
|
|
163
|
+
readonly step?: FormKey;
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* `restore` callback signature. Invoked at construction and watched
|
|
167
|
+
* reactively via `watchEffect` so external state changes (browser
|
|
168
|
+
* back/forward, cross-tab events, route changes) re-apply through the
|
|
169
|
+
* wizard. Returning `undefined` falls through to the first step.
|
|
170
|
+
*/
|
|
171
|
+
type WizardRestoreFn = () => WizardRestoreState | undefined;
|
|
172
|
+
/**
|
|
173
|
+
* `persist` callback signature. Invoked whenever `wizard.currentStep`
|
|
174
|
+
* changes; the wizard diffs against the last persisted value to break
|
|
175
|
+
* the restore-persist loop, so the callback only fires when the active
|
|
176
|
+
* step actually moves.
|
|
177
|
+
*/
|
|
178
|
+
type WizardPersistFn = (state: WizardRestoreState) => void;
|
|
179
|
+
/**
|
|
180
|
+
* Submit context passed to the `onSubmit` callback registered via
|
|
181
|
+
* `wizard.handleSubmit(onSubmit, onError?)`. Same shape on every step;
|
|
182
|
+
* `isFinal` distinguishes intermediate vs final calls.
|
|
183
|
+
*
|
|
184
|
+
* - `values` — namespaced aggregate keyed by form key, mirroring
|
|
185
|
+
* `wizard.allValues`. Reflects parsed output for every
|
|
186
|
+
* form whose validation has settled; noops contribute an
|
|
187
|
+
* empty record.
|
|
188
|
+
* - `get(form)` — typed accessor that reads the parsed output for a
|
|
189
|
+
* specific form ref. Works across cross-component graphs
|
|
190
|
+
* because the form ref carries its schema info.
|
|
191
|
+
* - `currentKey` — key of the step that fired this submission.
|
|
192
|
+
* - `isFinal` — `true` when `currentKey` is the last position in
|
|
193
|
+
* `wizard.steps`. Intermediate calls validate the active
|
|
194
|
+
* form only and advance; final calls validate every form
|
|
195
|
+
* and stay on the terminal step.
|
|
196
|
+
*/
|
|
197
|
+
type WizardSubmitContext = {
|
|
198
|
+
readonly values: Readonly<Record<FormKey, unknown>>;
|
|
199
|
+
readonly get: <F extends AnyForm>(form: F) => F extends {
|
|
200
|
+
readonly values: infer V;
|
|
201
|
+
} ? V : unknown;
|
|
202
|
+
readonly currentKey: FormKey;
|
|
203
|
+
readonly isFinal: boolean;
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* `onSubmit` callback registered via `wizard.handleSubmit`. Sync or
|
|
207
|
+
* async; the returned promise gates `wizard.submitting`.
|
|
208
|
+
*/
|
|
209
|
+
type WizardOnSubmit = (ctx: WizardSubmitContext) => void | Promise<void>;
|
|
210
|
+
/**
|
|
211
|
+
* Optional `onError` callback registered via `wizard.handleSubmit`.
|
|
212
|
+
* Receives the aggregate error list — entries originate from per-form
|
|
213
|
+
* validation and activation failures (`atta:activation-failed`). Sync
|
|
214
|
+
* or async; the returned promise gates `wizard.submitting`.
|
|
215
|
+
*/
|
|
216
|
+
type WizardOnError = (errors: readonly AggregateError[]) => void | Promise<void>;
|
|
217
|
+
/**
|
|
218
|
+
* Options for `useWizard({ steps, … })`. `steps` is the only required
|
|
219
|
+
* field; the rest are optional and default sensibly for the common
|
|
220
|
+
* URL-synchronized wizard case.
|
|
221
|
+
*/
|
|
222
|
+
type WizardOptions = {
|
|
223
|
+
/**
|
|
224
|
+
* Ordered list of slots that compile into the wizard's positional
|
|
225
|
+
* step list. See `StepSlot` for the per-slot shape contract.
|
|
226
|
+
*/
|
|
227
|
+
readonly steps: ReadonlyArray<StepSlot>;
|
|
228
|
+
/**
|
|
229
|
+
* Identifier used to register the wizard handle in the per-app
|
|
230
|
+
* registry. Descendant components call `injectWizard(key)` to reach
|
|
231
|
+
* the same wizard without prop-threading. Anonymous wizards (option
|
|
232
|
+
* omitted) get a synthetic `__atta:anon-wizard:<id>` key resolved
|
|
233
|
+
* via `useId()` so SSR-rendered and client-hydrated trees agree on
|
|
234
|
+
* the same registry entry; the synthetic key is opaque and
|
|
235
|
+
* descendants reach an anonymous wizard via ambient `injectWizard()`
|
|
236
|
+
* rather than by key.
|
|
237
|
+
*
|
|
238
|
+
* Duplicate-key registration is first-wins-silently (dev-warn on the
|
|
239
|
+
* second registration) to mirror `useForm`'s shared-key behavior.
|
|
240
|
+
* The dev-warn fires only for explicit keys — two anonymous wizards
|
|
241
|
+
* are guaranteed distinct synthetic keys, so the warning never
|
|
242
|
+
* misfires on independent anonymous wizards on the same page.
|
|
243
|
+
*/
|
|
244
|
+
readonly key?: string;
|
|
245
|
+
/**
|
|
246
|
+
* Seed status payload used while a form is pre-resolved (async
|
|
247
|
+
* `defaultValues` in flight, or wizard-deferred non-current).
|
|
248
|
+
* Mirrors `defaultValues`' trichotomy: plain object, sync factory,
|
|
249
|
+
* or async factory.
|
|
250
|
+
*
|
|
251
|
+
* Status resolution priority per form:
|
|
252
|
+
* 1. `store.defaultsResolved === true` → derive from `form.meta`
|
|
253
|
+
* 2. else noop form → built-in always-valid status
|
|
254
|
+
* 3. else seed value for this key → frozen seed
|
|
255
|
+
* 4. else → pending sentinel
|
|
256
|
+
*
|
|
257
|
+
* Unknown keys in the seed object dev-warn so a stale resume payload
|
|
258
|
+
* surfaces at construction.
|
|
259
|
+
*/
|
|
260
|
+
readonly defaultStatuses?: Record<string, FormStatus> | (() => Record<string, FormStatus>) | (() => Promise<Record<string, FormStatus>>);
|
|
261
|
+
/**
|
|
262
|
+
* Optional progress override. When omitted, the wizard exposes
|
|
263
|
+
* `progress` as `valid_step_count / count` (normalised to `[0, 1]`).
|
|
264
|
+
* When provided, the returned number is used as-is — the consumer is
|
|
265
|
+
* responsible for any normalisation.
|
|
266
|
+
*
|
|
267
|
+
* The override is invoked inside a Vue `computed` so it must be
|
|
268
|
+
* synchronous and may only read reactive sources.
|
|
269
|
+
*/
|
|
270
|
+
readonly progress?: (steps: ReadonlyArray<CompiledStep>) => number;
|
|
271
|
+
/**
|
|
272
|
+
* When `wizard.handleSubmit` finds errors, automatically focus the
|
|
273
|
+
* first failing form: jump to its step and invoke its
|
|
274
|
+
* `applyInvalidSubmitPolicy()` (focus / scroll per the form's own
|
|
275
|
+
* `onInvalidSubmit` configuration). Default `true`; pass `false` to
|
|
276
|
+
* keep the active step where the user left it and handle navigation
|
|
277
|
+
* manually in the `onError` callback.
|
|
278
|
+
*/
|
|
279
|
+
readonly focusFirstError?: boolean;
|
|
280
|
+
/**
|
|
281
|
+
* Source of truth for the active step. Invoked at construction and
|
|
282
|
+
* re-evaluated reactively via `watchEffect`. Default callback reads
|
|
283
|
+
* `?step=<key>` from the URL via `wizard-history.ts`; pass `false`
|
|
284
|
+
* to disable URL sync, or provide a custom callback for non-router
|
|
285
|
+
* persistence (localStorage, broadcast channel, etc.).
|
|
286
|
+
*/
|
|
287
|
+
readonly restore?: WizardRestoreFn | false;
|
|
288
|
+
/**
|
|
289
|
+
* Destination for the active step. Invoked whenever `currentStep`
|
|
290
|
+
* changes, with a diff check to break the restore-persist loop.
|
|
291
|
+
* Default callback writes `?step=<key>` via `wizard-history.ts`;
|
|
292
|
+
* pass `false` to disable persistence, or provide a custom callback
|
|
293
|
+
* to scope the param name or write to alternate storage.
|
|
294
|
+
*/
|
|
295
|
+
readonly persist?: WizardPersistFn | false;
|
|
296
|
+
};
|
|
297
|
+
/**
|
|
298
|
+
* Predicate: is the steps tuple statically guaranteed to compile to a
|
|
299
|
+
* non-empty list? A tuple passes when (a) it's not the empty array
|
|
300
|
+
* literal and (b) it carries no function or `lazy()` slot — those
|
|
301
|
+
* slot kinds can resolve to `undefined` at runtime and drop the
|
|
302
|
+
* compiled position. Form slots and bare-string affordance slots
|
|
303
|
+
* always preserve their position; a tuple made of only those kinds is
|
|
304
|
+
* statically safe.
|
|
305
|
+
*
|
|
306
|
+
* Used to narrow `currentStep` / `activeForm` to their non-`undefined`
|
|
307
|
+
* shapes in the common-case wizard, while keeping the honest union
|
|
308
|
+
* everywhere a runtime drop is reachable.
|
|
309
|
+
*/
|
|
310
|
+
type StaticallyNonEmpty<S> = S extends readonly [] ? false : S extends readonly (infer Item)[] ? Item extends LazyMarker | ((...args: unknown[]) => unknown) ? false : true : false;
|
|
311
|
+
/** Active step's key, narrowed to `string` when `S` is statically safe. */
|
|
312
|
+
type CurrentStepOf<S> = StaticallyNonEmpty<S> extends true ? FormKey : FormKey | undefined;
|
|
313
|
+
/** Active step's form handle, narrowed to `AnyForm` when `S` is statically safe. */
|
|
314
|
+
type ActiveFormOf<S> = StaticallyNonEmpty<S> extends true ? AnyForm : AnyForm | undefined;
|
|
315
|
+
/**
|
|
316
|
+
* Recursive tuple walk that builds the static portion of
|
|
317
|
+
* `wizard.forms`. Each step slot contributes to the record:
|
|
318
|
+
*
|
|
319
|
+
* - **String slot** (`'review'`): the literal becomes the record key
|
|
320
|
+
* and the value is `AnyForm` (the noop form synthesized for the
|
|
321
|
+
* affordance position is opaque at the type level).
|
|
322
|
+
* - **Form slot** (a `useForm` reference with a literal `key` field):
|
|
323
|
+
* the form's own `key` becomes the record key, and the value is
|
|
324
|
+
* the concrete form handle type — so drilling
|
|
325
|
+
* `wizard.forms.shipping.values.address` carries the schema-derived
|
|
326
|
+
* field types through.
|
|
327
|
+
* - **Function / `lazy()` slot**: contributes nothing to the static
|
|
328
|
+
* map. Runtime-resolved forms are still reachable via the
|
|
329
|
+
* catch-all index signature on `WizardForms` (typed as `AnyForm`).
|
|
330
|
+
*
|
|
331
|
+
* Recursion is bounded by the tuple length; real-world wizards land
|
|
332
|
+
* well below the TS instantiation budget.
|
|
333
|
+
*/
|
|
334
|
+
type FormsRecordOf<S> = S extends readonly [
|
|
335
|
+
infer First,
|
|
336
|
+
...infer Rest extends ReadonlyArray<StepSlot>
|
|
337
|
+
] ? (First extends string ? {
|
|
338
|
+
readonly [P in First]: AnyForm;
|
|
339
|
+
} : First extends {
|
|
340
|
+
readonly key: infer K extends string;
|
|
341
|
+
} ? {
|
|
342
|
+
readonly [P in K]: First;
|
|
343
|
+
} : unknown) & FormsRecordOf<Rest> : unknown;
|
|
344
|
+
/**
|
|
345
|
+
* `wizard.forms` typed view. Combines the static per-step type map
|
|
346
|
+
* with a catch-all `Record<FormKey, AnyForm>` fallback so:
|
|
347
|
+
*
|
|
348
|
+
* - Statically known slot keys → concrete form type via `FormsRecordOf`
|
|
349
|
+
* - Any other string key → `AnyForm` via the index signature
|
|
350
|
+
*
|
|
351
|
+
* The intersection collapses to the concrete form for statically
|
|
352
|
+
* known keys (because the concrete form type extends `AnyForm`) and
|
|
353
|
+
* to `AnyForm` for unknown keys.
|
|
354
|
+
*/
|
|
355
|
+
type WizardForms<S> = FormsRecordOf<S> & Readonly<Record<FormKey, AnyForm>>;
|
|
356
|
+
/**
|
|
357
|
+
* Return shape of `useWizard({ steps, … })`. Every reactive read is a
|
|
358
|
+
* plain getter (no `.value`) — `wizard.currentStep`, `wizard.progress`,
|
|
359
|
+
* `wizard.allValues` track inside `computed` / template effects
|
|
360
|
+
* directly.
|
|
361
|
+
*
|
|
362
|
+
* Parameterized by the steps tuple `S` so active-position fields
|
|
363
|
+
* (`currentStep`, `activeForm`) narrow to non-undefined for the common
|
|
364
|
+
* case (all positional Form / string slots) and stay as honest unions
|
|
365
|
+
* when a function or `lazy()` slot can drop the compiled position at
|
|
366
|
+
* runtime. The `const` type parameter on `useWizard` preserves literal
|
|
367
|
+
* tuple types without consumer-side `as const`, so the narrowing
|
|
368
|
+
* happens automatically from the call site.
|
|
369
|
+
*
|
|
370
|
+
* - `currentStep` — key of the active step. Narrows to `string` when
|
|
371
|
+
* the steps tuple is statically guaranteed to
|
|
372
|
+
* compile to a non-empty list (all positional
|
|
373
|
+
* Form / string slots, no function or `lazy()`
|
|
374
|
+
* slots). Otherwise reads as `string | undefined`
|
|
375
|
+
* so the degenerate case (empty list at runtime)
|
|
376
|
+
* surfaces honestly.
|
|
377
|
+
* - `activeForm` — the active step's form handle. Same narrowing as
|
|
378
|
+
* `currentStep`. Noop forms cover string slots in
|
|
379
|
+
* the normal path.
|
|
380
|
+
* - `activeIndex` — 0-based position of the active step.
|
|
381
|
+
* - `isFinalStep` — `true` when `currentStep === steps[count - 1].key`.
|
|
382
|
+
* - `steps` — ordered list of compiled `{ key, form }` slots.
|
|
383
|
+
* - `forms` — record indexable by step key; the value is the
|
|
384
|
+
* full form handle resolved for that slot.
|
|
385
|
+
* - `count` — `steps.length`.
|
|
386
|
+
* - `statuses` — callable readonly proxy over the per-key
|
|
387
|
+
* `FormStatus` record. Noop-form keys always read
|
|
388
|
+
* as default-valid.
|
|
389
|
+
* - `allValues` — namespaced aggregate of each form's values, keyed
|
|
390
|
+
* by step key.
|
|
391
|
+
* - `allErrors` — namespaced aggregate of each form's validation
|
|
392
|
+
* errors, keyed by step key. Noop forms map to an
|
|
393
|
+
* empty list.
|
|
394
|
+
* - `progress` — normalised step-validity ratio (or the consumer's
|
|
395
|
+
* `progress` override). Forward-looking: noops count
|
|
396
|
+
* as always-valid.
|
|
397
|
+
* - `canAdvance` — `true` when `activeIndex < count - 1`. Pure
|
|
398
|
+
* positional check; navigation never gates on
|
|
399
|
+
* validity.
|
|
400
|
+
* - `canGoBack` — `true` when `activeIndex > 0`.
|
|
401
|
+
* - `complete` — `isFinalStep && every step's form is valid`.
|
|
402
|
+
* Forward-looking; reactive to current form
|
|
403
|
+
* validity. Gates "Finish button enable" style UI.
|
|
404
|
+
* - `done` — monotonic latch: flips `true` the first time a
|
|
405
|
+
* final-step `handleSubmit` resolves without
|
|
406
|
+
* throwing, and stays `true` through subsequent
|
|
407
|
+
* edits or invalidations. Only `reset()` flips it
|
|
408
|
+
* back. Gates "show success card" style UI that
|
|
409
|
+
* should reflect submission history rather than
|
|
410
|
+
* current validity.
|
|
411
|
+
* - `submitting` — `true` while a `wizard.handleSubmit` call is in
|
|
412
|
+
* flight. Global re-entrance guard: every
|
|
413
|
+
* navigation method also refuses while this is on.
|
|
414
|
+
* - `submissionAttempts` — count of `wizard.handleSubmit` invocations
|
|
415
|
+
* (success or failure). Always bumps, including on
|
|
416
|
+
* noop-form steps.
|
|
417
|
+
* - `visited` — append-only breadcrumb of navigated step keys.
|
|
418
|
+
* `back()` does not pop; the trail is the audit
|
|
419
|
+
* log, not the back-stack.
|
|
420
|
+
* - `next/back/goTo` — pure navigation. Refuses while `submitting`.
|
|
421
|
+
* - `handleSubmit(onSubmit, onError?)` — universal across all steps.
|
|
422
|
+
* Intermediate calls validate the active form and
|
|
423
|
+
* advance; final calls validate every form. Returns
|
|
424
|
+
* an event handler suitable for `<form @submit>` or
|
|
425
|
+
* imperative use.
|
|
426
|
+
* - `reset()` — zeros wizard lifecycle (`submissionAttempts`,
|
|
427
|
+
* `visited`), resets every form, returns
|
|
428
|
+
* `currentStep` to `steps[0].key`, and invokes
|
|
429
|
+
* `persist` with the cleared state.
|
|
430
|
+
*/
|
|
431
|
+
type UseWizardReturnType<S extends ReadonlyArray<StepSlot> = ReadonlyArray<StepSlot>> = {
|
|
432
|
+
readonly key: string;
|
|
433
|
+
readonly currentStep: CurrentStepOf<S>;
|
|
434
|
+
readonly activeForm: ActiveFormOf<S>;
|
|
435
|
+
readonly activeIndex: number;
|
|
436
|
+
readonly isFinalStep: boolean;
|
|
437
|
+
readonly steps: ReadonlyArray<CompiledStep>;
|
|
438
|
+
readonly forms: WizardForms<S>;
|
|
439
|
+
readonly count: number;
|
|
440
|
+
readonly statuses: WizardStatusesProxy<Record<string, FormStatus>>;
|
|
441
|
+
readonly allValues: Readonly<Record<FormKey, unknown>>;
|
|
442
|
+
readonly allErrors: Readonly<Record<FormKey, readonly AggregateError[]>>;
|
|
443
|
+
readonly progress: number;
|
|
444
|
+
readonly canAdvance: boolean;
|
|
445
|
+
readonly canGoBack: boolean;
|
|
446
|
+
readonly complete: boolean;
|
|
447
|
+
readonly done: boolean;
|
|
448
|
+
readonly submitting: boolean;
|
|
449
|
+
readonly submissionAttempts: number;
|
|
450
|
+
readonly visited: readonly FormKey[];
|
|
451
|
+
readonly next: () => Promise<void>;
|
|
452
|
+
readonly back: () => void;
|
|
453
|
+
readonly goTo: (key: string) => void;
|
|
454
|
+
readonly handleSubmit: (onSubmit: WizardOnSubmit, onError?: WizardOnError) => (event?: Event) => Promise<void>;
|
|
455
|
+
readonly reset: () => void;
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Schema-driven coercion of user-typed DOM values at the v-register
|
|
460
|
+
* directive layer. When the slim schema declares a numeric or
|
|
461
|
+
* boolean type at a path, the directive coerces incoming string
|
|
462
|
+
* values (`'25'` → `25`, `'true'` → `true`) before the slim-primitive
|
|
463
|
+
* gate sees the write — making the schema authoritative for storage
|
|
464
|
+
* shape and freeing consumers from sprinkling `.number` modifiers
|
|
465
|
+
* across templates.
|
|
466
|
+
*
|
|
467
|
+
* Coercion is consumer-extensible: a `CoercionRegistry` is just an
|
|
468
|
+
* `Array<CoercionEntry>` keyed at config time by `(input, output)`
|
|
469
|
+
* `SlimPrimitiveKind` literals. The library ships
|
|
470
|
+
* `defaultCoercionRules` (string→number, string→boolean) and
|
|
471
|
+
* `defineCoercion` for type-narrowed authoring; consumers spread the
|
|
472
|
+
* defaults to extend or supply their own array to replace.
|
|
473
|
+
*
|
|
474
|
+
* Coercion applies ONLY to user-typed DOM values flowing through
|
|
475
|
+
* the directive's assigner. Programmatic writes (`form.setValue`,
|
|
476
|
+
* `setValueWithInternalPath`) bypass coercion — they're authoritative
|
|
477
|
+
* writes whose strict typing is on the caller. This mirrors the
|
|
478
|
+
* `transforms` pipeline's user-input-only contract.
|
|
479
|
+
*/
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Type-narrowing helper for authoring entries. At runtime it's
|
|
483
|
+
* identity; at compile time it preserves the `input` / `output`
|
|
484
|
+
* literal types so `transform`'s parameter is narrowed to the
|
|
485
|
+
* runtime type instead of widening to `SlimRuntimeOf<SlimPrimitiveKind>`.
|
|
486
|
+
*
|
|
487
|
+
* Without this helper, authoring `{ input: 'string', output:
|
|
488
|
+
* 'number', transform: (s) => ... }` against the broader
|
|
489
|
+
* `CoercionEntry` widens `s` to `string | number | boolean | ...`,
|
|
490
|
+
* forcing a cast in every transform body. `defineCoercion` is the
|
|
491
|
+
* opaque-free idiom.
|
|
492
|
+
*/
|
|
493
|
+
declare function defineCoercion<I extends SlimPrimitiveKind, O extends SlimPrimitiveKind>(entry: CoercionEntry<I, O>): CoercionEntry<I, O>;
|
|
494
|
+
/**
|
|
495
|
+
* Internal index built from a `CoercionRegistry` at config-resolve
|
|
496
|
+
* time. Keyed by `${input}->${output}` for O(1) per-keystroke
|
|
497
|
+
* dispatch. The authoring shape (array, ergonomic, type-narrowing-
|
|
498
|
+
* friendly) and the dispatch shape (Map, fast) decouple cleanly.
|
|
499
|
+
*/
|
|
500
|
+
type CoercionIndex = ReadonlyMap<`${SlimPrimitiveKind}->${SlimPrimitiveKind}`, CoercionEntry>;
|
|
501
|
+
/**
|
|
502
|
+
* The library's built-in registry. Two cells: string→number and
|
|
503
|
+
* string→boolean. Re-exported so consumers can spread it when
|
|
504
|
+
* supplying a custom registry that extends defaults.
|
|
505
|
+
*/
|
|
506
|
+
declare const defaultCoercionRules: CoercionRegistry;
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Per-form closure state — the single store owned by each `useForm` call.
|
|
510
|
+
* Bundles the form value, the summary record, element references, field
|
|
511
|
+
* state, the meta tracker, and the error stores under one keyed-by-
|
|
512
|
+
* `(formKey, path)` instance so cross-form DOM state cannot collide.
|
|
513
|
+
*
|
|
514
|
+
* This is NOT a singleton. Each call to `useForm` creates its own FormStore
|
|
515
|
+
* instance and holds onto it via closure. The registry provides SSR
|
|
516
|
+
* hydration; otherwise the state is per-component-per-form.
|
|
517
|
+
*/
|
|
518
|
+
/**
|
|
519
|
+
* Per-path field status. Replaced wholesale (not mutated in place) on
|
|
520
|
+
* every change. Three semantic groups:
|
|
521
|
+
*
|
|
522
|
+
* - `connected` — is a DOM element registered for this path?
|
|
523
|
+
* - `focused` / `blurred` — DOM-state flags. `null` while no element
|
|
524
|
+
* is connected (no DOM means the concepts don't apply); plain
|
|
525
|
+
* booleans once connected, with the invariant `blurred === !focused`
|
|
526
|
+
* enforced by `markFocused`.
|
|
527
|
+
* - `touched` — interaction history, not DOM state. Always a plain
|
|
528
|
+
* boolean: `false` at registration, sticky `true` after first blur,
|
|
529
|
+
* cleared only by `form.reset()` / `form.resetField(path)`. Persists
|
|
530
|
+
* across disconnects so v-if'd-away fields don't lose their touched
|
|
531
|
+
* state on rehide (wizard "show review of touched fields" patterns
|
|
532
|
+
* rely on this).
|
|
533
|
+
*/
|
|
534
|
+
type FieldRecord = {
|
|
535
|
+
readonly path: Path;
|
|
536
|
+
readonly updatedAt: string | null;
|
|
537
|
+
readonly connected: boolean;
|
|
538
|
+
readonly focused: boolean | null;
|
|
539
|
+
readonly blurred: boolean | null;
|
|
540
|
+
readonly touched: boolean;
|
|
541
|
+
};
|
|
542
|
+
/** Per-path DOM element tracking. Client-only. */
|
|
543
|
+
type ElementRecord = {
|
|
544
|
+
/**
|
|
545
|
+
* Original Path captured at first registration. Stored alongside the
|
|
546
|
+
* elements Set so the DOM-order sort cache can recover the structured
|
|
547
|
+
* Path without round-tripping through `JSON.parse(pathKey)`.
|
|
548
|
+
*/
|
|
549
|
+
readonly path: Path;
|
|
550
|
+
readonly elements: Set<HTMLElement>;
|
|
551
|
+
};
|
|
552
|
+
/**
|
|
553
|
+
* Per-path record stored in `originals`. Pairing `segments` with the tracked
|
|
554
|
+
* value means `dirty` and `resetField`'s container loop don't have to
|
|
555
|
+
* `JSON.parse(pathKey)` on every iteration — the canonical Path is already
|
|
556
|
+
* sitting next to the value it belongs to. PathKey still keys the Map (the
|
|
557
|
+
* stable string is the only collision-free identifier), but downstream
|
|
558
|
+
* iteration reads `segments` directly.
|
|
559
|
+
*/
|
|
560
|
+
type OriginalsRecord = {
|
|
561
|
+
readonly segments: Path;
|
|
562
|
+
readonly value: unknown;
|
|
563
|
+
};
|
|
564
|
+
type FormStore<F extends GenericForm, G extends GenericForm = F> = {
|
|
565
|
+
readonly formKey: FormKey;
|
|
566
|
+
readonly form: Ref<F>;
|
|
567
|
+
readonly fields: Map<PathKey, FieldRecord>;
|
|
568
|
+
readonly elements: Map<PathKey, ElementRecord>;
|
|
569
|
+
/**
|
|
570
|
+
* Schema-driven errors. Written ONLY by the schema validation pipeline:
|
|
571
|
+
* `scheduleFieldValidation`, `handleSubmit`, the construction-time seed,
|
|
572
|
+
* history restore, and hydration. Cleared by `reset` / `resetField` and by
|
|
573
|
+
* a successful submit. `setFieldErrors*` APIs do NOT touch this Map.
|
|
574
|
+
*/
|
|
575
|
+
readonly schemaErrors: Map<PathKey, ValidationError[]>;
|
|
576
|
+
/**
|
|
577
|
+
* User-injected errors. Written ONLY by the `setFieldErrors*` API surfaces
|
|
578
|
+
* (and history / hydration replay). Survives schema revalidation and
|
|
579
|
+
* successful submits — the consumer owns its lifetime explicitly.
|
|
580
|
+
*/
|
|
581
|
+
readonly userErrors: Map<PathKey, ValidationError[]>;
|
|
582
|
+
/**
|
|
583
|
+
* Reactively-derived "No value supplied" errors. Pure function of
|
|
584
|
+
* `(blankPaths, schema.isRequiredAtPath)` — no writers, no clears.
|
|
585
|
+
* Membership tracks `blankPaths` automatically: typing a value into
|
|
586
|
+
* a blank required numeric field removes the path from `blankPaths`
|
|
587
|
+
* and the derived error vanishes; clearing the numeric input re-adds
|
|
588
|
+
* the path and the error reappears. The `errors` proxy and
|
|
589
|
+
* `getErrorsForPath` merge this map in alongside `schemaErrors` and
|
|
590
|
+
* `userErrors`, so consumers see the "this required field is empty"
|
|
591
|
+
* error the moment it's true — no `validate()` / `handleSubmit`
|
|
592
|
+
* call required. Honors the founding principle that
|
|
593
|
+
* `errors = f(schema, state)`.
|
|
594
|
+
*
|
|
595
|
+
* Most entries flow through this map for `number` / `bigint` leaves
|
|
596
|
+
* (where the side-channel is needed to distinguish "user typed 0"
|
|
597
|
+
* from "user supplied nothing"). String / boolean leaves only land
|
|
598
|
+
* here when the consumer explicitly opted in via the `unset`
|
|
599
|
+
* sentinel — see `docs/validation/blank.md`.
|
|
600
|
+
*/
|
|
601
|
+
readonly derivedBlankErrors: ComputedRef<ReadonlyMap<PathKey, ValidationError[]>>;
|
|
602
|
+
readonly originals: Map<PathKey, OriginalsRecord>;
|
|
603
|
+
/**
|
|
604
|
+
* Reactive set of paths whose displayed state should be EMPTY even
|
|
605
|
+
* though storage holds a real, schema-conformant value (the slim
|
|
606
|
+
* default). It exists exclusively to record **storage / display
|
|
607
|
+
* divergence** — the case where the runtime can't tell "user typed
|
|
608
|
+
* 0" from "user supplied nothing" by looking at storage alone.
|
|
609
|
+
*
|
|
610
|
+
* The mechanism shines for `number` / `bigint`: storage holds the
|
|
611
|
+
* slim default (`0` / `0n`) but the DOM input shows `''`, so the
|
|
612
|
+
* directive's input listener marks the path here on clear. Strings
|
|
613
|
+
* and booleans don't need it — `''` storage equals `''` display,
|
|
614
|
+
* `false` storage equals unchecked display — so they're never
|
|
615
|
+
* auto-marked. Consumers can still mark any primitive leaf
|
|
616
|
+
* explicitly via the `unset` sentinel (`defaultValues: { x: unset }`,
|
|
617
|
+
* `setValue('x', unset)`, `reset({ x: unset })`); the mark is then
|
|
618
|
+
* a documented signal of consumer intent rather than runtime
|
|
619
|
+
* inference.
|
|
620
|
+
*
|
|
621
|
+
* Reads (`displayValue` computed, `fields.<path>.blank`,
|
|
622
|
+
* `derivedBlankErrors` computed) track via Vue 3.5's reactive Set
|
|
623
|
+
* handlers. Writes happen inside `setValueAtPath` (gate-hook
|
|
624
|
+
* bookkeeping: `blank: true` meta adds the path; any other write
|
|
625
|
+
* removes it) and `reset`.
|
|
626
|
+
*
|
|
627
|
+
* Storage NEVER reflects this set — calculations and reads against
|
|
628
|
+
* `form.value` see the slim default. The set is purely a UI/intent
|
|
629
|
+
* channel that `derivedBlankErrors` consults to surface
|
|
630
|
+
* "No value supplied" errors for required schemas.
|
|
631
|
+
*
|
|
632
|
+
* See `docs/validation/blank.md` for the conceptual model.
|
|
633
|
+
*/
|
|
634
|
+
readonly blankPaths: Set<PathKey>;
|
|
635
|
+
/**
|
|
636
|
+
* Snapshot of `blankPaths` captured at construction (and
|
|
637
|
+
* re-captured on `reset(args)`). Used by dirty calculation: a path
|
|
638
|
+
* whose membership differs from the snapshot is dirty even if
|
|
639
|
+
* storage matches the original. Eagerly populated to avoid a "dirty
|
|
640
|
+
* on first read" race after construction.
|
|
641
|
+
*/
|
|
642
|
+
readonly originalBlankPaths: Set<PathKey>;
|
|
643
|
+
readonly schema: AbstractSchema<F, G>;
|
|
644
|
+
/**
|
|
645
|
+
* Server-side flag, plumbed in from `registry.ssr`. The
|
|
646
|
+
* `register()`-returned `markConnectedOptimistically()` reads this
|
|
647
|
+
* before flipping `connected: true`; on the client it's a no-op so
|
|
648
|
+
* the eventual directive lifecycle remains the source of truth.
|
|
649
|
+
*/
|
|
650
|
+
readonly ssr: boolean;
|
|
651
|
+
/**
|
|
652
|
+
* Resolved `shouldShowErrors` predicate driving `field.showErrors`
|
|
653
|
+
* and `form.meta.showErrors`. Resolved once at construction via
|
|
654
|
+
* `resolveShouldShowErrors(options.shouldShowErrors)` so the
|
|
655
|
+
* field-state computeds don't repeat the boolean-vs-function
|
|
656
|
+
* branch on every read. Boolean shorthand has already been lifted
|
|
657
|
+
* to a constant predicate by the time it lands here; `undefined`
|
|
658
|
+
* config falls through to `defaultShouldShowErrors`.
|
|
659
|
+
*/
|
|
660
|
+
readonly shouldShowErrors: ShouldShowErrors;
|
|
661
|
+
readonly submitting: Ref<boolean>;
|
|
662
|
+
readonly activeSubmissions: Ref<number>;
|
|
663
|
+
readonly submissionAttempts: Ref<number>;
|
|
664
|
+
/**
|
|
665
|
+
* `true` once a `handleSubmit` callback resolved without throwing.
|
|
666
|
+
* Independent of `submissionAttempts` — a failed submit increments
|
|
667
|
+
* attempts but leaves `submitted` at `false`. Cleared by `reset()`
|
|
668
|
+
* alongside the rest of the submission surface.
|
|
669
|
+
*/
|
|
670
|
+
readonly submitted: Ref<boolean>;
|
|
671
|
+
readonly submitError: Ref<unknown>;
|
|
672
|
+
readonly departAttempts: Ref<number>;
|
|
673
|
+
/**
|
|
674
|
+
* `true` while a function-form `defaultValues` factory is in flight.
|
|
675
|
+
* Stays `false` for plain-value `defaultValues`. Shared across every
|
|
676
|
+
* `useForm({ key })` call that resolves to this store — the second
|
|
677
|
+
* caller sees the first caller's hydration state.
|
|
678
|
+
*/
|
|
679
|
+
readonly hydrating: Ref<boolean>;
|
|
680
|
+
/**
|
|
681
|
+
* Error from the most recent function-form `defaultValues` factory.
|
|
682
|
+
* Normalized to a `ValidationError` (code `atta:hydration-failed`) so the
|
|
683
|
+
* shape matches `form.errors` / `form.meta.errors` entries. `null` when
|
|
684
|
+
* no factory has fired or the last one succeeded.
|
|
685
|
+
*/
|
|
686
|
+
readonly hydrateError: Ref<ValidationError | null>;
|
|
687
|
+
/**
|
|
688
|
+
* The function-form `defaultValues` factory, captured at the first
|
|
689
|
+
* `useForm({ key })` call that wired this store. `undefined` for
|
|
690
|
+
* plain-value forms. Read by `form.rehydrate()`.
|
|
691
|
+
*/
|
|
692
|
+
readonly defaultValuesFactory: Ref<(() => unknown | Promise<unknown>) | undefined>;
|
|
693
|
+
/**
|
|
694
|
+
* `true` once the form's effective defaults have been applied —
|
|
695
|
+
* sync `defaultValues` at construction, or async factory whose
|
|
696
|
+
* settle completed. Stays `false` for dormant lazy forms until they
|
|
697
|
+
* activate. Read by `useWizard` to decide whether seed status or
|
|
698
|
+
* live meta should surface.
|
|
699
|
+
*/
|
|
700
|
+
readonly defaultsResolved: Ref<boolean>;
|
|
701
|
+
/**
|
|
702
|
+
* `true` once the captured async factory has been kicked off (set
|
|
703
|
+
* synchronously by `activate()`, before the factory itself resolves).
|
|
704
|
+
* Distinct from `defaultsResolved`, which only flips after the factory
|
|
705
|
+
* settles. The pair lets the API surface tell "we've started" apart
|
|
706
|
+
* from "we're done."
|
|
707
|
+
*/
|
|
708
|
+
readonly activated: Ref<boolean>;
|
|
709
|
+
/**
|
|
710
|
+
* In-flight activation promise. Concurrent callers (cross-component
|
|
711
|
+
* SSR consumers, recursive factory reads, parallel `activate()`
|
|
712
|
+
* calls) receive the same promise, ensuring the factory runs once
|
|
713
|
+
* even under contention.
|
|
714
|
+
*/
|
|
715
|
+
readonly activationPromise: Ref<Promise<void> | undefined>;
|
|
716
|
+
/**
|
|
717
|
+
* Idempotent activation entrypoint. Fires the captured function-form
|
|
718
|
+
* `defaultValues` factory on first call and stores the in-flight
|
|
719
|
+
* promise. Subsequent calls return the same promise until the factory
|
|
720
|
+
* settles; thereafter calls return `Promise.resolve()`. Plain-value
|
|
721
|
+
* forms (no factory captured) always return a resolved promise. The
|
|
722
|
+
* public API surface routes all reactive interactions (getters and
|
|
723
|
+
* methods, except `key`) through this entrypoint so the form
|
|
724
|
+
* activates on first use.
|
|
725
|
+
*/
|
|
726
|
+
activate(): Promise<void>;
|
|
727
|
+
/**
|
|
728
|
+
* Re-fire the captured function-form `defaultValues` factory. Throws
|
|
729
|
+
* synchronously when no factory was captured (plain-value form).
|
|
730
|
+
* Resolves after `hydrating` flips back to `false`; consumers can
|
|
731
|
+
* `await form.rehydrate()` to gate UI on the fresh load.
|
|
732
|
+
*
|
|
733
|
+
* Does NOT touch dirty / touched / submit state — chain
|
|
734
|
+
* `form.reset()` if you want a clean baseline.
|
|
735
|
+
*/
|
|
736
|
+
rehydrate(): Promise<void>;
|
|
737
|
+
/**
|
|
738
|
+
* Incremented by every `reset()` call. The submit wrapper captures
|
|
739
|
+
* this at entry and skips writing `submitError` from a catch that
|
|
740
|
+
* fires *after* a reset — otherwise a reset-during-submit would
|
|
741
|
+
* visibly clear `submitError` and then have it reappear when the
|
|
742
|
+
* in-flight promise rejects.
|
|
743
|
+
*/
|
|
744
|
+
readonly submissionGeneration: Ref<number>;
|
|
745
|
+
/**
|
|
746
|
+
* Counts in-flight validation calls across every `validate()` ref and
|
|
747
|
+
* every `validateAsync(...)` / `handleSubmit` pre-check. `validating`
|
|
748
|
+
* on the public API mirrors `activeValidations.value > 0`. Tracked
|
|
749
|
+
* separately from submissions because a validate-while-submitting
|
|
750
|
+
* (e.g. a debounced field check overlapping a submit) needs to show
|
|
751
|
+
* the union of both surfaces.
|
|
752
|
+
*/
|
|
753
|
+
readonly activeValidations: Ref<number>;
|
|
754
|
+
/**
|
|
755
|
+
* `true` once the form has completed at least one validation pass
|
|
756
|
+
* — flips when `activeValidations` returns to 0 from any positive
|
|
757
|
+
* value. Until that happens, `meta.valid` and `field.valid` report
|
|
758
|
+
* `false` even when `schemaErrors.size === 0`, because the absence
|
|
759
|
+
* of errors at frame 1 is just "we haven't checked yet," not "we
|
|
760
|
+
* checked and it's clean."
|
|
761
|
+
*
|
|
762
|
+
* This closes the brief flash window for schemas where the slim
|
|
763
|
+
* default-derivation parse strips refinements (`.refine`,
|
|
764
|
+
* `.superRefine`, async validators): the slim parse passes, no
|
|
765
|
+
* construction-time errors land, and the queued microtask hasn't
|
|
766
|
+
* run yet — so without the gate, frame 1 paints the form as
|
|
767
|
+
* "valid" before the real verdict arrives a tick later.
|
|
768
|
+
*
|
|
769
|
+
* Initialized to `!strict`: non-strict consumers opt out of the
|
|
770
|
+
* validation pipeline by design, so locking them on
|
|
771
|
+
* `firstValidationDone === false` would defeat the opt-out.
|
|
772
|
+
* Reset is left untouched — the post-reset validation flips it
|
|
773
|
+
* back true on completion, same as the construction-time path.
|
|
774
|
+
*/
|
|
775
|
+
readonly firstValidationDone: Ref<boolean>;
|
|
776
|
+
/**
|
|
777
|
+
* `true` when the sub-schema rooted at `path` (or any of its
|
|
778
|
+
* descendants) declares async work — composes
|
|
779
|
+
* `schema.getSchemasAtPath(path)` with each candidate's
|
|
780
|
+
* `needsAsyncValidation()`, memoised per canonical path key for
|
|
781
|
+
* the lifetime of the FormStore. Used by `meta.valid` /
|
|
782
|
+
* `field.valid` to skip the `firstValidationDone` gate on subtrees
|
|
783
|
+
* that are fully synchronous: their verdict resolves at construction
|
|
784
|
+
* (or on the next per-field run) without waiting on a microtask, so
|
|
785
|
+
* honouring the form-wide gate would just play dumb about a known
|
|
786
|
+
* answer.
|
|
787
|
+
*/
|
|
788
|
+
pathHasAsyncValidation(path: Path): boolean;
|
|
789
|
+
/**
|
|
790
|
+
* Per-path counter of in-flight field-level validation runs.
|
|
791
|
+
* `field.validating` on `FieldState` mirrors
|
|
792
|
+
* `(fieldValidationCounts.get(key) ?? 0) > 0`.
|
|
793
|
+
*
|
|
794
|
+
* Incremented at the same point as `activeValidations` inside
|
|
795
|
+
* `scheduleFieldValidation`'s `run` closure (right before the schema
|
|
796
|
+
* call) and decremented in the matching `.finally` — so the per-path
|
|
797
|
+
* bookkeeping is exactly co-extensive with the form-wide counter for
|
|
798
|
+
* the field-scheduled branch. Whole-form `validate()` /
|
|
799
|
+
* `validateAsync()` runs touch `activeValidations` only; they don't
|
|
800
|
+
* have a single field path and so don't contribute here.
|
|
801
|
+
*
|
|
802
|
+
* Counter (not Set) because two runs for the same path can briefly
|
|
803
|
+
* overlap: when an in-flight run is aborted and a new run starts,
|
|
804
|
+
* the new run increments before the aborted run's `.finally`
|
|
805
|
+
* decrements. With `> 0` semantics the field stays "validating"
|
|
806
|
+
* across the abort/restart boundary.
|
|
807
|
+
*
|
|
808
|
+
* Reactive Map: Vue 3's `reactive(new Map())` proxy makes `.get()`,
|
|
809
|
+
* `.has()`, and `.size` track per-key, so the FieldState
|
|
810
|
+
* computed only re-runs when the count for ITS key changes.
|
|
811
|
+
*/
|
|
812
|
+
readonly fieldValidationCounts: Map<PathKey, number>;
|
|
813
|
+
/**
|
|
814
|
+
* Replace the form value wholesale. Optional `meta` is forwarded to
|
|
815
|
+
* every `onFormChange` listener so they can decide whether THIS write
|
|
816
|
+
* is one they care about — most importantly, the persistence layer
|
|
817
|
+
* only writes when `meta?.persist === true`. Internal callers that
|
|
818
|
+
* don't pass meta default to no-persist.
|
|
819
|
+
*/
|
|
820
|
+
applyFormReplacement(next: F, meta?: WriteMeta): void;
|
|
821
|
+
/**
|
|
822
|
+
* Set a single path's value. `meta` is forwarded to listeners via
|
|
823
|
+
* `applyFormReplacement` (see above). The directive's input handler
|
|
824
|
+
* computes `meta.persist` from the per-element opt-in registry; other
|
|
825
|
+
* internal call sites pass `meta.persist = hasAnyOptInForPath(path)`.
|
|
826
|
+
* Public `form.setValue` passes no meta.
|
|
827
|
+
*
|
|
828
|
+
* Returns `false` when the slim-primitive gate rejects the write
|
|
829
|
+
* (the value's primitive shape doesn't match the schema's slim
|
|
830
|
+
* shape at the path). The store is unchanged in that case.
|
|
831
|
+
*/
|
|
832
|
+
setValueAtPath(path: Path, value: unknown, meta?: WriteMeta): boolean;
|
|
833
|
+
getValueAtPath(path: Path): unknown;
|
|
834
|
+
reset(nextDefaultValues?: DeepPartial<WriteShape<F>>): void;
|
|
835
|
+
resetField(path: Path): void;
|
|
836
|
+
/**
|
|
837
|
+
* Wipe `path` (or the whole form when `path === ''`) to the
|
|
838
|
+
* schema's "appropriate nullish value" — the underlying type's
|
|
839
|
+
* empty/falsy concrete, with `.default()` / `.catch()` wrappers
|
|
840
|
+
* INTENTIONALLY skipped. Sugar for
|
|
841
|
+
* `setValueAtPath(path, schema.getEmptyValueAtPath(path))`.
|
|
842
|
+
*/
|
|
843
|
+
clear(path: Path): boolean;
|
|
844
|
+
setSchemaErrorsForPath(path: Path, errors: ValidationError[]): void;
|
|
845
|
+
setAllSchemaErrors(errors: readonly ValidationError[]): void;
|
|
846
|
+
clearSchemaErrors(path?: Path): void;
|
|
847
|
+
/**
|
|
848
|
+
* Replace `schemaErrors` under `path` with `errors`, keying each
|
|
849
|
+
* error by its OWN absolute path. Used by validation pipelines
|
|
850
|
+
* (scheduleFieldValidation, validateAsync, handleSubmit, reset)
|
|
851
|
+
* to commit a parse result wholesale — entries not in the new
|
|
852
|
+
* pass get dropped from the subtree, surviving keys update in
|
|
853
|
+
* place to preserve insertion order. Pass `path === []` for the
|
|
854
|
+
* whole-form scope.
|
|
855
|
+
*/
|
|
856
|
+
applySchemaErrorsForSubtree(path: Path, errors: ValidationError[]): void;
|
|
857
|
+
setAllUserErrors(errors: readonly ValidationError[]): void;
|
|
858
|
+
addUserErrors(errors: readonly ValidationError[]): void;
|
|
859
|
+
clearUserErrors(path?: Path): void;
|
|
860
|
+
/**
|
|
861
|
+
* Merged read — returns `[...schemaErrors[path], ...userErrors[path]]`.
|
|
862
|
+
* Schema errors come first (structural validation before business logic),
|
|
863
|
+
* matching the iteration order for `getFirstErrorElement` and the
|
|
864
|
+
* top-level `errors` drillable Proxy.
|
|
865
|
+
*/
|
|
866
|
+
getErrorsForPath(path: Path): ValidationError[];
|
|
867
|
+
/**
|
|
868
|
+
* Returns a stable schema-declaration ordinal for `key`, assigning a
|
|
869
|
+
* fresh one if the path hasn't been seen before. Drives
|
|
870
|
+
* `form.meta.errors` sort order so the aggregate is a function of the
|
|
871
|
+
* SET of errors currently present (not the temporal order their
|
|
872
|
+
* Map keys were last `set`). Construction-time seed walks every leaf
|
|
873
|
+
* in the schema's slim default; runtime callers (DU variant 2, dynamic
|
|
874
|
+
* array indices, refines targeting cross-field paths) pick up
|
|
875
|
+
* first-encounter ordinals and keep them for the form's lifetime.
|
|
876
|
+
*/
|
|
877
|
+
ensurePathOrdinal(key: PathKey): number;
|
|
878
|
+
/**
|
|
879
|
+
* Register `element` as a binding for `path`, tagged with the calling
|
|
880
|
+
* `useForm()` instance's `formInstanceId`. The ID is the disambiguator
|
|
881
|
+
* used by `getFirstErrorElement` to scope focus / scroll to elements
|
|
882
|
+
* THIS form instance owns — important when two `useForm()` calls share
|
|
883
|
+
* a `key` (e.g. sidebar + main rendering the same form), since both
|
|
884
|
+
* write into one shared element store.
|
|
885
|
+
*/
|
|
886
|
+
registerElement(path: Path, element: HTMLElement, formInstanceId: string): boolean;
|
|
887
|
+
deregisterElement(path: Path, element: HTMLElement): number;
|
|
888
|
+
/**
|
|
889
|
+
* Optional `meta.instance` carries per-`useForm()`-instance overrides
|
|
890
|
+
* for `validateOn` / `debounceMs` so the blur-trigger respects the
|
|
891
|
+
* caller's config when sibling instances share a FormStore.
|
|
892
|
+
*/
|
|
893
|
+
markFocused(path: Path, focused: boolean, meta?: {
|
|
894
|
+
readonly instance?: WriteMeta['instance'];
|
|
895
|
+
}): void;
|
|
896
|
+
markTouched(path: Path): void;
|
|
897
|
+
/**
|
|
898
|
+
* Walk every active-variant leaf under `segments` and flip
|
|
899
|
+
* `touched: true`. Powers `form.touch(path?)`. Idempotent;
|
|
900
|
+
* does not mutate value / focused / blurred or trigger validation.
|
|
901
|
+
*/
|
|
902
|
+
touchAtPath(segments: Path): void;
|
|
903
|
+
/**
|
|
904
|
+
* SSR-only optimistic mark: flip `connected: true` on the field
|
|
905
|
+
* record without an actual DOM element. Called by the `vRegisterHint`
|
|
906
|
+
* compile-time transform via `RegisterValue.markConnectedOptimistically()`
|
|
907
|
+
* for every element rendered with `v-register`. Idempotent + no-op on
|
|
908
|
+
* the client (the directive's `created` hook is the authoritative
|
|
909
|
+
* source there).
|
|
910
|
+
*/
|
|
911
|
+
markConnectedOptimistically(path: Path): void;
|
|
912
|
+
/**
|
|
913
|
+
* Leaf-only pristine check. `originals` is populated via
|
|
914
|
+
* `diffAndApply`'s `added` patches, which fire only on primitive
|
|
915
|
+
* leaves — a container path (e.g. `['profile']`) that isn't in
|
|
916
|
+
* `originals` returns `true` here even when a descendant is dirty.
|
|
917
|
+
* Callers that need container semantics should either loop over
|
|
918
|
+
* leaves or walk `originals` manually. The public `getFieldState`
|
|
919
|
+
* surface is typed to accept leaf paths only, so in practice this
|
|
920
|
+
* isn't exposed to consumers.
|
|
921
|
+
*/
|
|
922
|
+
isPristineAtPath(path: Path): boolean;
|
|
923
|
+
getFieldRecord(path: Path): FieldRecord | undefined;
|
|
924
|
+
getOriginalAtPath(path: Path): unknown;
|
|
925
|
+
/**
|
|
926
|
+
* Returns the first errored field's first connected, visible DOM
|
|
927
|
+
* element scoped to `formInstanceId` — the target that
|
|
928
|
+
* `focusFirstError` / `scrollToFirstError` act on. "First" is
|
|
929
|
+
* VISUAL-first (DOM-tree order via `compareDocumentPosition`), not
|
|
930
|
+
* schema-declaration order, so a field rendered above another in the
|
|
931
|
+
* template focuses first regardless of which one the schema declared
|
|
932
|
+
* earlier. CSS `order:` flexbox/grid reordering is NOT respected
|
|
933
|
+
* (DOM-tree order wins) — documented as a tradeoff against forcing
|
|
934
|
+
* sync layout on every comparison.
|
|
935
|
+
*
|
|
936
|
+
* The `formInstanceId` filter scopes focus to elements registered
|
|
937
|
+
* through THIS form instance. When two `useForm({ key })` calls share
|
|
938
|
+
* a key, both register into the same element store; without the
|
|
939
|
+
* filter, the sidebar form's submit could focus the main form's
|
|
940
|
+
* input. With it, each `useForm()` callsite focuses only its own
|
|
941
|
+
* elements.
|
|
942
|
+
*
|
|
943
|
+
* Returns `null` when every errored path has no currently-attached
|
|
944
|
+
* element registered to this instance (fields behind `v-if="false"`,
|
|
945
|
+
* unmounted components, or a hidden `display:none` parent). Callers
|
|
946
|
+
* get the choice of no-op or a dev-only warning.
|
|
947
|
+
*/
|
|
948
|
+
getFirstErrorElement(formInstanceId: string): {
|
|
949
|
+
path: Path;
|
|
950
|
+
element: HTMLElement;
|
|
951
|
+
} | null;
|
|
952
|
+
/**
|
|
953
|
+
* Cancel every in-flight field-level validation run — clears timers
|
|
954
|
+
* for debounced 'change' runs that haven't fired, aborts controllers
|
|
955
|
+
* for runs whose async parse is in flight. Called by `handleSubmit`
|
|
956
|
+
* at entry (submit validation is authoritative) and by `reset()`.
|
|
957
|
+
*/
|
|
958
|
+
cancelFieldValidation(): void;
|
|
959
|
+
/**
|
|
960
|
+
* Kick off (or schedule) a field-level validation run for `path`. Pass
|
|
961
|
+
* `path = []` to cover the whole form; `applySchemaErrorsForSubtree`
|
|
962
|
+
* then wipes every `schemaErrors` entry and replaces them with the
|
|
963
|
+
* adapter's full async response. Used by persistence's post-hydration
|
|
964
|
+
* revalidation and by the construction-time async-refine seed.
|
|
965
|
+
*
|
|
966
|
+
* `immediate: true` skips the debounce window — the runtime kicks off
|
|
967
|
+
* the adapter call on the next microtask. Internal callsites use this
|
|
968
|
+
* for one-shot triggers; the per-keystroke writers pass `false` to
|
|
969
|
+
* coalesce rapid mutations under the configured debounceMs.
|
|
970
|
+
*
|
|
971
|
+
* `override` carries per-`useForm()`-instance values: when provided,
|
|
972
|
+
* the scheduler honors `override.mode` instead of the store's
|
|
973
|
+
* captured `validateOn`, and `override.debounceMs` instead of the
|
|
974
|
+
* store's captured `debounceMs`. Used so sibling instances sharing a
|
|
975
|
+
* FormStore can each validate on their own cadence.
|
|
976
|
+
*/
|
|
977
|
+
scheduleFieldValidation(path: Path, immediate: boolean, override?: {
|
|
978
|
+
readonly mode?: ValidateOn;
|
|
979
|
+
readonly debounceMs?: number;
|
|
980
|
+
}): void;
|
|
981
|
+
/**
|
|
982
|
+
* Subscribe to every `applyFormReplacement`. Fires synchronously
|
|
983
|
+
* after `form.value` has been swapped to `next` and all field /
|
|
984
|
+
* originals bookkeeping has run. Used by persistence + undo/redo
|
|
985
|
+
* to hook the single mutation funnel. The optional `meta` carries
|
|
986
|
+
* the originating call site's intent — the persistence subscription
|
|
987
|
+
* filters on `meta?.persist === true`; subscribers that don't care
|
|
988
|
+
* about meta can ignore the parameter. Returns an unsubscribe
|
|
989
|
+
* function.
|
|
990
|
+
*/
|
|
991
|
+
onFormChange(listener: (next: F, meta?: WriteMeta) => void): () => void;
|
|
992
|
+
/**
|
|
993
|
+
* Subscribe to successful submissions. Fires after the consumer's
|
|
994
|
+
* `onSubmit` callback has resolved — not on validation failure,
|
|
995
|
+
* not on callback throw. Used by persistence's `clearOnSubmitSuccess`
|
|
996
|
+
* to drop the stored payload once the form is safely through the
|
|
997
|
+
* server round-trip. Returns an unsubscribe function.
|
|
998
|
+
*/
|
|
999
|
+
onSubmitSuccess(listener: () => void): () => void;
|
|
1000
|
+
/**
|
|
1001
|
+
* Subscribe to `reset()` calls. Fires AFTER reset has replaced
|
|
1002
|
+
* the form and cleared errors + lifecycle, so listeners see the
|
|
1003
|
+
* fresh post-reset state. Used by the history module to drop the
|
|
1004
|
+
* undo/redo stack on reset. Returns an unsubscribe function.
|
|
1005
|
+
*/
|
|
1006
|
+
onReset(listener: () => void): () => void;
|
|
1007
|
+
/**
|
|
1008
|
+
* Internal: notify submit-success subscribers. Called by
|
|
1009
|
+
* `handleSubmit` in `process-form.ts` once the user callback has
|
|
1010
|
+
* resolved. Consumers shouldn't call this directly.
|
|
1011
|
+
*/
|
|
1012
|
+
emitSubmitSuccess(): void;
|
|
1013
|
+
/**
|
|
1014
|
+
* Register a teardown function whose lifetime is bound to the
|
|
1015
|
+
* FormStore itself (not a consumer's Vue effect scope). Called by
|
|
1016
|
+
* `dispose()` when the last consumer unmounts. Used by persistence /
|
|
1017
|
+
* history wiring so their subscribers aren't detached prematurely
|
|
1018
|
+
* when only the first consumer unmounts but others remain.
|
|
1019
|
+
*/
|
|
1020
|
+
registerCleanup(fn: () => void): void;
|
|
1021
|
+
/**
|
|
1022
|
+
* Register an async drain function. Called by the registry before
|
|
1023
|
+
* `dispose()` so async background work — chiefly the persistence
|
|
1024
|
+
* layer's debounced storage writes — has a chance to settle without
|
|
1025
|
+
* losing the last keystroke. Each registered function is awaited in
|
|
1026
|
+
* parallel; failures are swallowed to keep eviction reliable.
|
|
1027
|
+
*/
|
|
1028
|
+
registerDrain(fn: () => Promise<void>): void;
|
|
1029
|
+
/**
|
|
1030
|
+
* Drain async work registered via `registerDrain`. Resolves once
|
|
1031
|
+
* every registered drain has settled (in parallel). Safe to call
|
|
1032
|
+
* repeatedly — registered drains decide their own idempotency.
|
|
1033
|
+
*/
|
|
1034
|
+
awaitPendingWrites(): Promise<void>;
|
|
1035
|
+
/**
|
|
1036
|
+
* Cache for per-state modules (history, persistence) that must
|
|
1037
|
+
* outlive any single consumer. Subsequent `useForm` / `injectForm`
|
|
1038
|
+
* calls for the same key read from this map so the public API shape
|
|
1039
|
+
* is identical regardless of mount order. Keyed by a string identifier
|
|
1040
|
+
* owned by the caller (e.g. `'history'`).
|
|
1041
|
+
*/
|
|
1042
|
+
readonly modules: Map<string, unknown>;
|
|
1043
|
+
/**
|
|
1044
|
+
* Per-element persistence opt-in tracker. Empty by default; the
|
|
1045
|
+
* `v-register` directive populates entries on `mount` for each binding
|
|
1046
|
+
* that passed `register('foo', { persist: true })` and clears them on
|
|
1047
|
+
* `beforeUnmount`. Two SFCs sharing a key share this registry — opt-ins
|
|
1048
|
+
* are per-DOM-element, not per-component. Internal to the persistence
|
|
1049
|
+
* subsystem; not part of the consumer API surface.
|
|
1050
|
+
*/
|
|
1051
|
+
readonly persistOptIns: PersistOptInRegistry;
|
|
1052
|
+
/**
|
|
1053
|
+
* Resolved sensitive-path predicate for THIS form. Honors the
|
|
1054
|
+
* cascade (`useForm({ sensitiveNames })` > global default >
|
|
1055
|
+
* library `DEFAULT_SENSITIVE_NAMES`). Used by:
|
|
1056
|
+
* - persistence enforcement (`enforceSensitiveCheck` at write time);
|
|
1057
|
+
* - the multi-tab sync module (outbound strip + inbound reject);
|
|
1058
|
+
* - DevTools edit rejection;
|
|
1059
|
+
* - any future surface that needs to flag "this path holds
|
|
1060
|
+
* sensitive data."
|
|
1061
|
+
*
|
|
1062
|
+
* Frozen at FormStore construction. Two callsites sharing a key
|
|
1063
|
+
* share the predicate — consistent with the rest of the per-form
|
|
1064
|
+
* resolved-config surface.
|
|
1065
|
+
*/
|
|
1066
|
+
readonly isSensitivePath: (path: Path | PathKey | string) => boolean;
|
|
1067
|
+
/**
|
|
1068
|
+
* Single-segment variant of `isSensitivePath`. Used by the DevTools
|
|
1069
|
+
* redact walk to short-circuit whole subtrees the moment any
|
|
1070
|
+
* ancestor segment matches — saving an O(leaves × ancestors) regex
|
|
1071
|
+
* sweep per timeline event. Resolved from the same `sensitiveNames`
|
|
1072
|
+
* cascade as `isSensitivePath`.
|
|
1073
|
+
*/
|
|
1074
|
+
readonly segmentMatchesSensitive: (segment: Segment) => boolean;
|
|
1075
|
+
/**
|
|
1076
|
+
* Canonical path keys explicitly opted OUT of multi-tab sync by
|
|
1077
|
+
* `register(path, { multiTab: false })`. The sync module's outbound
|
|
1078
|
+
* broadcaster strips patches at these paths AND the inbound listener
|
|
1079
|
+
* rejects them — symmetric tab-local behaviour for selected fields.
|
|
1080
|
+
*
|
|
1081
|
+
* Read-only Set view; mutate via `incrementNoSyncOptOut` /
|
|
1082
|
+
* `decrementNoSyncOptOut` which maintain a per-path ref count so
|
|
1083
|
+
* multiple bindings on the same path balance correctly across
|
|
1084
|
+
* dynamic conditional renders. Empty by default.
|
|
1085
|
+
*/
|
|
1086
|
+
readonly noSyncPaths: ReadonlySet<PathKey>;
|
|
1087
|
+
/**
|
|
1088
|
+
* Ref-counted "this path is tab-local" registration. Called by
|
|
1089
|
+
* `v-register`'s `created` hook for any binding that declared
|
|
1090
|
+
* `register('x', { multiTab: false })`. The first call for a given
|
|
1091
|
+
* path adds it to `noSyncPaths`; subsequent calls just bump the
|
|
1092
|
+
* ref count. Pair with `decrementNoSyncOptOut`.
|
|
1093
|
+
*/
|
|
1094
|
+
incrementNoSyncOptOut(path: PathKey): void;
|
|
1095
|
+
/**
|
|
1096
|
+
* Symmetric companion to `incrementNoSyncOptOut`. Called by
|
|
1097
|
+
* `v-register`'s `beforeUnmount` hook. When the ref count for a
|
|
1098
|
+
* path drops to zero, the path is removed from `noSyncPaths` —
|
|
1099
|
+
* dynamic toggling (the binding rendered conditionally) restores
|
|
1100
|
+
* full sync to the path when the last opt-out unmounts.
|
|
1101
|
+
*/
|
|
1102
|
+
decrementNoSyncOptOut(path: PathKey): void;
|
|
1103
|
+
/**
|
|
1104
|
+
* Resolved schema-coercion index — the merged config from
|
|
1105
|
+
* `createAttaform({ defaults: { coerce } })` ∪ `useForm({ coerce })`,
|
|
1106
|
+
* keyed by `${input}->${output}` for O(1) per-keystroke dispatch.
|
|
1107
|
+
* Empty Map when coercion is disabled. Read at `register()` time
|
|
1108
|
+
* by `buildCoerceFn` to bake the per-path coerce closure on
|
|
1109
|
+
* `RegisterValue.coerce`.
|
|
1110
|
+
*/
|
|
1111
|
+
readonly coerceIndex: CoercionIndex;
|
|
1112
|
+
/**
|
|
1113
|
+
* Tear down non-reactive resources owned by this FormStore. Invoked
|
|
1114
|
+
* by the registry when the last consumer unmounts. Cancels pending
|
|
1115
|
+
* field-validation timers, drops every subscriber, and fires each
|
|
1116
|
+
* cleanup hook registered via `registerCleanup`.
|
|
1117
|
+
*/
|
|
1118
|
+
dispose(): void;
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Portable SSR detection. The plugin captures this value at install time and
|
|
1123
|
+
* exposes it via the registry so every runtime branch reads a single source
|
|
1124
|
+
* of truth instead of sniffing `import.meta.*` (bundler-specific) at each
|
|
1125
|
+
* call site.
|
|
1126
|
+
*
|
|
1127
|
+
* Consumers can override the heuristic explicitly via
|
|
1128
|
+
* `createAttaform({ ssr: true })`; the default handles the common
|
|
1129
|
+
* Node-vs-browser split without relying on any bundler-injected flag.
|
|
1130
|
+
*/
|
|
1131
|
+
interface SSRDetectOptions {
|
|
1132
|
+
/**
|
|
1133
|
+
* Force SSR-vs-client mode, bypassing the `typeof window` heuristic.
|
|
1134
|
+
* `true` activates the SSR code paths (no devtools, no persistence
|
|
1135
|
+
* wiring, payload serialisation enabled); `false` forces client mode.
|
|
1136
|
+
* The Nuxt plugin sets this from `import.meta.server` so SSR detection
|
|
1137
|
+
* never depends on whether `window` is polyfilled. Tests that need to
|
|
1138
|
+
* exercise the SSR code paths under jsdom pass `ssr: true`.
|
|
1139
|
+
*/
|
|
1140
|
+
ssr?: boolean;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Per-Vue-app container for all form state instances. Each
|
|
1145
|
+
* `app.use(createAttaform())` call gets its own registry,
|
|
1146
|
+
* so the library runs under bare Vue 3 + SSR (via
|
|
1147
|
+
* `@vue/server-renderer`) and Nuxt with the same code path.
|
|
1148
|
+
*
|
|
1149
|
+
* Each form's state lives in `forms: Map<FormKey, FormStore<GenericForm>>`.
|
|
1150
|
+
* The type relaxation at storage time is necessary because different
|
|
1151
|
+
* forms in the same app have different `Form` generics; callers recover
|
|
1152
|
+
* the specific form type via `useForm`'s overloads.
|
|
1153
|
+
*/
|
|
1154
|
+
/**
|
|
1155
|
+
* Serialised snapshot of one form's state, captured by
|
|
1156
|
+
* `renderAttaformState` for SSR and replayed by
|
|
1157
|
+
* `hydrateAttaformState` on the client. Round-trips through
|
|
1158
|
+
* JSON-safe tuples; field references are intentionally omitted
|
|
1159
|
+
* (DOM nodes don't survive serialisation).
|
|
1160
|
+
*/
|
|
1161
|
+
type SerializedFormData = {
|
|
1162
|
+
/** The form's value at snapshot time. */
|
|
1163
|
+
readonly form: unknown;
|
|
1164
|
+
/**
|
|
1165
|
+
* Errors produced by the schema at snapshot time. Replayed into
|
|
1166
|
+
* the client form's error state at hydration; cleared on
|
|
1167
|
+
* successful re-validation client-side.
|
|
1168
|
+
*/
|
|
1169
|
+
readonly schemaErrors: ReadonlyArray<readonly [string, unknown]>;
|
|
1170
|
+
/**
|
|
1171
|
+
* Errors set explicitly via `setFieldErrors` / `addFieldErrors`
|
|
1172
|
+
* (typically from a server response parsed via `parseApiErrors`)
|
|
1173
|
+
* at snapshot time. Replayed at hydration; persists across
|
|
1174
|
+
* client-side re-validation.
|
|
1175
|
+
*/
|
|
1176
|
+
readonly userErrors: ReadonlyArray<readonly [string, unknown]>;
|
|
1177
|
+
/** Per-field metadata (timestamps, raw values, connection flags) captured at snapshot time. */
|
|
1178
|
+
readonly fields: ReadonlyArray<readonly [string, unknown]>;
|
|
1179
|
+
/**
|
|
1180
|
+
* Path keys that were in the form's `blankPaths` set at
|
|
1181
|
+
* snapshot time. Round-trips the "displayed empty" UI state across
|
|
1182
|
+
* the SSR boundary — without it, the client briefly renders
|
|
1183
|
+
* `String(slim-default)` (e.g. `'0'`) for fields the server
|
|
1184
|
+
* rendered as blank. Optional in the wire format so older payload
|
|
1185
|
+
* shapes deserialise cleanly.
|
|
1186
|
+
*/
|
|
1187
|
+
readonly blankPaths?: ReadonlyArray<string>;
|
|
1188
|
+
};
|
|
1189
|
+
type PendingHydration = Map<FormKey, SerializedFormData>;
|
|
1190
|
+
/**
|
|
1191
|
+
* The library's per-Vue-app container. One `AttaformRegistry` is
|
|
1192
|
+
* created per `app.use(createAttaform())` call.
|
|
1193
|
+
*
|
|
1194
|
+
* Most consumers never touch this directly — `useForm` and
|
|
1195
|
+
* `injectForm` reach the registry on your behalf. Access it
|
|
1196
|
+
* explicitly only when wiring SSR or a custom plugin integration.
|
|
1197
|
+
*/
|
|
1198
|
+
type AttaformRegistry = {
|
|
1199
|
+
/**
|
|
1200
|
+
* Live forms keyed by `FormKey`.
|
|
1201
|
+
* @internal
|
|
1202
|
+
*/
|
|
1203
|
+
readonly forms: Map<FormKey, FormStore<GenericForm>>;
|
|
1204
|
+
/**
|
|
1205
|
+
* Live wizards keyed by the consumer-supplied `key` option. Populated
|
|
1206
|
+
* by `useWizard(entryForm, { key })`; consulted by `injectWizard(key)` to
|
|
1207
|
+
* resolve cross-component wizard handles. Anonymous wizards (no
|
|
1208
|
+
* `key`) do NOT register here — they're reachable only via ambient
|
|
1209
|
+
* provide/inject.
|
|
1210
|
+
* @internal
|
|
1211
|
+
*/
|
|
1212
|
+
readonly wizards: Map<string, UseWizardReturnType>;
|
|
1213
|
+
/**
|
|
1214
|
+
* Snapshots staged by `hydrateAttaformState` waiting to be consumed by the next `useForm` call.
|
|
1215
|
+
* @internal
|
|
1216
|
+
*/
|
|
1217
|
+
readonly pendingHydration: PendingHydration;
|
|
1218
|
+
/** `true` while running on the server during SSR; `false` on the client. */
|
|
1219
|
+
readonly ssr: boolean;
|
|
1220
|
+
/** App-level defaults applied to every `useForm` call. */
|
|
1221
|
+
readonly defaults: AttaformDefaults;
|
|
1222
|
+
/**
|
|
1223
|
+
* Track a consumer of `key`. Returns a dispose function — call it
|
|
1224
|
+
* when the consumer unmounts. The form is evicted automatically
|
|
1225
|
+
* when the last consumer disposes, so long-running SPAs don't
|
|
1226
|
+
* leak detached state across navigations.
|
|
1227
|
+
* @internal
|
|
1228
|
+
*/
|
|
1229
|
+
readonly trackConsumer: (key: FormKey) => () => void;
|
|
1230
|
+
/**
|
|
1231
|
+
* Track a consumer of wizard `key`. Returns a dispose function — call
|
|
1232
|
+
* it when the consumer unmounts. The wizard handle is evicted from
|
|
1233
|
+
* `wizards` once the last consumer disposes, mirroring the form
|
|
1234
|
+
* consumer-counting mechanics. Anonymous wizards never enter this
|
|
1235
|
+
* counter (they have no key to count under).
|
|
1236
|
+
* @internal
|
|
1237
|
+
*/
|
|
1238
|
+
readonly trackWizardConsumer: (key: string) => () => void;
|
|
1239
|
+
/**
|
|
1240
|
+
* Mark a form as eligible for SSR prefetch. The form's
|
|
1241
|
+
* `onServerPrefetch` hook consults `shouldPrefetch(key)` and runs the
|
|
1242
|
+
* captured `defaultValues` factory only when this set contains the
|
|
1243
|
+
* key (and the skip set does not). Set by `form.activate()`,
|
|
1244
|
+
* `useWizard`'s current-step auto-mark, and the future Vite transform.
|
|
1245
|
+
* @internal
|
|
1246
|
+
*/
|
|
1247
|
+
readonly enqueuePrefetch: (key: FormKey) => void;
|
|
1248
|
+
/**
|
|
1249
|
+
* Mark a form as ineligible for SSR prefetch. Overrides `enqueuePrefetch`.
|
|
1250
|
+
* Used by `useWizard` to keep non-current steps dormant on the
|
|
1251
|
+
* server even when a transform mark or stray `activate()` would
|
|
1252
|
+
* otherwise enqueue them — the wizard's "user isn't on this step"
|
|
1253
|
+
* signal wins.
|
|
1254
|
+
* @internal
|
|
1255
|
+
*/
|
|
1256
|
+
readonly skipPrefetch: (key: FormKey) => void;
|
|
1257
|
+
/**
|
|
1258
|
+
* Whether `key`'s SSR prefetch should run. Returns `true` iff the key
|
|
1259
|
+
* is enqueued AND not skipped.
|
|
1260
|
+
* @internal
|
|
1261
|
+
*/
|
|
1262
|
+
readonly shouldPrefetch: (key: FormKey) => boolean;
|
|
1263
|
+
/**
|
|
1264
|
+
* Wait for all pending persistence writes across every live form
|
|
1265
|
+
* to settle. Useful for SSR shutdown and integration tests that
|
|
1266
|
+
* need a deterministic teardown.
|
|
1267
|
+
* @internal
|
|
1268
|
+
*/
|
|
1269
|
+
readonly shutdown: () => Promise<void>;
|
|
1270
|
+
};
|
|
1271
|
+
/**
|
|
1272
|
+
* The Vue `InjectionKey` under which the registry is provided on the
|
|
1273
|
+
* app. Most consumers never need this — `useForm` and
|
|
1274
|
+
* `injectForm` resolve the registry automatically.
|
|
1275
|
+
*/
|
|
1276
|
+
declare const kAttaformRegistry: InjectionKey<AttaformRegistry>;
|
|
1277
|
+
declare module 'vue' {
|
|
1278
|
+
interface App {
|
|
1279
|
+
/** @internal */
|
|
1280
|
+
_attaform?: AttaformRegistry;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
/** Options for `createRegistry`. */
|
|
1284
|
+
type CreateRegistryOptions = SSRDetectOptions & {
|
|
1285
|
+
/**
|
|
1286
|
+
* App-level defaults applied to every `useForm` call. Per-form
|
|
1287
|
+
* options always win. Omitted is equivalent to `{}`.
|
|
1288
|
+
*/
|
|
1289
|
+
defaults?: AttaformDefaults;
|
|
1290
|
+
};
|
|
1291
|
+
/**
|
|
1292
|
+
* Create a fresh `AttaformRegistry`. `createAttaform()` calls
|
|
1293
|
+
* this internally — most consumers never need to call it directly.
|
|
1294
|
+
* Use it when building a custom plugin that doesn't want the
|
|
1295
|
+
* `createAttaform` plugin's auto-install behaviour (e.g. test
|
|
1296
|
+
* harnesses, embedded apps).
|
|
1297
|
+
*/
|
|
1298
|
+
declare function createRegistry(options?: CreateRegistryOptions): AttaformRegistry;
|
|
1299
|
+
/**
|
|
1300
|
+
* Look up the current app's registry from inside a component's
|
|
1301
|
+
* `setup()` (or any synchronous code on the setup call stack).
|
|
1302
|
+
*
|
|
1303
|
+
* Most consumers don't need this — `useForm` and `injectForm`
|
|
1304
|
+
* call it on your behalf. Reach for it directly when building
|
|
1305
|
+
* custom integrations that need the raw registry.
|
|
1306
|
+
*
|
|
1307
|
+
* Throws:
|
|
1308
|
+
* - `OutsideSetupError` when called outside a Vue setup context
|
|
1309
|
+
* (e.g. from an event handler or async callback). Move the call
|
|
1310
|
+
* into setup, or trigger it from a child component.
|
|
1311
|
+
* - `RegistryNotInstalledError` when called inside setup but the
|
|
1312
|
+
* plugin wasn't installed. Add
|
|
1313
|
+
* `app.use(createAttaform())` to your app entry.
|
|
1314
|
+
*/
|
|
1315
|
+
declare function useRegistry(): AttaformRegistry;
|
|
1316
|
+
/**
|
|
1317
|
+
* Look up a Vue app's registry by `App` reference. Used by
|
|
1318
|
+
* SSR helpers (`renderAttaformState`, `hydrateAttaformState`) that
|
|
1319
|
+
* run outside a component setup context.
|
|
1320
|
+
*
|
|
1321
|
+
* Throws `RegistryNotInstalledError` when the app hasn't been wired
|
|
1322
|
+
* with `createAttaform()`.
|
|
1323
|
+
*/
|
|
1324
|
+
declare function getRegistryFromApp(app: App): AttaformRegistry;
|
|
1325
|
+
|
|
1326
|
+
/**
|
|
1327
|
+
* Options accepted by `injectForm` when passing an object instead of
|
|
1328
|
+
* a bare key string. `__ssrAccessed: true` is set by the Phase 3
|
|
1329
|
+
* `attaform-vite` transform on descendant calls whose template reads
|
|
1330
|
+
* the injected form's reactive state — it tells the runtime to
|
|
1331
|
+
* enqueue the form for SSR prefetch and register the descendant's
|
|
1332
|
+
* `onServerPrefetch` hook. Consumers may set it manually as the
|
|
1333
|
+
* escape hatch when the transform isn't installed or doesn't see
|
|
1334
|
+
* the reference.
|
|
1335
|
+
*/
|
|
1336
|
+
type InjectFormInput = {
|
|
1337
|
+
readonly key?: FormKey;
|
|
1338
|
+
/**
|
|
1339
|
+
* Set by the Vite transform when this `injectForm` call site sits in
|
|
1340
|
+
* a component whose template / script reads the form's reactive
|
|
1341
|
+
* state. On the server, this enqueues the form for SSR prefetch and
|
|
1342
|
+
* wires `onServerPrefetch` so the descendant awaits the activation
|
|
1343
|
+
* promise before its render emits HTML.
|
|
1344
|
+
*
|
|
1345
|
+
* @internal Transform-emitted. Manual use is the documented escape
|
|
1346
|
+
* hatch when the transform can't reach the reference (dynamic
|
|
1347
|
+
* property access, untransformed bundlers).
|
|
1348
|
+
*/
|
|
1349
|
+
readonly __ssrAccessed?: boolean;
|
|
1350
|
+
};
|
|
1351
|
+
/**
|
|
1352
|
+
* Access an existing form from a descendant component without passing
|
|
1353
|
+
* it through props. Counterpart to `useForm` — `useForm` creates and
|
|
1354
|
+
* provides; `injectForm` looks up via Vue's inject mechanism.
|
|
1355
|
+
*
|
|
1356
|
+
* Three ways to call it:
|
|
1357
|
+
*
|
|
1358
|
+
* ```ts
|
|
1359
|
+
* // Reach the nearest ancestor's anonymous useForm() call.
|
|
1360
|
+
* const form = injectForm<SignupShape>()
|
|
1361
|
+
*
|
|
1362
|
+
* // Reach a specific form by its key — works from anywhere in the app.
|
|
1363
|
+
* const cart = injectForm<CartShape>('cart')
|
|
1364
|
+
*
|
|
1365
|
+
* // Options form. The Vite transform emits this with `__ssrAccessed: true`
|
|
1366
|
+
* // when the descendant's template / script reads the form's reactive
|
|
1367
|
+
* // state, so the descendant participates in SSR prefetch coordination.
|
|
1368
|
+
* const cart = injectForm<CartShape>({ key: 'cart', __ssrAccessed: true })
|
|
1369
|
+
* ```
|
|
1370
|
+
*
|
|
1371
|
+
* Resolution rules (no-key form):
|
|
1372
|
+
* - Closest ambient ancestor wins.
|
|
1373
|
+
* - Only anonymous `useForm()` (no `key`) fills the ambient slot;
|
|
1374
|
+
* keyed forms are reachable only via `injectForm(key)`.
|
|
1375
|
+
* - Inherits the resolved ancestor's `formInstanceId`.
|
|
1376
|
+
*
|
|
1377
|
+
* Resolution rules (keyed form): registry lookup by string key,
|
|
1378
|
+
* independent of component-tree position.
|
|
1379
|
+
*
|
|
1380
|
+
* Returns `null` when no matching form exists (no ambient ancestor, or
|
|
1381
|
+
* the named key isn't registered). A dev-mode warning points at the
|
|
1382
|
+
* call site to help diagnose typos. Always narrow before using:
|
|
1383
|
+
*
|
|
1384
|
+
* ```ts
|
|
1385
|
+
* const form = injectForm<Shape>('signup')
|
|
1386
|
+
* if (!form) return
|
|
1387
|
+
* form.register('email')
|
|
1388
|
+
* ```
|
|
1389
|
+
*
|
|
1390
|
+
* Pass the `Form` generic explicitly — Vue's provide/inject erases
|
|
1391
|
+
* generics, so the library can't recover the shape automatically.
|
|
1392
|
+
*
|
|
1393
|
+
* The form is kept alive for this component's lifetime; once every
|
|
1394
|
+
* consumer unmounts, the form is cleaned up automatically.
|
|
1395
|
+
*/
|
|
1396
|
+
declare function injectForm<Form extends GenericForm, GetValueFormType extends GenericForm = Form>(input?: FormKey | InjectFormInput): UseFormReturnType<Form, GetValueFormType> | null;
|
|
1397
|
+
|
|
1398
|
+
/**
|
|
1399
|
+
* Multistep-form orchestrator built around an ordered list of step slots.
|
|
1400
|
+
* Each slot resolves to a participating form: an existing `useForm`
|
|
1401
|
+
* reference, a bare string key (desugared to a noop form so affordance
|
|
1402
|
+
* steps participate uniformly), an eagerly-evaluated function slot for
|
|
1403
|
+
* runtime branching, or a `lazy()`-wrapped function slot that caches
|
|
1404
|
+
* its resolution and re-fires only on its own tracked deps.
|
|
1405
|
+
*
|
|
1406
|
+
* The wizard's surface is read-only from the consumer's side:
|
|
1407
|
+
* navigation (`next` / `back` / `goTo`) walks positional indices,
|
|
1408
|
+
* `handleSubmit` validates the active form on intermediate steps and
|
|
1409
|
+
* the whole wizard on the final step, and URL synchronization rides on
|
|
1410
|
+
* `restore` / `persist` callbacks that default to `?step=<key>`.
|
|
1411
|
+
*/
|
|
1412
|
+
declare function useWizard<const S extends ReadonlyArray<StepSlot>>(options: WizardOptions & {
|
|
1413
|
+
readonly steps: S;
|
|
1414
|
+
}): UseWizardReturnType<S>;
|
|
1415
|
+
|
|
1416
|
+
/**
|
|
1417
|
+
* Options accepted by `injectWizard` when passing an object instead of
|
|
1418
|
+
* a bare key string. Mirrors `injectForm`'s `InjectFormInput` shape so
|
|
1419
|
+
* the two composables present an identical call surface.
|
|
1420
|
+
*/
|
|
1421
|
+
type InjectWizardInput = {
|
|
1422
|
+
readonly key?: string | undefined;
|
|
1423
|
+
};
|
|
1424
|
+
/**
|
|
1425
|
+
* Access an existing wizard handle from a descendant component without
|
|
1426
|
+
* passing it through props. Counterpart to `useWizard` — `useWizard`
|
|
1427
|
+
* creates and provides; `injectWizard` looks up via Vue's inject
|
|
1428
|
+
* mechanism or the per-app registry.
|
|
1429
|
+
*
|
|
1430
|
+
* Three ways to call it:
|
|
1431
|
+
*
|
|
1432
|
+
* ```ts
|
|
1433
|
+
* // Reach the nearest ancestor's useWizard call (ambient).
|
|
1434
|
+
* const wizard = injectWizard()
|
|
1435
|
+
*
|
|
1436
|
+
* // Reach a specific wizard by its key — works from anywhere in the app.
|
|
1437
|
+
* const signup = injectWizard('signup-wizard')
|
|
1438
|
+
*
|
|
1439
|
+
* // Object form (equivalent; convenient for spread).
|
|
1440
|
+
* const signup = injectWizard({ key: 'signup-wizard' })
|
|
1441
|
+
* ```
|
|
1442
|
+
*
|
|
1443
|
+
* Resolution rules (no-key form):
|
|
1444
|
+
* - Closest ambient ancestor wins via `provide(kAttaformAncestorWizard)`.
|
|
1445
|
+
* - Only anonymous (no-`key`) `useWizard()` calls fill the ambient
|
|
1446
|
+
* slot. Descendants of a keyed wizard must address it explicitly
|
|
1447
|
+
* via `injectWizard('the-key')`. Mirrors `useForm`'s ambient gate.
|
|
1448
|
+
*
|
|
1449
|
+
* Resolution rules (keyed form): registry lookup by string key,
|
|
1450
|
+
* independent of component-tree position. The wizard must have been
|
|
1451
|
+
* constructed with `useWizard({ steps, key })` to be reachable.
|
|
1452
|
+
*
|
|
1453
|
+
* Returns `null` when no matching wizard exists (no ambient ancestor,
|
|
1454
|
+
* or the named key isn't registered). A dev-mode warning points at the
|
|
1455
|
+
* call site to help diagnose typos. Always narrow before using:
|
|
1456
|
+
*
|
|
1457
|
+
* ```ts
|
|
1458
|
+
* const wizard = injectWizard('signup')
|
|
1459
|
+
* if (!wizard) return
|
|
1460
|
+
* wizard.next()
|
|
1461
|
+
* ```
|
|
1462
|
+
*
|
|
1463
|
+
* Consumer ref-counting: keyed lookups pin the wizard handle in the
|
|
1464
|
+
* registry for this component's lifetime, so the handle survives even
|
|
1465
|
+
* if the parent `useWizard` component unmounts before the child does.
|
|
1466
|
+
* Once every consumer disposes, the registry evicts the entry on the
|
|
1467
|
+
* next microtask. Ambient lookups don't ref-count — the parent
|
|
1468
|
+
* `useWizard`'s scope owns the lifetime.
|
|
1469
|
+
*/
|
|
1470
|
+
declare function injectWizard(input?: string | InjectWizardInput): UseWizardReturnType | null;
|
|
1471
|
+
|
|
1472
|
+
/**
|
|
1473
|
+
* Wrap a function slot in `lazy()` to give that slot its own memoized
|
|
1474
|
+
* cache, distinct from the wizard-wide step compiler.
|
|
1475
|
+
*
|
|
1476
|
+
* Default function slots in `useWizard({ steps })` re-evaluate whenever
|
|
1477
|
+
* the compiled step list re-evaluates, including when an unrelated
|
|
1478
|
+
* slot's reactive deps change. That keeps the rail correct but can
|
|
1479
|
+
* trigger spurious work in a resolver that is expensive (a network
|
|
1480
|
+
* lookup, a heavy schema derivation, an async-derived factory).
|
|
1481
|
+
*
|
|
1482
|
+
* `lazy()` isolates each wrapped slot behind its own cache. The
|
|
1483
|
+
* resolver runs eagerly on the first compile pass; subsequent reads
|
|
1484
|
+
* reuse the cached form. The cache invalidates only when one of the
|
|
1485
|
+
* resolver's own tracked reactive dependencies changes (Vue's
|
|
1486
|
+
* `computed` semantics applied to a slot), or when `wizard.reset()`
|
|
1487
|
+
* triggers a global re-compile:
|
|
1488
|
+
*
|
|
1489
|
+
* const wizard = useWizard({
|
|
1490
|
+
* steps: [
|
|
1491
|
+
* account,
|
|
1492
|
+
* lazy((ctx) => loadShippingForm(ctx.forms.account.values.region)),
|
|
1493
|
+
* confirm,
|
|
1494
|
+
* ],
|
|
1495
|
+
* })
|
|
1496
|
+
*
|
|
1497
|
+
* Resolution semantics:
|
|
1498
|
+
* - Eager at construction so `wizard.steps` and SSR markup are honest
|
|
1499
|
+
* from t=0.
|
|
1500
|
+
* - Memoized: the resolver re-fires only when one of its own tracked
|
|
1501
|
+
* reads changes. An unrelated slot re-evaluating does not re-fire
|
|
1502
|
+
* this one.
|
|
1503
|
+
* - `wizard.reset()` clears every lazy slot's cache so a reboot truly
|
|
1504
|
+
* resolves from scratch.
|
|
1505
|
+
* - Resolving to `undefined` drops the slot from the compiled list
|
|
1506
|
+
* until the resolver next re-fires.
|
|
1507
|
+
*
|
|
1508
|
+
* The runtime brand returned by `lazy()` is opaque. Use
|
|
1509
|
+
* {@link isLazyMarker} to detect it.
|
|
1510
|
+
*/
|
|
1511
|
+
declare function lazy<Ctx = WizardCtx>(resolve: (ctx: Ctx) => AnyForm | string | undefined): LazyMarker<Ctx>;
|
|
1512
|
+
|
|
1513
|
+
/**
|
|
1514
|
+
* Re-bind a parent's `v-register` onto an inner native element. Use
|
|
1515
|
+
* inside a component that wraps a single form field whose root is
|
|
1516
|
+
* NOT the input itself (e.g. a labelled-row that renders `<label>`
|
|
1517
|
+
* around the input).
|
|
1518
|
+
*
|
|
1519
|
+
* ```vue
|
|
1520
|
+
* <!-- Parent -->
|
|
1521
|
+
* <MyInput v-register="form.register('email')" />
|
|
1522
|
+
*
|
|
1523
|
+
* <!-- MyInput.vue -->
|
|
1524
|
+
* <script setup lang="ts">
|
|
1525
|
+
* import { useRegister } from 'attaform'
|
|
1526
|
+
* const rv = useRegister()
|
|
1527
|
+
* // rv.path / rv.segments / rv.formKey / rv.formInstanceId / rv.innerRef
|
|
1528
|
+
* // are all reachable directly — no `.value` unwrap.
|
|
1529
|
+
* </script>
|
|
1530
|
+
*
|
|
1531
|
+
* <template>
|
|
1532
|
+
* <label class="field">
|
|
1533
|
+
* <span>Email</span>
|
|
1534
|
+
* <input v-register="rv" />
|
|
1535
|
+
* </label>
|
|
1536
|
+
* </template>
|
|
1537
|
+
* ```
|
|
1538
|
+
*
|
|
1539
|
+
* Returns a hybrid Proxy: it answers `__v_isRef` / `.value` like a
|
|
1540
|
+
* Vue `Ref<RegisterValue | undefined>` (so templates auto-unwrap
|
|
1541
|
+
* correctly and `v-register="rv"` feeds the underlying RV to the
|
|
1542
|
+
* directive — preserving the directive's path-migration diff across
|
|
1543
|
+
* renders), AND every other property read pierces to the captured
|
|
1544
|
+
* RV's field (so `rv.path` works directly in script setup). Reads
|
|
1545
|
+
* inside reactive scopes (`computed` / `watchEffect`) track the
|
|
1546
|
+
* underlying `shallowRef`, so `rv.path` re-runs when the parent
|
|
1547
|
+
* rebinds to a different path.
|
|
1548
|
+
*
|
|
1549
|
+
* Unbound state: when the parent didn't pass `v-register`, every
|
|
1550
|
+
* piercing read returns `undefined` at runtime, and the return type
|
|
1551
|
+
* surfaces this honestly as `UseRegisterReturn<V> | undefined`.
|
|
1552
|
+
* Consumers defend with optional chaining (`rv?.formKey`,
|
|
1553
|
+
* `rv?.segments`); the directive accepts `undefined` peacefully (its
|
|
1554
|
+
* binding value type is already `RegisterValue<V> | undefined`), so
|
|
1555
|
+
* `v-register="rv"` works whether or not a parent has bound. The
|
|
1556
|
+
* composable's `onMounted` warn fires once per instance to surface
|
|
1557
|
+
* the misuse case at runtime.
|
|
1558
|
+
*
|
|
1559
|
+
* Diagnostic: in dev mode, a single `console.warn` fires per instance
|
|
1560
|
+
* at `onMounted` if the captured value is still `undefined` — by then
|
|
1561
|
+
* the parent has had its full mount lifecycle to bind, so a missing
|
|
1562
|
+
* binding is conclusive misuse. The warn does NOT fire on every read
|
|
1563
|
+
* of the proxy, and is intentionally silent under SSR
|
|
1564
|
+
* (`renderToString` skips `onMounted`); the CSR hydration pass
|
|
1565
|
+
* surfaces the same diagnostic without double-counting through Nuxt's
|
|
1566
|
+
* `dev:ssr-logs` channel.
|
|
1567
|
+
*
|
|
1568
|
+
* When the wrapper's root IS the input itself, Vue's attribute
|
|
1569
|
+
* fallthrough handles the binding and `useRegister` is unnecessary.
|
|
1570
|
+
* For wrappers that bind multiple fields (compound forms), use
|
|
1571
|
+
* `injectForm<Form>(key?)` and call `ctx.register(...)` directly.
|
|
1572
|
+
*/
|
|
1573
|
+
|
|
1574
|
+
/**
|
|
1575
|
+
* Return type of `useRegister()`. Hybrid of `RegisterValue<V>` (so
|
|
1576
|
+
* `rv.path` / `rv.segments` / `rv.formKey` etc. work directly in
|
|
1577
|
+
* script setup) and `Ref<RegisterValue<V> | undefined>` (so Vue's
|
|
1578
|
+
* template auto-unwrap surfaces the underlying RV to `v-register`
|
|
1579
|
+
* and the directive's path-migration diff sees the real RV across
|
|
1580
|
+
* renders).
|
|
1581
|
+
*
|
|
1582
|
+
* The two surfaces don't clash at the type level: `RegisterValue`
|
|
1583
|
+
* doesn't carry a `value` field, and `Ref<T>`'s `value: T` becomes
|
|
1584
|
+
* the hybrid's only `.value`. Older code that read `rv.value?.path`
|
|
1585
|
+
* keeps working; new code can write `rv.path` directly.
|
|
1586
|
+
*/
|
|
1587
|
+
type UseRegisterReturn<V = unknown> = RegisterValue<V> & Ref<RegisterValue<V> | undefined>;
|
|
1588
|
+
declare function useRegister<V = unknown>(): UseRegisterReturn<V> | undefined;
|
|
1589
|
+
|
|
1590
|
+
/**
|
|
1591
|
+
* Stable identifiers for library-emitted `ValidationError` codes.
|
|
1592
|
+
*
|
|
1593
|
+
* Convention: `<scope>:<kebab-case-identifier>`. Three scopes are
|
|
1594
|
+
* recognised by the library:
|
|
1595
|
+
*
|
|
1596
|
+
* - `atta:` — emitted by the framework-agnostic core (this map).
|
|
1597
|
+
* - `zod:` — emitted by the Zod adapter; computed inline from
|
|
1598
|
+
* `issue.code` (e.g. `zod:too_small`). No enum here because
|
|
1599
|
+
* Zod's code list evolves.
|
|
1600
|
+
* - consumer-defined — anything the consumer's backend / app stamps
|
|
1601
|
+
* onto a `ValidationError` (via the `parseApiErrors` wire payload
|
|
1602
|
+
* or `setFieldErrors` directly). Pick a scope (`api:`, `auth:`,
|
|
1603
|
+
* etc.) and stay consistent.
|
|
1604
|
+
*
|
|
1605
|
+
* Use these constants in tests and error-routing UI:
|
|
1606
|
+
*
|
|
1607
|
+
* ```ts
|
|
1608
|
+
* if (error.code === AttaformErrorCode.NoValueSupplied) {
|
|
1609
|
+
* // user hasn't filled this field
|
|
1610
|
+
* }
|
|
1611
|
+
* ```
|
|
1612
|
+
*/
|
|
1613
|
+
declare const AttaformErrorCode: {
|
|
1614
|
+
/** A required field is in the blank set — user hasn't supplied a value. */
|
|
1615
|
+
readonly NoValueSupplied: "atta:no-value-supplied";
|
|
1616
|
+
/** The schema adapter's `validateAtPath` threw synchronously. */
|
|
1617
|
+
readonly AdapterThrew: "atta:adapter-threw";
|
|
1618
|
+
/**
|
|
1619
|
+
* User code inside a `z.preprocess`, `.refine`, or `.transform`
|
|
1620
|
+
* threw (sync or async). The adapter caught the throw and surfaced
|
|
1621
|
+
* it as a `ValidationError` at the field path so the form's normal
|
|
1622
|
+
* error pipeline handles it instead of leaking as an unhandled
|
|
1623
|
+
* rejection or routing through `submitError`.
|
|
1624
|
+
*/
|
|
1625
|
+
readonly ValidatorThrew: "atta:validator-threw";
|
|
1626
|
+
/**
|
|
1627
|
+
* A function-form `defaultValues` factory threw or its promise
|
|
1628
|
+
* rejected. The runtime captures the raw error on `form.hydrateError`
|
|
1629
|
+
* and ALSO surfaces a form-level `ValidationError` (path `[]`) so
|
|
1630
|
+
* the standard error pipeline carries the signal. Critical for the
|
|
1631
|
+
* SSR round-trip: `hydrateError` itself does not ride the wire
|
|
1632
|
+
* payload, but `schemaErrors` does, so the client sees the failure
|
|
1633
|
+
* after rehydration without an extra channel.
|
|
1634
|
+
*/
|
|
1635
|
+
readonly HydrationFailed: "atta:hydration-failed";
|
|
1636
|
+
/** The supplied path didn't resolve to any node in the schema. */
|
|
1637
|
+
readonly PathNotFound: "atta:path-not-found";
|
|
1638
|
+
/**
|
|
1639
|
+
* A walked form's `activate()` (async `defaultValues` factory) threw
|
|
1640
|
+
* during `wizard.handleSubmit`'s path walk. Surfaced as a synthetic
|
|
1641
|
+
* `ValidationError` at the form-level path (`[]`) so the wizard's
|
|
1642
|
+
* aggregate error pipeline can carry the failure alongside ordinary
|
|
1643
|
+
* validation errors. The raw factory error remains on
|
|
1644
|
+
* `form.hydrateError` for retry UX.
|
|
1645
|
+
*/
|
|
1646
|
+
readonly ActivationFailed: "atta:activation-failed";
|
|
1647
|
+
};
|
|
1648
|
+
type AttaformErrorCode = (typeof AttaformErrorCode)[keyof typeof AttaformErrorCode];
|
|
1649
|
+
|
|
1650
|
+
export { AttaformErrorCode as d, createRegistry as p, defaultCoercionRules as q, defineCoercion as r, getRegistryFromApp as s, injectForm as t, injectWizard as u, kAttaformRegistry as v, lazy as w, useRegister as x, useRegistry as y, useWizard as z };
|
|
1651
|
+
export type { AttaformRegistry as A, CompiledStep as C, FormStatus as F, InjectWizardInput as I, LazyMarker as L, SSRDetectOptions as S, UseRegisterReturn as U, WizardCtx as W, SerializedFormData as a, AggregateError as b, AnyForm as c, StepSlot as e, UseWizardReturnType as f, WizardCtxForm as g, WizardOnError as h, WizardOnSubmit as i, WizardOptions as j, WizardPersistFn as k, WizardRestoreFn as l, WizardRestoreState as m, WizardStatusesProxy as n, WizardSubmitContext as o };
|