attaform 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -11
- package/bin/attaform.mjs +78 -0
- package/dist/abstract.cjs +47 -0
- package/dist/abstract.cjs.map +1 -0
- package/dist/abstract.d.cts +42 -0
- package/dist/abstract.d.mts +42 -0
- package/dist/abstract.d.ts +42 -0
- package/dist/abstract.mjs +4 -0
- package/dist/abstract.mjs.map +1 -0
- package/dist/chunks/devtools.cjs +2 -2
- package/dist/chunks/devtools.cjs.map +1 -1
- package/dist/chunks/devtools.mjs +1 -1
- package/dist/chunks/fingerprint2.cjs +1 -1
- package/dist/chunks/fingerprint2.mjs +1 -1
- package/dist/esbuild.cjs +2 -2
- package/dist/esbuild.cjs.map +1 -1
- package/dist/esbuild.d.cts +3 -3
- package/dist/esbuild.d.mts +3 -3
- package/dist/esbuild.d.ts +3 -3
- package/dist/esbuild.mjs +2 -2
- package/dist/esbuild.mjs.map +1 -1
- package/dist/index.cjs +41 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +194 -282
- package/dist/index.d.mts +194 -282
- package/dist/index.d.ts +194 -282
- package/dist/index.mjs +4 -25
- package/dist/index.mjs.map +1 -1
- package/dist/nuxt.cjs +3 -1
- package/dist/nuxt.cjs.map +1 -1
- package/dist/nuxt.d.cts +17 -4
- package/dist/nuxt.d.mts +17 -4
- package/dist/nuxt.d.ts +17 -4
- package/dist/nuxt.mjs +4 -2
- package/dist/nuxt.mjs.map +1 -1
- package/dist/rollup.cjs +2 -2
- package/dist/rollup.cjs.map +1 -1
- package/dist/rollup.d.cts +3 -3
- package/dist/rollup.d.mts +3 -3
- package/dist/rollup.d.ts +3 -3
- package/dist/rollup.mjs +2 -2
- package/dist/rollup.mjs.map +1 -1
- package/dist/rspack.cjs +1 -1
- package/dist/rspack.d.cts +1 -1
- package/dist/rspack.d.mts +1 -1
- package/dist/rspack.d.ts +1 -1
- package/dist/rspack.mjs +1 -1
- package/dist/runtime/plugins/attaform.cjs +4 -5
- package/dist/runtime/plugins/attaform.cjs.map +1 -1
- package/dist/runtime/plugins/attaform.mjs +1 -2
- package/dist/runtime/plugins/attaform.mjs.map +1 -1
- package/dist/shared/{attaform.BRzKBj2g.cjs → attaform.8tFZu1Ub.cjs} +2 -2
- package/dist/shared/{attaform.BRzKBj2g.cjs.map → attaform.8tFZu1Ub.cjs.map} +1 -1
- package/dist/shared/{attaform.k9V0py0s.mjs → attaform.BERt5YMU.mjs} +2 -2
- package/dist/shared/{attaform.k9V0py0s.mjs.map → attaform.BERt5YMU.mjs.map} +1 -1
- package/dist/shared/{attaform.DSOIz08n.mjs → attaform.BceJrhQ7.mjs} +5 -5
- package/dist/shared/{attaform.DSOIz08n.mjs.map → attaform.BceJrhQ7.mjs.map} +1 -1
- package/dist/shared/{attaform.DBNjHgNj.d.cts → attaform.C3WnCDXz.d.cts} +11 -9
- package/dist/shared/{attaform.DBNjHgNj.d.mts → attaform.C3WnCDXz.d.mts} +11 -9
- package/dist/shared/{attaform.DBNjHgNj.d.ts → attaform.C3WnCDXz.d.ts} +11 -9
- package/dist/shared/{attaform.DuzQYscR.d.mts → attaform.C9fPTxjB.d.cts} +5 -5
- package/dist/shared/{attaform.DuzQYscR.d.ts → attaform.C9fPTxjB.d.mts} +5 -5
- package/dist/shared/{attaform.DuzQYscR.d.cts → attaform.C9fPTxjB.d.ts} +5 -5
- package/dist/shared/{attaform.Be5otUVw.d.ts → attaform.CC_y1_Ej.d.ts} +33 -33
- package/dist/shared/{attaform.ceYeVkjn.d.ts → attaform.CEXUlUkc.d.ts} +1579 -1192
- package/dist/shared/{attaform.tZ0YKisf.cjs → attaform.C__F01CE.cjs} +15 -15
- package/dist/shared/{attaform.tZ0YKisf.cjs.map → attaform.C__F01CE.cjs.map} +1 -1
- package/dist/shared/{attaform.BPl-ECRT.cjs → attaform.Co5LNRnv.cjs} +162 -129
- package/dist/shared/attaform.Co5LNRnv.cjs.map +1 -0
- package/dist/shared/{attaform.B71rfNL5.d.mts → attaform.CpXHK60W.d.cts} +33 -33
- package/dist/shared/{attaform.BFfY7HRI.d.cts → attaform.CvVYOvZF.d.mts} +33 -33
- package/dist/shared/{attaform.BoYqnIPI.mjs → attaform.CyQxTOdX.mjs} +55 -36
- package/dist/shared/attaform.CyQxTOdX.mjs.map +1 -0
- package/dist/shared/{attaform.CEY_WLRJ.d.mts → attaform.D99pd7JG.d.cts} +1579 -1192
- package/dist/shared/{attaform.CXVmZtFq.mjs → attaform.DDAmSFnl.mjs} +4 -4
- package/dist/shared/{attaform.CXVmZtFq.mjs.map → attaform.DDAmSFnl.mjs.map} +1 -1
- package/dist/shared/{attaform.Du9RZp8m.cjs → attaform.DQT7_-FO.cjs} +272 -252
- package/dist/shared/attaform.DQT7_-FO.cjs.map +1 -0
- package/dist/shared/{attaform.DGhIix5D.mjs → attaform.Da8EdI2t.mjs} +160 -129
- package/dist/shared/attaform.Da8EdI2t.mjs.map +1 -0
- package/dist/shared/{attaform.Cs0UZ7q6.cjs → attaform.DhVgG2jh.cjs} +16 -16
- package/dist/shared/{attaform.Cs0UZ7q6.cjs.map → attaform.DhVgG2jh.cjs.map} +1 -1
- package/dist/shared/{attaform.sHkHv_98.mjs → attaform.DhnPb6nK.mjs} +6 -3
- package/dist/shared/attaform.DhnPb6nK.mjs.map +1 -0
- package/dist/shared/attaform.DkD0DR7R.mjs +42 -0
- package/dist/shared/attaform.DkD0DR7R.mjs.map +1 -0
- package/dist/shared/{attaform.CSXDW2tO.mjs → attaform.DlSu08F5.mjs} +4 -4
- package/dist/shared/{attaform.CSXDW2tO.mjs.map → attaform.DlSu08F5.mjs.map} +1 -1
- package/dist/shared/{attaform.LEWUFqUw.cjs → attaform.DoQm_Vkd.cjs} +7 -3
- package/dist/shared/attaform.DoQm_Vkd.cjs.map +1 -0
- package/dist/shared/{attaform.BQflyVOL.cjs → attaform.Drsi43GG.cjs} +19 -19
- package/dist/shared/{attaform.BQflyVOL.cjs.map → attaform.Drsi43GG.cjs.map} +1 -1
- package/dist/shared/attaform.K6Thc6By.cjs +46 -0
- package/dist/shared/attaform.K6Thc6By.cjs.map +1 -0
- package/dist/shared/{attaform.Vp2-CWZB.mjs → attaform.KvlEedlF.mjs} +3 -3
- package/dist/shared/{attaform.Vp2-CWZB.mjs.map → attaform.KvlEedlF.mjs.map} +1 -1
- package/dist/shared/{attaform.BRGIpZo4.cjs → attaform.RaTzp1WF.cjs} +3 -3
- package/dist/shared/attaform.RaTzp1WF.cjs.map +1 -0
- package/dist/shared/{attaform.MgaYKlPP.d.cts → attaform.kBkYlKP_.d.mts} +1579 -1192
- package/dist/shared/{attaform.C3Doa9Pt.mjs → attaform.v0fQtWyA.mjs} +3 -3
- package/dist/shared/attaform.v0fQtWyA.mjs.map +1 -0
- package/dist/shared/{attaform.ZXZ6dH72.cjs → attaform.yUrz7C2z.cjs} +3 -3
- package/dist/shared/{attaform.ZXZ6dH72.cjs.map → attaform.yUrz7C2z.cjs.map} +1 -1
- package/dist/transforms.cjs +1 -1
- package/dist/transforms.mjs +1 -1
- package/dist/vite.cjs +21 -3
- package/dist/vite.cjs.map +1 -1
- package/dist/vite.d.cts +70 -17
- package/dist/vite.d.mts +70 -17
- package/dist/vite.d.ts +70 -17
- package/dist/vite.mjs +20 -4
- package/dist/vite.mjs.map +1 -1
- package/dist/webpack.cjs +1 -1
- package/dist/webpack.d.cts +1 -1
- package/dist/webpack.d.mts +1 -1
- package/dist/webpack.d.ts +1 -1
- package/dist/webpack.mjs +1 -1
- package/dist/zod-v3.cjs +41 -10
- package/dist/zod-v3.cjs.map +1 -1
- package/dist/zod-v3.d.cts +5 -5
- package/dist/zod-v3.d.mts +5 -5
- package/dist/zod-v3.d.ts +5 -5
- package/dist/zod-v3.mjs +4 -2
- package/dist/zod-v3.mjs.map +1 -1
- package/dist/zod-v4.cjs +41 -10
- package/dist/zod-v4.cjs.map +1 -1
- package/dist/zod-v4.d.cts +7 -7
- package/dist/zod-v4.d.mts +7 -7
- package/dist/zod-v4.d.ts +7 -7
- package/dist/zod-v4.mjs +4 -2
- package/dist/zod-v4.mjs.map +1 -1
- package/dist/zod.cjs +44 -49
- package/dist/zod.cjs.map +1 -1
- package/dist/zod.d.cts +6 -248
- package/dist/zod.d.mts +6 -248
- package/dist/zod.d.ts +6 -248
- package/dist/zod.mjs +4 -42
- package/dist/zod.mjs.map +1 -1
- package/package.json +12 -2
- package/skills/attaform/SKILL.md +128 -0
- package/skills/attaform/references/custom-components.md +72 -0
- package/skills/attaform/references/errors.md +62 -0
- package/skills/attaform/references/ssr.md +26 -0
- package/skills/attaform/references/validation.md +40 -0
- package/skills/attaform/references/wizards.md +104 -0
- package/dist/shared/attaform.55hFR_cl.d.ts +0 -97
- package/dist/shared/attaform.BPl-ECRT.cjs.map +0 -1
- package/dist/shared/attaform.BRGIpZo4.cjs.map +0 -1
- package/dist/shared/attaform.BoYqnIPI.mjs.map +0 -1
- package/dist/shared/attaform.C3Doa9Pt.mjs.map +0 -1
- package/dist/shared/attaform.D51IwDtP.cjs +0 -39
- package/dist/shared/attaform.D51IwDtP.cjs.map +0 -1
- package/dist/shared/attaform.DGhIix5D.mjs.map +0 -1
- package/dist/shared/attaform.DJRz_bzM.d.cts +0 -97
- package/dist/shared/attaform.Du9RZp8m.cjs.map +0 -1
- package/dist/shared/attaform.JPsaqjLl.d.mts +0 -97
- package/dist/shared/attaform.LEWUFqUw.cjs.map +0 -1
- package/dist/shared/attaform.NqYEzRcM.mjs +0 -35
- package/dist/shared/attaform.NqYEzRcM.mjs.map +0 -1
- package/dist/shared/attaform.sHkHv_98.mjs.map +0 -1
- package/dist/shared/{attaform.DdjDqTah.d.ts → attaform.DRUT703x.d.cts} +14 -14
- package/dist/shared/{attaform.DdjDqTah.d.cts → attaform.DRUT703x.d.mts} +14 -14
- package/dist/shared/{attaform.DdjDqTah.d.mts → attaform.DRUT703x.d.ts} +14 -14
|
@@ -1,931 +1,556 @@
|
|
|
1
|
-
import { F as FormKey,
|
|
2
|
-
import { Ref, ComputedRef, App, InjectionKey } from 'vue';
|
|
1
|
+
import { G as GenericForm, F as FormKey, a as UseFormReturnType, a4 as RegisterValue, T as PathKey, k as DisplayCtx, w as GetDisplayState, l as DisplayMachine, S as Path, a9 as SlimPrimitiveKind, C as CoercionEntry, e as CoercionRegistry, af as ValidationError, A as AbstractSchema, aj as WriteMeta, h as DeepPartial, ak as WriteShape, as as TransformAbortHolder, ad as ValidateOn, d as AttaformDefaults, at as RegisterModelDynamicCustomDirective } from './attaform.C3WnCDXz.cjs';
|
|
2
|
+
import { Ref, ComputedRef, App, InjectionKey, Plugin } from 'vue';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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.
|
|
5
|
+
* Options accepted by `injectForm` when passing an object instead of
|
|
6
|
+
* a bare key string. `__ssrAccessed: true` is set by the Phase 3
|
|
7
|
+
* `attaform-vite` transform on descendant calls whose template reads
|
|
8
|
+
* the injected form's reactive state — it tells the runtime to
|
|
9
|
+
* enqueue the form for SSR prefetch and register the descendant's
|
|
10
|
+
* `onServerPrefetch` hook. Consumers may set it manually as the
|
|
11
|
+
* escape hatch when the transform isn't installed or doesn't see
|
|
12
|
+
* the reference.
|
|
27
13
|
*/
|
|
28
|
-
type
|
|
29
|
-
readonly key
|
|
14
|
+
type InjectFormInput = {
|
|
15
|
+
readonly key?: FormKey;
|
|
16
|
+
/**
|
|
17
|
+
* Set by the Vite transform when this `injectForm` call site sits in
|
|
18
|
+
* a component whose template / script reads the form's reactive
|
|
19
|
+
* state. On the server, this enqueues the form for SSR prefetch and
|
|
20
|
+
* wires `onServerPrefetch` so the descendant awaits the activation
|
|
21
|
+
* promise before its render emits HTML.
|
|
22
|
+
*
|
|
23
|
+
* @internal Transform-emitted. Manual use is the documented escape
|
|
24
|
+
* hatch when the transform can't reach the reference (dynamic
|
|
25
|
+
* property access, untransformed bundlers).
|
|
26
|
+
*/
|
|
27
|
+
readonly __ssrAccessed?: boolean;
|
|
30
28
|
};
|
|
31
29
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* (`{{ wizard.statuses.cargo.valid }}`), while `form.meta` carries the
|
|
36
|
-
* full per-form lifecycle surface.
|
|
30
|
+
* Access an existing form from a descendant component without passing
|
|
31
|
+
* it through props. Counterpart to `useForm` — `useForm` creates and
|
|
32
|
+
* provides; `injectForm` looks up via Vue's inject mechanism.
|
|
37
33
|
*
|
|
38
|
-
*
|
|
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).
|
|
34
|
+
* Three ways to call it:
|
|
49
35
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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 WizardAggregateError = {
|
|
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.
|
|
36
|
+
* ```ts
|
|
37
|
+
* // Reach the nearest ancestor's anonymous useForm() call.
|
|
38
|
+
* const form = injectForm<SignupShape>()
|
|
81
39
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
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.
|
|
40
|
+
* // Reach a specific form by its key — works from anywhere in the app.
|
|
41
|
+
* const cart = injectForm<CartShape>('cart')
|
|
94
42
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
43
|
+
* // Options form. The Vite transform emits this with `__ssrAccessed: true`
|
|
44
|
+
* // when the descendant's template / script reads the form's reactive
|
|
45
|
+
* // state, so the descendant participates in SSR prefetch coordination.
|
|
46
|
+
* const cart = injectForm<CartShape>({ key: 'cart', __ssrAccessed: true })
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* Resolution rules (no-key form):
|
|
50
|
+
* - Closest ambient ancestor wins.
|
|
51
|
+
* - Only anonymous `useForm()` (no `key`) fills the ambient slot;
|
|
52
|
+
* keyed forms are reachable only via `injectForm(key)`.
|
|
53
|
+
* - Inherits the resolved ancestor's `formInstanceId`.
|
|
54
|
+
*
|
|
55
|
+
* Resolution rules (keyed form): registry lookup by string key,
|
|
56
|
+
* independent of component-tree position.
|
|
57
|
+
*
|
|
58
|
+
* Returns `null` when no matching form exists (no ambient ancestor, or
|
|
59
|
+
* the named key isn't registered yet). A dev-mode warning points at the
|
|
60
|
+
* call site, lists the registered keys, and flags the mount-timing case
|
|
61
|
+
* (a form created by a child or sibling isn't registered until its own
|
|
62
|
+
* setup runs). Always narrow before using:
|
|
63
|
+
*
|
|
64
|
+
* ```ts
|
|
65
|
+
* const form = injectForm<Shape>('signup')
|
|
66
|
+
* if (!form) return
|
|
67
|
+
* form.register('email')
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* Pass the `Form` generic explicitly — Vue's provide/inject erases
|
|
71
|
+
* generics, so the library can't recover the shape automatically.
|
|
72
|
+
*
|
|
73
|
+
* The form is kept alive for this component's lifetime; once every
|
|
74
|
+
* consumer unmounts, the form is cleaned up automatically.
|
|
99
75
|
*/
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
};
|
|
76
|
+
declare function injectForm<Form extends GenericForm, GetValueFormType extends GenericForm = Form>(input?: FormKey | InjectFormInput): UseFormReturnType<Form, GetValueFormType> | null;
|
|
77
|
+
|
|
103
78
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
79
|
+
* Re-bind a parent's `v-register` onto an inner native element. Use
|
|
80
|
+
* inside a component that wraps a single form field whose root is
|
|
81
|
+
* NOT the input itself (e.g. a labelled-row that renders `<label>`
|
|
82
|
+
* around the input).
|
|
108
83
|
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
84
|
+
* ```vue
|
|
85
|
+
* <!-- Parent -->
|
|
86
|
+
* <MyInput v-register="form.register('email')" />
|
|
87
|
+
*
|
|
88
|
+
* <!-- MyInput.vue -->
|
|
89
|
+
* <script setup lang="ts">
|
|
90
|
+
* import { useRegister } from 'attaform'
|
|
91
|
+
* const rv = useRegister()
|
|
92
|
+
* // rv.path / rv.segments / rv.formKey / rv.formInstanceId / rv.innerRef
|
|
93
|
+
* // are all reachable directly — no `.value` unwrap.
|
|
94
|
+
* </script>
|
|
95
|
+
*
|
|
96
|
+
* <template>
|
|
97
|
+
* <label class="field">
|
|
98
|
+
* <span>Email</span>
|
|
99
|
+
* <input v-register="rv" />
|
|
100
|
+
* </label>
|
|
101
|
+
* </template>
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* Returns a hybrid Proxy: it answers `__v_isRef` / `.value` like a
|
|
105
|
+
* Vue `Ref<RegisterValue | undefined>` (so templates auto-unwrap
|
|
106
|
+
* correctly and `v-register="rv"` feeds the underlying RV to the
|
|
107
|
+
* directive — preserving the directive's path-migration diff across
|
|
108
|
+
* renders), AND every other property read pierces to the captured
|
|
109
|
+
* RV's field (so `rv.path` works directly in script setup). Reads
|
|
110
|
+
* inside reactive scopes (`computed` / `watchEffect`) track the
|
|
111
|
+
* underlying `shallowRef`, so `rv.path` re-runs when the parent
|
|
112
|
+
* rebinds to a different path.
|
|
113
|
+
*
|
|
114
|
+
* Unbound state: when the parent didn't pass `v-register`, every
|
|
115
|
+
* piercing read returns `undefined` at runtime, and the return type
|
|
116
|
+
* surfaces this honestly as `UseRegisterReturn<V> | undefined`.
|
|
117
|
+
* Consumers defend with optional chaining (`rv?.formKey`,
|
|
118
|
+
* `rv?.segments`); the directive accepts `undefined` peacefully (its
|
|
119
|
+
* binding value type is already `RegisterValue<V> | undefined`), so
|
|
120
|
+
* `v-register="rv"` works whether or not a parent has bound. The
|
|
121
|
+
* composable's `onMounted` warn fires once per instance to surface
|
|
122
|
+
* the misuse case at runtime.
|
|
123
|
+
*
|
|
124
|
+
* Diagnostic: in dev mode, a single `console.warn` fires per instance
|
|
125
|
+
* at `onMounted` if the captured value is still `undefined` — by then
|
|
126
|
+
* the parent has had its full mount lifecycle to bind, so a missing
|
|
127
|
+
* binding is conclusive misuse. The warn does NOT fire on every read
|
|
128
|
+
* of the proxy, and is intentionally silent under SSR
|
|
129
|
+
* (`renderToString` skips `onMounted`); the CSR hydration pass
|
|
130
|
+
* surfaces the same diagnostic without double-counting through Nuxt's
|
|
131
|
+
* `dev:ssr-logs` channel.
|
|
132
|
+
*
|
|
133
|
+
* When the wrapper's root IS the input itself, Vue's attribute
|
|
134
|
+
* fallthrough handles the binding and `useRegister` is unnecessary.
|
|
135
|
+
* For wrappers that bind multiple fields (compound forms), use
|
|
136
|
+
* `injectForm<Form>(key?)` and call `ctx.register(...)` directly.
|
|
114
137
|
*/
|
|
115
|
-
|
|
116
|
-
readonly forms: Readonly<Record<FormKey, WizardCtxForm>>;
|
|
117
|
-
readonly currentKey: FormKey | undefined;
|
|
118
|
-
};
|
|
138
|
+
|
|
119
139
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
140
|
+
* Return type of `useRegister()`. Hybrid of `RegisterValue<V>` (so
|
|
141
|
+
* `rv.path` / `rv.segments` / `rv.formKey` etc. work directly in
|
|
142
|
+
* script setup) and `Ref<RegisterValue<V> | undefined>` (so Vue's
|
|
143
|
+
* template auto-unwrap surfaces the underlying RV to `v-register`
|
|
144
|
+
* and the directive's path-migration diff sees the real RV across
|
|
145
|
+
* renders).
|
|
146
|
+
*
|
|
147
|
+
* The two surfaces don't clash at the type level: `RegisterValue`
|
|
148
|
+
* doesn't carry a `value` field, and `Ref<T>`'s `value: T` becomes
|
|
149
|
+
* the hybrid's only `.value`. Older code that read `rv.value?.path`
|
|
150
|
+
* keeps working; new code can write `rv.path` directly.
|
|
123
151
|
*/
|
|
124
|
-
|
|
152
|
+
type UseRegisterReturn<V = unknown> = RegisterValue<V> & Ref<RegisterValue<V> | undefined>;
|
|
153
|
+
declare function useRegister<V = unknown>(): UseRegisterReturn<V> | undefined;
|
|
154
|
+
|
|
125
155
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
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.
|
|
156
|
+
* Per-form display engine: owns the clock and the timers that the pure
|
|
157
|
+
* `getDisplayState` reducer policy needs, so the reducer itself stays a
|
|
158
|
+
* deterministic `(prev, ctx) => next` function.
|
|
133
159
|
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
160
|
+
* It keeps a `Map` of the machines that are still *active* (a spinner is
|
|
161
|
+
* showing, a verdict is being held, or a `reviewAt` deadline is pending),
|
|
162
|
+
* a single reactive `tick` ref, and a single `setTimeout` aimed at the
|
|
163
|
+
* nearest `reviewAt` across every active field. When a field-state
|
|
164
|
+
* computed reads `resolve`, it subscribes to `tick`; when the timer
|
|
165
|
+
* fires, `tick` bumps and every dependent computed re-runs the reducer,
|
|
166
|
+
* so a field whose deadline elapsed transitions (verdict → spinner, or
|
|
167
|
+
* spinner → settled verdict) without any per-field watcher.
|
|
168
|
+
*
|
|
169
|
+
* Eviction: a machine is dropped once it reaches a terminal *idle* state
|
|
170
|
+
* with no pending review. Error / success / pending machines are retained
|
|
171
|
+
* so the reducer can hold the prior verdict under the show-delay window of
|
|
172
|
+
* the *next* validation streak (no success → idle → success flicker). The
|
|
173
|
+
* retained set is bounded by the rendered non-idle fields; none of them
|
|
174
|
+
* arm a timer, so retention costs memory, never CPU.
|
|
175
|
+
*
|
|
176
|
+
* Untrusted reducer: `getDisplayState` is consumer-overridable, so the
|
|
177
|
+
* engine treats the returned `reviewAt` as untrusted. A non-finite deadline
|
|
178
|
+
* (NaN / ±Infinity from a custom predicate's bad arithmetic) is ignored
|
|
179
|
+
* rather than handed to `setTimeout`, where it coerces to 0 and spins; an
|
|
180
|
+
* over-large finite deadline is clamped below the 32-bit `setTimeout`
|
|
181
|
+
* overflow; and the timer refuses to re-arm for the exact deadline it just
|
|
182
|
+
* fired, so a predicate re-emitting a fixed or past `reviewAt` can't drive an
|
|
183
|
+
* infinite fire loop. None of these arise from the library default, which
|
|
184
|
+
* always advances its deadline or drops it.
|
|
185
|
+
*
|
|
186
|
+
* Background tabs: `setTimeout` is throttled to >= 1s while a tab is hidden,
|
|
187
|
+
* so a min-visible hold can overshoot. A `visibilitychange` listener bumps
|
|
188
|
+
* the clock on return to the foreground, so any overdue deadline resolves at
|
|
189
|
+
* once instead of lingering.
|
|
190
|
+
*
|
|
191
|
+
* SSR: no clock, no timers, no listener. With `now` frozen and nothing
|
|
192
|
+
* validating at render, the reducer returns the plain verdict (never
|
|
193
|
+
* pending) and the engine stores nothing, so the server HTML and the
|
|
194
|
+
* client's first render agree — no hydration mismatch on the display
|
|
195
|
+
* projection.
|
|
137
196
|
*/
|
|
138
|
-
type
|
|
139
|
-
|
|
140
|
-
|
|
197
|
+
type DisplayEngine = {
|
|
198
|
+
/**
|
|
199
|
+
* Resolve a path's next `DisplayMachine`. Subscribes the calling
|
|
200
|
+
* computed to the engine clock, threads the path's previous machine
|
|
201
|
+
* through `reducer`, persists or evicts the result, and re-arms the
|
|
202
|
+
* single timer to the nearest deadline.
|
|
203
|
+
*/
|
|
204
|
+
resolve(key: PathKey, ctx: DisplayCtx, reducer: GetDisplayState): DisplayMachine;
|
|
205
|
+
/** Drop every retained machine and cancel the timer (used by `reset()`). */
|
|
206
|
+
clear(): void;
|
|
207
|
+
/** Tear down for good: `clear()` plus detaching the visibility listener. */
|
|
208
|
+
dispose(): void;
|
|
209
|
+
/** Introspection for tests: count of retained machines. */
|
|
210
|
+
size(): number;
|
|
211
|
+
/** Introspection for tests: whether a path currently has a retained machine. */
|
|
212
|
+
has(key: PathKey): boolean;
|
|
213
|
+
/** Introspection for tests: whether a deadline timer is currently armed. */
|
|
214
|
+
hasTimer(): boolean;
|
|
141
215
|
};
|
|
216
|
+
|
|
142
217
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
218
|
+
* Per-path field status. Replaced wholesale (not mutated in place) on
|
|
219
|
+
* every change. Five semantic groups:
|
|
145
220
|
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
221
|
+
* - `connected` — is a DOM element registered for this path?
|
|
222
|
+
* - `focused` / `blurred` — DOM-state flags. `null` while no element
|
|
223
|
+
* is connected (no DOM means the concepts don't apply); plain
|
|
224
|
+
* booleans once connected, with the invariant `blurred === !focused`
|
|
225
|
+
* enforced by `markFocused`.
|
|
226
|
+
* - `touched` — focus/blur history, not DOM state. Always a plain
|
|
227
|
+
* boolean: `false` at registration, sticky `true` after first blur,
|
|
228
|
+
* cleared only by `form.reset()` / `form.resetField(path)`. Persists
|
|
229
|
+
* across disconnects so v-if'd-away fields don't lose their touched
|
|
230
|
+
* state on rehide (wizard "show review of touched fields" patterns
|
|
231
|
+
* rely on this).
|
|
232
|
+
* - `interacted` — value-mutation history, not DOM state. Plain
|
|
233
|
+
* boolean: `false` at registration, sticky `true` once the user
|
|
234
|
+
* issues a value edit through the directive's input listeners
|
|
235
|
+
* (never on hydration, default seeding, or programmatic setValue);
|
|
236
|
+
* cleared with `touched` by `form.reset()` / `form.resetField(path)`.
|
|
237
|
+
* - `blurredAfterInteraction` — the first blur that follows a value
|
|
238
|
+
* edit (the field has been edited and then left). Plain boolean,
|
|
239
|
+
* sticky `true`. A tab-through blur with no prior edit does NOT set
|
|
240
|
+
* it (`interacted` is still false at that blur). Composes
|
|
241
|
+
* `interacted` with the departure; drives the default display gate.
|
|
165
242
|
*/
|
|
166
|
-
type
|
|
167
|
-
readonly
|
|
243
|
+
type FieldRecord = {
|
|
244
|
+
readonly path: Path;
|
|
245
|
+
readonly updatedAt: string | null;
|
|
246
|
+
readonly connected: boolean;
|
|
247
|
+
readonly focused: boolean | null;
|
|
248
|
+
readonly blurred: boolean | null;
|
|
249
|
+
readonly touched: boolean;
|
|
250
|
+
readonly interacted: boolean;
|
|
251
|
+
readonly blurredAfterInteraction: boolean;
|
|
252
|
+
};
|
|
253
|
+
/** Per-path DOM element tracking. Client-only. */
|
|
254
|
+
type ElementRecord = {
|
|
255
|
+
/**
|
|
256
|
+
* Original Path captured at first registration. Stored alongside the
|
|
257
|
+
* elements Set so the DOM-order sort cache can recover the structured
|
|
258
|
+
* Path without round-tripping through `JSON.parse(pathKey)`.
|
|
259
|
+
*/
|
|
260
|
+
readonly path: Path;
|
|
261
|
+
readonly elements: Set<HTMLElement>;
|
|
168
262
|
};
|
|
169
263
|
/**
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* `persist` callback signature. Invoked whenever `wizard.currentStep`
|
|
178
|
-
* changes; the wizard diffs against the last persisted value to break
|
|
179
|
-
* the restore-persist loop, so the callback only fires when the active
|
|
180
|
-
* step actually moves.
|
|
264
|
+
* Per-path record stored in `originals`. Pairing `segments` with the tracked
|
|
265
|
+
* value means `dirty` and `resetField`'s container loop don't have to
|
|
266
|
+
* `JSON.parse(pathKey)` on every iteration — the canonical Path is already
|
|
267
|
+
* sitting next to the value it belongs to. PathKey still keys the Map (the
|
|
268
|
+
* stable string is the only collision-free identifier), but downstream
|
|
269
|
+
* iteration reads `segments` directly.
|
|
181
270
|
*/
|
|
182
|
-
type
|
|
271
|
+
type OriginalsRecord = {
|
|
272
|
+
readonly segments: Path;
|
|
273
|
+
readonly value: unknown;
|
|
274
|
+
};
|
|
275
|
+
|
|
183
276
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
277
|
+
* Schema-driven coercion of user-typed DOM values at the v-register
|
|
278
|
+
* directive layer. When the slim schema declares a numeric or
|
|
279
|
+
* boolean type at a path, the directive coerces incoming string
|
|
280
|
+
* values (`'25'` → `25`, `'true'` → `true`) before the slim-primitive
|
|
281
|
+
* gate sees the write — making the schema authoritative for storage
|
|
282
|
+
* shape and freeing consumers from sprinkling `.number` modifiers
|
|
283
|
+
* across templates.
|
|
188
284
|
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* `isFinal === false`, yet the whole list is still
|
|
202
|
-
* processed and `done` still latches on success.
|
|
285
|
+
* Coercion is consumer-extensible: a `CoercionRegistry` is just an
|
|
286
|
+
* `Array<CoercionEntry>` keyed at config time by `(input, output)`
|
|
287
|
+
* `SlimPrimitiveKind` literals. The library ships
|
|
288
|
+
* `defaultCoercionRules` (string→number, string→boolean) and
|
|
289
|
+
* `defineCoercion` for type-narrowed authoring; consumers spread the
|
|
290
|
+
* defaults to extend or supply their own array to replace.
|
|
291
|
+
*
|
|
292
|
+
* Coercion applies ONLY to user-typed DOM values flowing through
|
|
293
|
+
* the directive's assigner. Programmatic writes (`form.setValue`,
|
|
294
|
+
* `setValueWithInternalPath`) bypass coercion — they're authoritative
|
|
295
|
+
* writes whose strict typing is on the caller. This mirrors the
|
|
296
|
+
* `transforms` pipeline's user-input-only contract.
|
|
203
297
|
*/
|
|
204
|
-
|
|
205
|
-
readonly values: Readonly<Record<FormKey, unknown>>;
|
|
206
|
-
readonly get: <F extends AnyForm>(form: F) => F extends {
|
|
207
|
-
readonly values: infer V;
|
|
208
|
-
} ? V : unknown;
|
|
209
|
-
readonly currentKey: FormKey;
|
|
210
|
-
readonly isFinal: boolean;
|
|
211
|
-
};
|
|
298
|
+
|
|
212
299
|
/**
|
|
213
|
-
*
|
|
214
|
-
*
|
|
300
|
+
* Type-narrowing helper for authoring entries. At runtime it's
|
|
301
|
+
* identity; at compile time it preserves the `input` / `output`
|
|
302
|
+
* literal types so `transform`'s parameter is narrowed to the
|
|
303
|
+
* runtime type instead of widening to `SlimRuntimeOf<SlimPrimitiveKind>`.
|
|
304
|
+
*
|
|
305
|
+
* Without this helper, authoring `{ input: 'string', output:
|
|
306
|
+
* 'number', transform: (s) => ... }` against the broader
|
|
307
|
+
* `CoercionEntry` widens `s` to `string | number | boolean | ...`,
|
|
308
|
+
* forcing a cast in every transform body. `defineCoercion` is the
|
|
309
|
+
* opaque-free idiom.
|
|
215
310
|
*/
|
|
216
|
-
|
|
311
|
+
declare function defineCoercion<I extends SlimPrimitiveKind, O extends SlimPrimitiveKind>(entry: CoercionEntry<I, O>): CoercionEntry<I, O>;
|
|
217
312
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
* per-form validation, activation failures (`atta:activation-failed`),
|
|
223
|
-
* and a submit callback that left errors on a processed step (the
|
|
224
|
-
* `setErrors(...); return` server-rejection path). Sync or async; the
|
|
225
|
-
* returned promise gates `wizard.submitting`.
|
|
313
|
+
* Internal index built from a `CoercionRegistry` at config-resolve
|
|
314
|
+
* time. Keyed by `${input}->${output}` for O(1) per-keystroke
|
|
315
|
+
* dispatch. The authoring shape (array, ergonomic, type-narrowing-
|
|
316
|
+
* friendly) and the dispatch shape (Map, fast) decouple cleanly.
|
|
226
317
|
*/
|
|
227
|
-
type
|
|
318
|
+
type CoercionIndex = ReadonlyMap<`${SlimPrimitiveKind}->${SlimPrimitiveKind}`, CoercionEntry>;
|
|
228
319
|
/**
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
320
|
+
* The library's built-in registry. Two cells: string→number and
|
|
321
|
+
* string→boolean. Re-exported so consumers can spread it when
|
|
322
|
+
* supplying a custom registry that extends defaults.
|
|
232
323
|
*/
|
|
233
|
-
|
|
324
|
+
declare const defaultCoercionRules: CoercionRegistry;
|
|
325
|
+
|
|
326
|
+
type FormStore<F extends GenericForm, G extends GenericForm = F> = {
|
|
327
|
+
readonly formKey: FormKey;
|
|
328
|
+
readonly form: Ref<F>;
|
|
329
|
+
readonly fields: Map<PathKey, FieldRecord>;
|
|
330
|
+
readonly elements: Map<PathKey, ElementRecord>;
|
|
234
331
|
/**
|
|
235
|
-
*
|
|
236
|
-
*
|
|
332
|
+
* Schema-driven errors. Written ONLY by the schema validation pipeline:
|
|
333
|
+
* `scheduleFieldValidation`, `handleSubmit`, the construction-time seed,
|
|
334
|
+
* history restore, and hydration. Cleared by `reset` / `resetField` and by
|
|
335
|
+
* a successful submit. `setErrors` / `clearErrors` do NOT touch this Map.
|
|
237
336
|
*/
|
|
238
|
-
readonly
|
|
337
|
+
readonly schemaErrors: Map<PathKey, ValidationError[]>;
|
|
239
338
|
/**
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
* omitted) get a synthetic `__atta:anon-wizard:<id>` key resolved
|
|
244
|
-
* via `useId()` so SSR-rendered and client-hydrated trees agree on
|
|
245
|
-
* the same registry entry; the synthetic key is opaque and
|
|
246
|
-
* descendants reach an anonymous wizard via ambient `injectWizard()`
|
|
247
|
-
* rather than by key.
|
|
248
|
-
*
|
|
249
|
-
* Duplicate-key registration is first-wins-silently (dev-warn on the
|
|
250
|
-
* second registration) to mirror `useForm`'s shared-key behavior.
|
|
251
|
-
* The dev-warn fires only for explicit keys — two anonymous wizards
|
|
252
|
-
* are guaranteed distinct synthetic keys, so the warning never
|
|
253
|
-
* misfires on independent anonymous wizards on the same page.
|
|
339
|
+
* User-injected errors. Written ONLY by the `setErrors` / `clearErrors`
|
|
340
|
+
* API surface (and history / hydration replay). Survives schema revalidation and
|
|
341
|
+
* successful submits — the consumer owns its lifetime explicitly.
|
|
254
342
|
*/
|
|
255
|
-
readonly
|
|
343
|
+
readonly userErrors: Map<PathKey, ValidationError[]>;
|
|
256
344
|
/**
|
|
257
|
-
*
|
|
258
|
-
* `
|
|
259
|
-
*
|
|
260
|
-
*
|
|
345
|
+
* Reactively-derived "No value supplied" errors. Pure function of
|
|
346
|
+
* `(blankPaths, schema.isRequiredAtPath)` — no writers, no clears.
|
|
347
|
+
* Membership tracks `blankPaths` automatically: typing a value into
|
|
348
|
+
* a blank required numeric field removes the path from `blankPaths`
|
|
349
|
+
* and the derived error vanishes; clearing the numeric input re-adds
|
|
350
|
+
* the path and the error reappears. The `errors` proxy and
|
|
351
|
+
* `getErrorsForPath` merge this map in alongside `schemaErrors` and
|
|
352
|
+
* `userErrors`, so consumers see the "this required field is empty"
|
|
353
|
+
* error the moment it's true — no `validate()` / `handleSubmit`
|
|
354
|
+
* call required. Honors the founding principle that
|
|
355
|
+
* `errors = f(schema, state)`.
|
|
261
356
|
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
357
|
+
* Most entries flow through this map for `number` / `bigint` leaves
|
|
358
|
+
* (where the side-channel is needed to distinguish "user typed 0"
|
|
359
|
+
* from "user supplied nothing"). String / boolean leaves only land
|
|
360
|
+
* here when the consumer explicitly opted in via the `unset`
|
|
361
|
+
* sentinel — see `docs/validation/blank.md`.
|
|
362
|
+
*/
|
|
363
|
+
readonly derivedBlankErrors: ComputedRef<ReadonlyMap<PathKey, ValidationError[]>>;
|
|
364
|
+
readonly originals: Map<PathKey, OriginalsRecord>;
|
|
365
|
+
/**
|
|
366
|
+
* Reactive set of paths whose displayed state should be EMPTY even
|
|
367
|
+
* though storage holds a real, schema-conformant value (the slim
|
|
368
|
+
* default). It exists exclusively to record **storage / display
|
|
369
|
+
* divergence** — the case where the runtime can't tell "user typed
|
|
370
|
+
* 0" from "user supplied nothing" by looking at storage alone.
|
|
267
371
|
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
372
|
+
* The mechanism shines for `number` / `bigint`: storage holds the
|
|
373
|
+
* slim default (`0` / `0n`) but the DOM input shows `''`, so the
|
|
374
|
+
* directive's input listener marks the path here on clear. Strings
|
|
375
|
+
* and booleans don't need it — `''` storage equals `''` display,
|
|
376
|
+
* `false` storage equals unchecked display — so they're never
|
|
377
|
+
* auto-marked. Consumers can still mark any primitive leaf
|
|
378
|
+
* explicitly via the `unset` sentinel (`defaultValues: { x: unset }`,
|
|
379
|
+
* `setValue('x', unset)`, `reset({ x: unset })`); the mark is then
|
|
380
|
+
* a documented signal of consumer intent rather than runtime
|
|
381
|
+
* inference.
|
|
382
|
+
*
|
|
383
|
+
* Reads (`displayValue` computed, `fields.<path>.blank`,
|
|
384
|
+
* `derivedBlankErrors` computed) track via Vue 3.5's reactive Set
|
|
385
|
+
* handlers. Writes happen inside `setValueAtPath` (gate-hook
|
|
386
|
+
* bookkeeping: `blank: true` meta adds the path; any other write
|
|
387
|
+
* removes it) and `reset`.
|
|
388
|
+
*
|
|
389
|
+
* Storage NEVER reflects this set — calculations and reads against
|
|
390
|
+
* `form.value` see the slim default. The set is purely a UI/intent
|
|
391
|
+
* channel that `derivedBlankErrors` consults to surface
|
|
392
|
+
* "No value supplied" errors for required schemas.
|
|
393
|
+
*
|
|
394
|
+
* See `docs/validation/blank.md` for the conceptual model.
|
|
270
395
|
*/
|
|
271
|
-
readonly
|
|
396
|
+
readonly blankPaths: Set<PathKey>;
|
|
272
397
|
/**
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
398
|
+
* Snapshot of `blankPaths` captured at construction (and
|
|
399
|
+
* re-captured on `reset(args)`). Used by dirty calculation: a path
|
|
400
|
+
* whose membership differs from the snapshot is dirty even if
|
|
401
|
+
* storage matches the original. Eagerly populated to avoid a "dirty
|
|
402
|
+
* on first read" race after construction.
|
|
403
|
+
*/
|
|
404
|
+
readonly originalBlankPaths: Set<PathKey>;
|
|
405
|
+
readonly schema: AbstractSchema<F, G>;
|
|
406
|
+
/**
|
|
407
|
+
* Server-side flag, plumbed in from `registry.ssr`. The
|
|
408
|
+
* `register()`-returned `markConnectedOptimistically()` reads this
|
|
409
|
+
* before flipping `connected: true`; on the client it's a no-op so
|
|
410
|
+
* the eventual directive lifecycle remains the source of truth.
|
|
411
|
+
*/
|
|
412
|
+
readonly ssr: boolean;
|
|
413
|
+
/**
|
|
414
|
+
* Resolved `getDisplayState` predicate driving `field.displayState`,
|
|
415
|
+
* the `show*` booleans, and their `form.meta` rollups. Resolved once
|
|
416
|
+
* at construction via `resolveGetDisplayState(options.getDisplayState)`;
|
|
417
|
+
* `undefined` config falls through to `defaultDisplayState`. The
|
|
418
|
+
* field-state computeds read the resolved function directly on every
|
|
419
|
+
* read.
|
|
420
|
+
*/
|
|
421
|
+
readonly getDisplayState: GetDisplayState;
|
|
422
|
+
/**
|
|
423
|
+
* Per-form display engine: owns the clock and the single timer the timed
|
|
424
|
+
* `getDisplayState` reducer policy needs, keeping the reducer itself a
|
|
425
|
+
* pure `(prev, ctx) => next` function. The field-state computeds route
|
|
426
|
+
* every `displayState` read through `displayEngine.resolve(...)`, which
|
|
427
|
+
* threads the path's previous machine, persists or evicts the result, and
|
|
428
|
+
* re-arms the nearest-deadline timer. Constructed once at form
|
|
429
|
+
* construction; torn down via `registerCleanup` on store eviction.
|
|
430
|
+
*/
|
|
431
|
+
readonly displayEngine: DisplayEngine;
|
|
432
|
+
readonly submitting: Ref<boolean>;
|
|
433
|
+
readonly activeSubmissions: Ref<number>;
|
|
434
|
+
readonly submissionAttempts: Ref<number>;
|
|
435
|
+
/**
|
|
436
|
+
* `true` once a `handleSubmit` callback resolved without throwing.
|
|
437
|
+
* Independent of `submissionAttempts` — a failed submit increments
|
|
438
|
+
* attempts but leaves `submitted` at `false`. Cleared by `reset()`
|
|
439
|
+
* alongside the rest of the submission surface.
|
|
440
|
+
*/
|
|
441
|
+
readonly submitted: Ref<boolean>;
|
|
442
|
+
readonly submitError: Ref<Error | null>;
|
|
443
|
+
readonly departAttempts: Ref<number>;
|
|
444
|
+
/**
|
|
445
|
+
* `true` while a function-form `defaultValues` factory is in flight.
|
|
446
|
+
* Stays `false` for plain-value `defaultValues`. Shared across every
|
|
447
|
+
* `useForm({ key })` call that resolves to this store — the second
|
|
448
|
+
* caller sees the first caller's hydration state.
|
|
449
|
+
*/
|
|
450
|
+
readonly hydrating: Ref<boolean>;
|
|
451
|
+
/**
|
|
452
|
+
* Error from the most recent function-form `defaultValues` factory.
|
|
453
|
+
* Normalized to a `ValidationError` (code `atta:hydration-failed`) so the
|
|
454
|
+
* shape matches `form.errors` / `form.meta.errors` entries. `null` when
|
|
455
|
+
* no factory has fired or the last one succeeded.
|
|
456
|
+
*/
|
|
457
|
+
readonly hydrateError: Ref<ValidationError | null>;
|
|
458
|
+
/**
|
|
459
|
+
* The function-form `defaultValues` factory, captured at the first
|
|
460
|
+
* `useForm({ key })` call that wired this store. `undefined` for
|
|
461
|
+
* plain-value forms. Read by `form.rehydrate()`.
|
|
462
|
+
*/
|
|
463
|
+
readonly defaultValuesFactory: Ref<(() => unknown | Promise<unknown>) | undefined>;
|
|
464
|
+
/**
|
|
465
|
+
* `true` when this store carries an SSR prefetch queue (server path
|
|
466
|
+
* where `state.activate()` must enqueue intent before deciding
|
|
467
|
+
* whether to fire). The flag lets `buildFormApi` skip the lazy
|
|
468
|
+
* activation gate for forms with no factory AND no SSR prefetch —
|
|
469
|
+
* the common client-side case where `gated()` is otherwise pure
|
|
470
|
+
* reactive overhead on every public method call.
|
|
471
|
+
*/
|
|
472
|
+
readonly hasSsrPrefetch: boolean;
|
|
473
|
+
/**
|
|
474
|
+
* `true` once the form's effective defaults have been applied —
|
|
475
|
+
* sync `defaultValues` at construction, or async factory whose
|
|
476
|
+
* settle completed. Stays `false` for dormant lazy forms until they
|
|
477
|
+
* activate. Read by `useWizard` to decide whether seed status or
|
|
478
|
+
* live meta should surface.
|
|
479
|
+
*/
|
|
480
|
+
readonly defaultsResolved: Ref<boolean>;
|
|
481
|
+
/**
|
|
482
|
+
* `true` once the captured async factory has been kicked off (set
|
|
483
|
+
* synchronously by `activate()`, before the factory itself resolves).
|
|
484
|
+
* Distinct from `defaultsResolved`, which only flips after the factory
|
|
485
|
+
* settles. The pair lets the API surface tell "we've started" apart
|
|
486
|
+
* from "we're done."
|
|
487
|
+
*/
|
|
488
|
+
readonly activated: Ref<boolean>;
|
|
489
|
+
/**
|
|
490
|
+
* In-flight activation promise. Concurrent callers (cross-component
|
|
491
|
+
* SSR consumers, recursive factory reads, parallel `activate()`
|
|
492
|
+
* calls) receive the same promise, ensuring the factory runs once
|
|
493
|
+
* even under contention.
|
|
494
|
+
*/
|
|
495
|
+
readonly activationPromise: Ref<Promise<void> | undefined>;
|
|
496
|
+
/**
|
|
497
|
+
* Idempotent activation entrypoint. Fires the captured function-form
|
|
498
|
+
* `defaultValues` factory on first call and stores the in-flight
|
|
499
|
+
* promise. Subsequent calls return the same promise until the factory
|
|
500
|
+
* settles; thereafter calls return `Promise.resolve()`. Plain-value
|
|
501
|
+
* forms (no factory captured) always return a resolved promise. The
|
|
502
|
+
* public API surface routes all reactive interactions (getters and
|
|
503
|
+
* methods, except `key`) through this entrypoint so the form
|
|
504
|
+
* activates on first use.
|
|
505
|
+
*/
|
|
506
|
+
activate(): Promise<void>;
|
|
507
|
+
/**
|
|
508
|
+
* Re-fire the captured function-form `defaultValues` factory. Throws
|
|
509
|
+
* synchronously when no factory was captured (plain-value form).
|
|
510
|
+
* Resolves after `hydrating` flips back to `false`; consumers can
|
|
511
|
+
* `await form.rehydrate()` to gate UI on the fresh load.
|
|
277
512
|
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
513
|
+
* Does NOT touch dirty / touched / submit state — chain
|
|
514
|
+
* `form.reset()` if you want a clean baseline.
|
|
280
515
|
*/
|
|
281
|
-
|
|
516
|
+
rehydrate(): Promise<void>;
|
|
282
517
|
/**
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
* `
|
|
287
|
-
*
|
|
288
|
-
* manually in the `onError` callback.
|
|
518
|
+
* Incremented by every `reset()` call. The submit wrapper captures
|
|
519
|
+
* this at entry and skips writing `submitError` from a catch that
|
|
520
|
+
* fires *after* a reset — otherwise a reset-during-submit would
|
|
521
|
+
* visibly clear `submitError` and then have it reappear when the
|
|
522
|
+
* in-flight promise rejects.
|
|
289
523
|
*/
|
|
290
|
-
readonly
|
|
524
|
+
readonly submissionGeneration: Ref<number>;
|
|
291
525
|
/**
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
526
|
+
* Counts in-flight validation calls across every `validate()` ref and
|
|
527
|
+
* every `validateAsync(...)` / `handleSubmit` pre-check. `validating`
|
|
528
|
+
* on the public API mirrors `activeValidations.value > 0`. Tracked
|
|
529
|
+
* separately from submissions because a validate-while-submitting
|
|
530
|
+
* (e.g. a debounced field check overlapping a submit) needs to show
|
|
531
|
+
* the union of both surfaces.
|
|
297
532
|
*/
|
|
298
|
-
readonly
|
|
533
|
+
readonly activeValidations: Ref<number>;
|
|
299
534
|
/**
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
* non-empty list? True when at least one element is a guaranteed step
|
|
320
|
-
* (see `IsGuaranteedStep`). Function / `lazy()` slots may resolve to
|
|
321
|
-
* nothing, and `null` / `undefined` (a literal absence or a
|
|
322
|
-
* `cond ? form : null` union) is an explicit drop, so none of those
|
|
323
|
-
* prove non-emptiness; a tuple of only maybe-absent slots stays
|
|
324
|
-
* honestly `| undefined`.
|
|
325
|
-
*
|
|
326
|
-
* Used to narrow `currentStep` / `activeForm` to their non-`undefined`
|
|
327
|
-
* shapes in the common-case wizard, while keeping the honest union
|
|
328
|
-
* everywhere a runtime drop is reachable.
|
|
329
|
-
*/
|
|
330
|
-
type StaticallyNonEmpty<S> = S extends readonly [infer First, ...infer Rest] ? IsGuaranteedStep<First> extends true ? true : StaticallyNonEmpty<Rest> : false;
|
|
331
|
-
/** Active step's key, narrowed to `string` when `S` is statically safe. */
|
|
332
|
-
type CurrentStepOf<S> = StaticallyNonEmpty<S> extends true ? FormKey : FormKey | undefined;
|
|
333
|
-
/**
|
|
334
|
-
* Active step's form handle, schema-erased to `UseFormReturnType<GenericForm>`
|
|
335
|
-
* and narrowed to non-`undefined` when `S` is statically safe. The runtime
|
|
336
|
-
* value is a live facade over the active step, so reads (`.values`, `.meta`,
|
|
337
|
-
* `.history`) and `.handleSubmit` always target the current step. Values are
|
|
338
|
-
* loose here because the active step's schema is not statically known; reach
|
|
339
|
-
* for the original form ref or `ctx.get(ref)` when you need typed per-step
|
|
340
|
-
* values.
|
|
341
|
-
*/
|
|
342
|
-
type ActiveFormOf<S> = StaticallyNonEmpty<S> extends true ? UseFormReturnType<GenericForm> : UseFormReturnType<GenericForm> | undefined;
|
|
343
|
-
/**
|
|
344
|
-
* Per-slot contribution to {@link FormsRecordOf}. Strips a `null` /
|
|
345
|
-
* `undefined` (a literal absence or a `cond ? form : null` union) off the
|
|
346
|
-
* slot first, so a conditionally-present form still maps to its key and
|
|
347
|
-
* stays concretely typed on `wizard.forms`; a purely nullish slot
|
|
348
|
-
* contributes nothing.
|
|
349
|
-
*/
|
|
350
|
-
type SlotRecord<First, F = Exclude<First, null | undefined>> = [F] extends [never] ? unknown : F extends string ? {
|
|
351
|
-
readonly [P in F]: AnyForm;
|
|
352
|
-
} : F extends {
|
|
353
|
-
readonly key: infer K extends string;
|
|
354
|
-
} ? {
|
|
355
|
-
readonly [P in K]: F;
|
|
356
|
-
} : unknown;
|
|
357
|
-
/**
|
|
358
|
-
* Recursive tuple walk that builds the static portion of
|
|
359
|
-
* `wizard.forms`. Each step slot contributes to the record:
|
|
360
|
-
*
|
|
361
|
-
* - **String slot** (`'review'`): the literal becomes the record key
|
|
362
|
-
* and the value is `AnyForm` (the noop form synthesized for the
|
|
363
|
-
* affordance position is opaque at the type level).
|
|
364
|
-
* - **Form slot** (a `useForm` reference with a literal `key` field):
|
|
365
|
-
* the form's own `key` becomes the record key, and the value is
|
|
366
|
-
* the concrete form handle type — so drilling
|
|
367
|
-
* `wizard.forms.shipping.values.address` carries the schema-derived
|
|
368
|
-
* field types through. A form kept behind a `cond ? form : null`
|
|
369
|
-
* conditional still maps to its key.
|
|
370
|
-
* - **`null` / `undefined` slot**: contributes nothing.
|
|
371
|
-
* - **Function / `lazy()` slot**: contributes nothing to the static
|
|
372
|
-
* map. Runtime-resolved forms are still reachable via the
|
|
373
|
-
* catch-all index signature on `WizardForms` (typed as `AnyForm`).
|
|
374
|
-
*
|
|
375
|
-
* Recursion is bounded by the tuple length; real-world wizards land
|
|
376
|
-
* well below the TS instantiation budget.
|
|
377
|
-
*/
|
|
378
|
-
type FormsRecordOf<S> = S extends readonly [
|
|
379
|
-
infer First,
|
|
380
|
-
...infer Rest extends ReadonlyArray<StepSlot>
|
|
381
|
-
] ? SlotRecord<First> & FormsRecordOf<Rest> : unknown;
|
|
382
|
-
/**
|
|
383
|
-
* `wizard.forms` typed view. Combines the static per-step type map
|
|
384
|
-
* with a catch-all `Record<FormKey, AnyForm>` fallback so:
|
|
385
|
-
*
|
|
386
|
-
* - Statically known slot keys → concrete form type via `FormsRecordOf`
|
|
387
|
-
* - Any other string key → `AnyForm` via the index signature
|
|
388
|
-
*
|
|
389
|
-
* The intersection collapses to the concrete form for statically
|
|
390
|
-
* known keys (because the concrete form type extends `AnyForm`) and
|
|
391
|
-
* to `AnyForm` for unknown keys.
|
|
392
|
-
*/
|
|
393
|
-
type WizardForms<S> = FormsRecordOf<S> & Readonly<Record<FormKey, AnyForm>>;
|
|
394
|
-
/**
|
|
395
|
-
* Return shape of `useWizard({ steps, … })`. Every reactive read is a
|
|
396
|
-
* plain getter (no `.value`) — `wizard.currentStep`, `wizard.progress`,
|
|
397
|
-
* `wizard.allValues` track inside `computed` / template effects
|
|
398
|
-
* directly.
|
|
399
|
-
*
|
|
400
|
-
* Parameterized by the steps tuple `S` so active-position fields
|
|
401
|
-
* (`currentStep`, `activeForm`) narrow to non-undefined for the common
|
|
402
|
-
* case (all positional Form / string slots) and stay as honest unions
|
|
403
|
-
* when a function or `lazy()` slot can drop the compiled position at
|
|
404
|
-
* runtime. The `const` type parameter on `useWizard` preserves literal
|
|
405
|
-
* tuple types without consumer-side `as const`, so the narrowing
|
|
406
|
-
* happens automatically from the call site.
|
|
407
|
-
*
|
|
408
|
-
* - `currentStep` — key of the active step. Narrows to `string` when
|
|
409
|
-
* the steps tuple is statically guaranteed to
|
|
410
|
-
* compile to a non-empty list (all positional
|
|
411
|
-
* Form / string slots, no function or `lazy()`
|
|
412
|
-
* slots). Otherwise reads as `string | undefined`
|
|
413
|
-
* so the degenerate case (empty list at runtime)
|
|
414
|
-
* surfaces honestly.
|
|
415
|
-
* - `activeForm` — a LIVE view of the active step's form. Operating
|
|
416
|
-
* through it always targets the current step, so a
|
|
417
|
-
* handler captured once at setup
|
|
418
|
-
* (`wizard.activeForm.handleSubmit(() =>
|
|
419
|
-
* wizard.next())`) retargets as the wizard advances.
|
|
420
|
-
* No longer `===` the raw per-step handle; reach for
|
|
421
|
-
* `wizard.forms[key]` when you need raw identity.
|
|
422
|
-
* Same `undefined` narrowing as `currentStep`. Noop
|
|
423
|
-
* forms cover string slots in the normal path.
|
|
424
|
-
* - `activeIndex` — 0-based position of the active step.
|
|
425
|
-
* - `isFinalStep` — `true` when `currentStep === steps[count - 1].key`.
|
|
426
|
-
* - `steps` — ordered list of compiled `{ key, form }` slots.
|
|
427
|
-
* - `forms` — record indexable by step key; the value is the
|
|
428
|
-
* full form handle resolved for that slot.
|
|
429
|
-
* - `count` — `steps.length`.
|
|
430
|
-
* - `statuses` — callable readonly proxy over the per-key
|
|
431
|
-
* `FormStatus` record. Noop-form keys always read
|
|
432
|
-
* as default-valid.
|
|
433
|
-
* - `allValues` — namespaced aggregate of each form's values, keyed
|
|
434
|
-
* by step key.
|
|
435
|
-
* - `allErrors` — namespaced aggregate of each form's validation
|
|
436
|
-
* errors, keyed by step key. Noop forms map to an
|
|
437
|
-
* empty list.
|
|
438
|
-
* - `progress` — normalised step-validity ratio (or the consumer's
|
|
439
|
-
* `progress` override). Forward-looking: noops count
|
|
440
|
-
* as always-valid.
|
|
441
|
-
* - `canAdvance` — `true` when `activeIndex < count - 1`. Pure
|
|
442
|
-
* positional check; navigation never gates on
|
|
443
|
-
* validity.
|
|
444
|
-
* - `canGoBack` — `true` when `activeIndex > 0`.
|
|
445
|
-
* - `complete` — `isFinalStep && every step's form is valid`.
|
|
446
|
-
* Forward-looking; reactive to current form
|
|
447
|
-
* validity. Gates "Finish button enable" style UI.
|
|
448
|
-
* - `done` — monotonic latch: flips `true` the first time a
|
|
449
|
-
* `handleSubmit` resolves without throwing AND leaves
|
|
450
|
-
* no errors set on any step, and stays `true` through
|
|
451
|
-
* subsequent edits or invalidations. A callback that
|
|
452
|
-
* calls `setErrors` and returns (the documented
|
|
453
|
-
* server-rejection path) is a failed submit, so it
|
|
454
|
-
* does not flip `done`. Only `reset()` flips it back.
|
|
455
|
-
* Gates "show success card" style UI that should
|
|
456
|
-
* reflect submission history rather than current
|
|
457
|
-
* validity.
|
|
458
|
-
* - `submitting` — `true` while a `wizard.handleSubmit` call is in
|
|
459
|
-
* flight. Global re-entrance guard: every
|
|
460
|
-
* navigation method also refuses while this is on.
|
|
461
|
-
* - `submissionAttempts` — count of `wizard.handleSubmit` invocations
|
|
462
|
-
* (success or failure). Always bumps, including on
|
|
463
|
-
* noop-form steps.
|
|
464
|
-
* - `submitError` — the error THROWN by the most recent
|
|
465
|
-
* `wizard.handleSubmit` callback (or its `onError`),
|
|
466
|
-
* coerced to a real `Error`. Mirrors
|
|
467
|
-
* `form.meta.submitError`: this is the unexpected-throw
|
|
468
|
-
* channel, so an expected rejection handled via
|
|
469
|
-
* `setErrors` (no throw) surfaces through the error
|
|
470
|
-
* surface and `onError` instead, leaving this `null`.
|
|
471
|
-
* Cleared at submit entry and by `reset()`, parked here
|
|
472
|
-
* rather than re-thrown, so the handler resolves and
|
|
473
|
-
* never manufactures a `window` unhandledrejection.
|
|
474
|
-
* `null` on success.
|
|
475
|
-
* - `visited` — append-only breadcrumb of navigated step keys.
|
|
476
|
-
* `back()` does not pop; the trail is the audit
|
|
477
|
-
* log, not the back-stack.
|
|
478
|
-
* - `next/back/goTo` — pure navigation. Refuses while `submitting`.
|
|
479
|
-
* - `tryNext()` — validate the active step, then advance iff it
|
|
480
|
-
* passed; invalid input keeps the pin put under the
|
|
481
|
-
* form's standard error reveal (first error focused,
|
|
482
|
-
* display state advanced). The inline-bindable
|
|
483
|
-
* shorthand for `activeForm.handleSubmit(() =>
|
|
484
|
-
* next())`. Resolves to whether the pin moved. No-ops
|
|
485
|
-
* to `false` on a degenerate or final-step wizard
|
|
486
|
-
* (finish via `handleSubmit`).
|
|
487
|
-
* - `handleSubmit(onSubmit, onError?)` — always validates the entire
|
|
488
|
-
* step list, from any step, and never advances the
|
|
489
|
-
* pin: on success it latches `done`; on any error it
|
|
490
|
-
* focuses the first failing step and fires `onError`
|
|
491
|
-
* with errors spanning every step. To gate advancing
|
|
492
|
-
* a step on its own validity, compose with the active
|
|
493
|
-
* form's submit: `activeForm.handleSubmit(() =>
|
|
494
|
-
* wizard.next())`. Returns an event handler suitable
|
|
495
|
-
* for `<form @submit>` or imperative use.
|
|
496
|
-
* - `reset()` — zeros wizard lifecycle (`submissionAttempts`,
|
|
497
|
-
* `visited`), resets every form, returns
|
|
498
|
-
* `currentStep` to `steps[0].key`, and invokes
|
|
499
|
-
* `persist` with the cleared state.
|
|
500
|
-
*/
|
|
501
|
-
type UseWizardReturnType<S extends ReadonlyArray<StepSlot> = ReadonlyArray<StepSlot>> = {
|
|
502
|
-
readonly key: string;
|
|
503
|
-
readonly currentStep: CurrentStepOf<S>;
|
|
504
|
-
readonly activeForm: ActiveFormOf<S>;
|
|
505
|
-
readonly activeIndex: number;
|
|
506
|
-
readonly isFinalStep: boolean;
|
|
507
|
-
readonly steps: ReadonlyArray<CompiledStep>;
|
|
508
|
-
readonly forms: WizardForms<S>;
|
|
509
|
-
readonly count: number;
|
|
510
|
-
readonly statuses: WizardStatusesProxy<Record<string, FormStatus>>;
|
|
511
|
-
readonly allValues: Readonly<Record<FormKey, unknown>>;
|
|
512
|
-
readonly allErrors: Readonly<Record<FormKey, readonly WizardAggregateError[]>>;
|
|
513
|
-
readonly progress: number;
|
|
514
|
-
readonly canAdvance: boolean;
|
|
515
|
-
readonly canGoBack: boolean;
|
|
516
|
-
readonly complete: boolean;
|
|
517
|
-
readonly done: boolean;
|
|
518
|
-
readonly submitting: boolean;
|
|
519
|
-
readonly submissionAttempts: number;
|
|
520
|
-
readonly submitError: Error | null;
|
|
521
|
-
readonly visited: readonly FormKey[];
|
|
522
|
-
readonly next: () => Promise<void>;
|
|
523
|
-
readonly back: () => void;
|
|
524
|
-
readonly goTo: (key: string) => void;
|
|
525
|
-
readonly tryNext: () => Promise<boolean>;
|
|
526
|
-
readonly handleSubmit: (onSubmit: WizardOnSubmit, onError?: WizardOnError) => (event?: Event) => Promise<void>;
|
|
527
|
-
readonly reset: () => void;
|
|
528
|
-
};
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* Per-form display engine: owns the clock and the timers that the pure
|
|
532
|
-
* `getDisplayState` reducer policy needs, so the reducer itself stays a
|
|
533
|
-
* deterministic `(prev, ctx) => next` function.
|
|
534
|
-
*
|
|
535
|
-
* It keeps a `Map` of the machines that are still *active* (a spinner is
|
|
536
|
-
* showing, a verdict is being held, or a `reviewAt` deadline is pending),
|
|
537
|
-
* a single reactive `tick` ref, and a single `setTimeout` aimed at the
|
|
538
|
-
* nearest `reviewAt` across every active field. When a field-state
|
|
539
|
-
* computed reads `resolve`, it subscribes to `tick`; when the timer
|
|
540
|
-
* fires, `tick` bumps and every dependent computed re-runs the reducer,
|
|
541
|
-
* so a field whose deadline elapsed transitions (verdict → spinner, or
|
|
542
|
-
* spinner → settled verdict) without any per-field watcher.
|
|
543
|
-
*
|
|
544
|
-
* Eviction: a machine is dropped once it reaches a terminal *idle* state
|
|
545
|
-
* with no pending review. Error / success / pending machines are retained
|
|
546
|
-
* so the reducer can hold the prior verdict under the show-delay window of
|
|
547
|
-
* the *next* validation streak (no success → idle → success flicker). The
|
|
548
|
-
* retained set is bounded by the rendered non-idle fields; none of them
|
|
549
|
-
* arm a timer, so retention costs memory, never CPU.
|
|
550
|
-
*
|
|
551
|
-
* Untrusted reducer: `getDisplayState` is consumer-overridable, so the
|
|
552
|
-
* engine treats the returned `reviewAt` as untrusted. A non-finite deadline
|
|
553
|
-
* (NaN / ±Infinity from a custom predicate's bad arithmetic) is ignored
|
|
554
|
-
* rather than handed to `setTimeout`, where it coerces to 0 and spins; an
|
|
555
|
-
* over-large finite deadline is clamped below the 32-bit `setTimeout`
|
|
556
|
-
* overflow; and the timer refuses to re-arm for the exact deadline it just
|
|
557
|
-
* fired, so a predicate re-emitting a fixed or past `reviewAt` can't drive an
|
|
558
|
-
* infinite fire loop. None of these arise from the library default, which
|
|
559
|
-
* always advances its deadline or drops it.
|
|
560
|
-
*
|
|
561
|
-
* Background tabs: `setTimeout` is throttled to >= 1s while a tab is hidden,
|
|
562
|
-
* so a min-visible hold can overshoot. A `visibilitychange` listener bumps
|
|
563
|
-
* the clock on return to the foreground, so any overdue deadline resolves at
|
|
564
|
-
* once instead of lingering.
|
|
565
|
-
*
|
|
566
|
-
* SSR: no clock, no timers, no listener. With `now` frozen and nothing
|
|
567
|
-
* validating at render, the reducer returns the plain verdict (never
|
|
568
|
-
* pending) and the engine stores nothing, so the server HTML and the
|
|
569
|
-
* client's first render agree — no hydration mismatch on the display
|
|
570
|
-
* projection.
|
|
571
|
-
*/
|
|
572
|
-
type DisplayEngine = {
|
|
573
|
-
/**
|
|
574
|
-
* Resolve a path's next `DisplayMachine`. Subscribes the calling
|
|
575
|
-
* computed to the engine clock, threads the path's previous machine
|
|
576
|
-
* through `reducer`, persists or evicts the result, and re-arms the
|
|
577
|
-
* single timer to the nearest deadline.
|
|
578
|
-
*/
|
|
579
|
-
resolve(key: PathKey, ctx: DisplayCtx, reducer: GetDisplayState): DisplayMachine;
|
|
580
|
-
/** Drop every retained machine and cancel the timer (used by `reset()`). */
|
|
581
|
-
clear(): void;
|
|
582
|
-
/** Tear down for good: `clear()` plus detaching the visibility listener. */
|
|
583
|
-
dispose(): void;
|
|
584
|
-
/** Introspection for tests: count of retained machines. */
|
|
585
|
-
size(): number;
|
|
586
|
-
/** Introspection for tests: whether a path currently has a retained machine. */
|
|
587
|
-
has(key: PathKey): boolean;
|
|
588
|
-
/** Introspection for tests: whether a deadline timer is currently armed. */
|
|
589
|
-
hasTimer(): boolean;
|
|
590
|
-
};
|
|
591
|
-
|
|
592
|
-
/**
|
|
593
|
-
* Per-path field status. Replaced wholesale (not mutated in place) on
|
|
594
|
-
* every change. Five semantic groups:
|
|
595
|
-
*
|
|
596
|
-
* - `connected` — is a DOM element registered for this path?
|
|
597
|
-
* - `focused` / `blurred` — DOM-state flags. `null` while no element
|
|
598
|
-
* is connected (no DOM means the concepts don't apply); plain
|
|
599
|
-
* booleans once connected, with the invariant `blurred === !focused`
|
|
600
|
-
* enforced by `markFocused`.
|
|
601
|
-
* - `touched` — focus/blur history, not DOM state. Always a plain
|
|
602
|
-
* boolean: `false` at registration, sticky `true` after first blur,
|
|
603
|
-
* cleared only by `form.reset()` / `form.resetField(path)`. Persists
|
|
604
|
-
* across disconnects so v-if'd-away fields don't lose their touched
|
|
605
|
-
* state on rehide (wizard "show review of touched fields" patterns
|
|
606
|
-
* rely on this).
|
|
607
|
-
* - `interacted` — value-mutation history, not DOM state. Plain
|
|
608
|
-
* boolean: `false` at registration, sticky `true` once the user
|
|
609
|
-
* issues a value edit through the directive's input listeners
|
|
610
|
-
* (never on hydration, default seeding, or programmatic setValue);
|
|
611
|
-
* cleared with `touched` by `form.reset()` / `form.resetField(path)`.
|
|
612
|
-
* - `blurredAfterInteraction` — the first blur that follows a value
|
|
613
|
-
* edit (the field has been edited and then left). Plain boolean,
|
|
614
|
-
* sticky `true`. A tab-through blur with no prior edit does NOT set
|
|
615
|
-
* it (`interacted` is still false at that blur). Composes
|
|
616
|
-
* `interacted` with the departure; drives the default display gate.
|
|
617
|
-
*/
|
|
618
|
-
type FieldRecord = {
|
|
619
|
-
readonly path: Path;
|
|
620
|
-
readonly updatedAt: string | null;
|
|
621
|
-
readonly connected: boolean;
|
|
622
|
-
readonly focused: boolean | null;
|
|
623
|
-
readonly blurred: boolean | null;
|
|
624
|
-
readonly touched: boolean;
|
|
625
|
-
readonly interacted: boolean;
|
|
626
|
-
readonly blurredAfterInteraction: boolean;
|
|
627
|
-
};
|
|
628
|
-
/** Per-path DOM element tracking. Client-only. */
|
|
629
|
-
type ElementRecord = {
|
|
630
|
-
/**
|
|
631
|
-
* Original Path captured at first registration. Stored alongside the
|
|
632
|
-
* elements Set so the DOM-order sort cache can recover the structured
|
|
633
|
-
* Path without round-tripping through `JSON.parse(pathKey)`.
|
|
634
|
-
*/
|
|
635
|
-
readonly path: Path;
|
|
636
|
-
readonly elements: Set<HTMLElement>;
|
|
637
|
-
};
|
|
638
|
-
/**
|
|
639
|
-
* Per-path record stored in `originals`. Pairing `segments` with the tracked
|
|
640
|
-
* value means `dirty` and `resetField`'s container loop don't have to
|
|
641
|
-
* `JSON.parse(pathKey)` on every iteration — the canonical Path is already
|
|
642
|
-
* sitting next to the value it belongs to. PathKey still keys the Map (the
|
|
643
|
-
* stable string is the only collision-free identifier), but downstream
|
|
644
|
-
* iteration reads `segments` directly.
|
|
645
|
-
*/
|
|
646
|
-
type OriginalsRecord = {
|
|
647
|
-
readonly segments: Path;
|
|
648
|
-
readonly value: unknown;
|
|
649
|
-
};
|
|
650
|
-
|
|
651
|
-
/**
|
|
652
|
-
* Schema-driven coercion of user-typed DOM values at the v-register
|
|
653
|
-
* directive layer. When the slim schema declares a numeric or
|
|
654
|
-
* boolean type at a path, the directive coerces incoming string
|
|
655
|
-
* values (`'25'` → `25`, `'true'` → `true`) before the slim-primitive
|
|
656
|
-
* gate sees the write — making the schema authoritative for storage
|
|
657
|
-
* shape and freeing consumers from sprinkling `.number` modifiers
|
|
658
|
-
* across templates.
|
|
659
|
-
*
|
|
660
|
-
* Coercion is consumer-extensible: a `CoercionRegistry` is just an
|
|
661
|
-
* `Array<CoercionEntry>` keyed at config time by `(input, output)`
|
|
662
|
-
* `SlimPrimitiveKind` literals. The library ships
|
|
663
|
-
* `defaultCoercionRules` (string→number, string→boolean) and
|
|
664
|
-
* `defineCoercion` for type-narrowed authoring; consumers spread the
|
|
665
|
-
* defaults to extend or supply their own array to replace.
|
|
666
|
-
*
|
|
667
|
-
* Coercion applies ONLY to user-typed DOM values flowing through
|
|
668
|
-
* the directive's assigner. Programmatic writes (`form.setValue`,
|
|
669
|
-
* `setValueWithInternalPath`) bypass coercion — they're authoritative
|
|
670
|
-
* writes whose strict typing is on the caller. This mirrors the
|
|
671
|
-
* `transforms` pipeline's user-input-only contract.
|
|
672
|
-
*/
|
|
673
|
-
|
|
674
|
-
/**
|
|
675
|
-
* Type-narrowing helper for authoring entries. At runtime it's
|
|
676
|
-
* identity; at compile time it preserves the `input` / `output`
|
|
677
|
-
* literal types so `transform`'s parameter is narrowed to the
|
|
678
|
-
* runtime type instead of widening to `SlimRuntimeOf<SlimPrimitiveKind>`.
|
|
679
|
-
*
|
|
680
|
-
* Without this helper, authoring `{ input: 'string', output:
|
|
681
|
-
* 'number', transform: (s) => ... }` against the broader
|
|
682
|
-
* `CoercionEntry` widens `s` to `string | number | boolean | ...`,
|
|
683
|
-
* forcing a cast in every transform body. `defineCoercion` is the
|
|
684
|
-
* opaque-free idiom.
|
|
685
|
-
*/
|
|
686
|
-
declare function defineCoercion<I extends SlimPrimitiveKind, O extends SlimPrimitiveKind>(entry: CoercionEntry<I, O>): CoercionEntry<I, O>;
|
|
687
|
-
/**
|
|
688
|
-
* Internal index built from a `CoercionRegistry` at config-resolve
|
|
689
|
-
* time. Keyed by `${input}->${output}` for O(1) per-keystroke
|
|
690
|
-
* dispatch. The authoring shape (array, ergonomic, type-narrowing-
|
|
691
|
-
* friendly) and the dispatch shape (Map, fast) decouple cleanly.
|
|
692
|
-
*/
|
|
693
|
-
type CoercionIndex = ReadonlyMap<`${SlimPrimitiveKind}->${SlimPrimitiveKind}`, CoercionEntry>;
|
|
694
|
-
/**
|
|
695
|
-
* The library's built-in registry. Two cells: string→number and
|
|
696
|
-
* string→boolean. Re-exported so consumers can spread it when
|
|
697
|
-
* supplying a custom registry that extends defaults.
|
|
698
|
-
*/
|
|
699
|
-
declare const defaultCoercionRules: CoercionRegistry;
|
|
700
|
-
|
|
701
|
-
type FormStore<F extends GenericForm, G extends GenericForm = F> = {
|
|
702
|
-
readonly formKey: FormKey;
|
|
703
|
-
readonly form: Ref<F>;
|
|
704
|
-
readonly fields: Map<PathKey, FieldRecord>;
|
|
705
|
-
readonly elements: Map<PathKey, ElementRecord>;
|
|
706
|
-
/**
|
|
707
|
-
* Schema-driven errors. Written ONLY by the schema validation pipeline:
|
|
708
|
-
* `scheduleFieldValidation`, `handleSubmit`, the construction-time seed,
|
|
709
|
-
* history restore, and hydration. Cleared by `reset` / `resetField` and by
|
|
710
|
-
* a successful submit. `setErrors` / `clearErrors` do NOT touch this Map.
|
|
711
|
-
*/
|
|
712
|
-
readonly schemaErrors: Map<PathKey, ValidationError[]>;
|
|
713
|
-
/**
|
|
714
|
-
* User-injected errors. Written ONLY by the `setErrors` / `clearErrors`
|
|
715
|
-
* API surface (and history / hydration replay). Survives schema revalidation and
|
|
716
|
-
* successful submits — the consumer owns its lifetime explicitly.
|
|
717
|
-
*/
|
|
718
|
-
readonly userErrors: Map<PathKey, ValidationError[]>;
|
|
719
|
-
/**
|
|
720
|
-
* Reactively-derived "No value supplied" errors. Pure function of
|
|
721
|
-
* `(blankPaths, schema.isRequiredAtPath)` — no writers, no clears.
|
|
722
|
-
* Membership tracks `blankPaths` automatically: typing a value into
|
|
723
|
-
* a blank required numeric field removes the path from `blankPaths`
|
|
724
|
-
* and the derived error vanishes; clearing the numeric input re-adds
|
|
725
|
-
* the path and the error reappears. The `errors` proxy and
|
|
726
|
-
* `getErrorsForPath` merge this map in alongside `schemaErrors` and
|
|
727
|
-
* `userErrors`, so consumers see the "this required field is empty"
|
|
728
|
-
* error the moment it's true — no `validate()` / `handleSubmit`
|
|
729
|
-
* call required. Honors the founding principle that
|
|
730
|
-
* `errors = f(schema, state)`.
|
|
731
|
-
*
|
|
732
|
-
* Most entries flow through this map for `number` / `bigint` leaves
|
|
733
|
-
* (where the side-channel is needed to distinguish "user typed 0"
|
|
734
|
-
* from "user supplied nothing"). String / boolean leaves only land
|
|
735
|
-
* here when the consumer explicitly opted in via the `unset`
|
|
736
|
-
* sentinel — see `docs/validation/blank.md`.
|
|
737
|
-
*/
|
|
738
|
-
readonly derivedBlankErrors: ComputedRef<ReadonlyMap<PathKey, ValidationError[]>>;
|
|
739
|
-
readonly originals: Map<PathKey, OriginalsRecord>;
|
|
740
|
-
/**
|
|
741
|
-
* Reactive set of paths whose displayed state should be EMPTY even
|
|
742
|
-
* though storage holds a real, schema-conformant value (the slim
|
|
743
|
-
* default). It exists exclusively to record **storage / display
|
|
744
|
-
* divergence** — the case where the runtime can't tell "user typed
|
|
745
|
-
* 0" from "user supplied nothing" by looking at storage alone.
|
|
746
|
-
*
|
|
747
|
-
* The mechanism shines for `number` / `bigint`: storage holds the
|
|
748
|
-
* slim default (`0` / `0n`) but the DOM input shows `''`, so the
|
|
749
|
-
* directive's input listener marks the path here on clear. Strings
|
|
750
|
-
* and booleans don't need it — `''` storage equals `''` display,
|
|
751
|
-
* `false` storage equals unchecked display — so they're never
|
|
752
|
-
* auto-marked. Consumers can still mark any primitive leaf
|
|
753
|
-
* explicitly via the `unset` sentinel (`defaultValues: { x: unset }`,
|
|
754
|
-
* `setValue('x', unset)`, `reset({ x: unset })`); the mark is then
|
|
755
|
-
* a documented signal of consumer intent rather than runtime
|
|
756
|
-
* inference.
|
|
757
|
-
*
|
|
758
|
-
* Reads (`displayValue` computed, `fields.<path>.blank`,
|
|
759
|
-
* `derivedBlankErrors` computed) track via Vue 3.5's reactive Set
|
|
760
|
-
* handlers. Writes happen inside `setValueAtPath` (gate-hook
|
|
761
|
-
* bookkeeping: `blank: true` meta adds the path; any other write
|
|
762
|
-
* removes it) and `reset`.
|
|
763
|
-
*
|
|
764
|
-
* Storage NEVER reflects this set — calculations and reads against
|
|
765
|
-
* `form.value` see the slim default. The set is purely a UI/intent
|
|
766
|
-
* channel that `derivedBlankErrors` consults to surface
|
|
767
|
-
* "No value supplied" errors for required schemas.
|
|
768
|
-
*
|
|
769
|
-
* See `docs/validation/blank.md` for the conceptual model.
|
|
770
|
-
*/
|
|
771
|
-
readonly blankPaths: Set<PathKey>;
|
|
772
|
-
/**
|
|
773
|
-
* Snapshot of `blankPaths` captured at construction (and
|
|
774
|
-
* re-captured on `reset(args)`). Used by dirty calculation: a path
|
|
775
|
-
* whose membership differs from the snapshot is dirty even if
|
|
776
|
-
* storage matches the original. Eagerly populated to avoid a "dirty
|
|
777
|
-
* on first read" race after construction.
|
|
778
|
-
*/
|
|
779
|
-
readonly originalBlankPaths: Set<PathKey>;
|
|
780
|
-
readonly schema: AbstractSchema<F, G>;
|
|
781
|
-
/**
|
|
782
|
-
* Server-side flag, plumbed in from `registry.ssr`. The
|
|
783
|
-
* `register()`-returned `markConnectedOptimistically()` reads this
|
|
784
|
-
* before flipping `connected: true`; on the client it's a no-op so
|
|
785
|
-
* the eventual directive lifecycle remains the source of truth.
|
|
786
|
-
*/
|
|
787
|
-
readonly ssr: boolean;
|
|
788
|
-
/**
|
|
789
|
-
* Resolved `getDisplayState` predicate driving `field.displayState`,
|
|
790
|
-
* the `show*` booleans, and their `form.meta` rollups. Resolved once
|
|
791
|
-
* at construction via `resolveGetDisplayState(options.getDisplayState)`;
|
|
792
|
-
* `undefined` config falls through to `defaultDisplayState`. The
|
|
793
|
-
* field-state computeds read the resolved function directly on every
|
|
794
|
-
* read.
|
|
795
|
-
*/
|
|
796
|
-
readonly getDisplayState: GetDisplayState;
|
|
797
|
-
/**
|
|
798
|
-
* Per-form display engine: owns the clock and the single timer the timed
|
|
799
|
-
* `getDisplayState` reducer policy needs, keeping the reducer itself a
|
|
800
|
-
* pure `(prev, ctx) => next` function. The field-state computeds route
|
|
801
|
-
* every `displayState` read through `displayEngine.resolve(...)`, which
|
|
802
|
-
* threads the path's previous machine, persists or evicts the result, and
|
|
803
|
-
* re-arms the nearest-deadline timer. Constructed once at form
|
|
804
|
-
* construction; torn down via `registerCleanup` on store eviction.
|
|
805
|
-
*/
|
|
806
|
-
readonly displayEngine: DisplayEngine;
|
|
807
|
-
readonly submitting: Ref<boolean>;
|
|
808
|
-
readonly activeSubmissions: Ref<number>;
|
|
809
|
-
readonly submissionAttempts: Ref<number>;
|
|
810
|
-
/**
|
|
811
|
-
* `true` once a `handleSubmit` callback resolved without throwing.
|
|
812
|
-
* Independent of `submissionAttempts` — a failed submit increments
|
|
813
|
-
* attempts but leaves `submitted` at `false`. Cleared by `reset()`
|
|
814
|
-
* alongside the rest of the submission surface.
|
|
815
|
-
*/
|
|
816
|
-
readonly submitted: Ref<boolean>;
|
|
817
|
-
readonly submitError: Ref<Error | null>;
|
|
818
|
-
readonly departAttempts: Ref<number>;
|
|
819
|
-
/**
|
|
820
|
-
* `true` while a function-form `defaultValues` factory is in flight.
|
|
821
|
-
* Stays `false` for plain-value `defaultValues`. Shared across every
|
|
822
|
-
* `useForm({ key })` call that resolves to this store — the second
|
|
823
|
-
* caller sees the first caller's hydration state.
|
|
824
|
-
*/
|
|
825
|
-
readonly hydrating: Ref<boolean>;
|
|
826
|
-
/**
|
|
827
|
-
* Error from the most recent function-form `defaultValues` factory.
|
|
828
|
-
* Normalized to a `ValidationError` (code `atta:hydration-failed`) so the
|
|
829
|
-
* shape matches `form.errors` / `form.meta.errors` entries. `null` when
|
|
830
|
-
* no factory has fired or the last one succeeded.
|
|
831
|
-
*/
|
|
832
|
-
readonly hydrateError: Ref<ValidationError | null>;
|
|
833
|
-
/**
|
|
834
|
-
* The function-form `defaultValues` factory, captured at the first
|
|
835
|
-
* `useForm({ key })` call that wired this store. `undefined` for
|
|
836
|
-
* plain-value forms. Read by `form.rehydrate()`.
|
|
837
|
-
*/
|
|
838
|
-
readonly defaultValuesFactory: Ref<(() => unknown | Promise<unknown>) | undefined>;
|
|
839
|
-
/**
|
|
840
|
-
* `true` when this store carries an SSR prefetch queue (server path
|
|
841
|
-
* where `state.activate()` must enqueue intent before deciding
|
|
842
|
-
* whether to fire). The flag lets `buildFormApi` skip the lazy
|
|
843
|
-
* activation gate for forms with no factory AND no SSR prefetch —
|
|
844
|
-
* the common client-side case where `gated()` is otherwise pure
|
|
845
|
-
* reactive overhead on every public method call.
|
|
846
|
-
*/
|
|
847
|
-
readonly hasSsrPrefetch: boolean;
|
|
848
|
-
/**
|
|
849
|
-
* `true` once the form's effective defaults have been applied —
|
|
850
|
-
* sync `defaultValues` at construction, or async factory whose
|
|
851
|
-
* settle completed. Stays `false` for dormant lazy forms until they
|
|
852
|
-
* activate. Read by `useWizard` to decide whether seed status or
|
|
853
|
-
* live meta should surface.
|
|
854
|
-
*/
|
|
855
|
-
readonly defaultsResolved: Ref<boolean>;
|
|
856
|
-
/**
|
|
857
|
-
* `true` once the captured async factory has been kicked off (set
|
|
858
|
-
* synchronously by `activate()`, before the factory itself resolves).
|
|
859
|
-
* Distinct from `defaultsResolved`, which only flips after the factory
|
|
860
|
-
* settles. The pair lets the API surface tell "we've started" apart
|
|
861
|
-
* from "we're done."
|
|
862
|
-
*/
|
|
863
|
-
readonly activated: Ref<boolean>;
|
|
864
|
-
/**
|
|
865
|
-
* In-flight activation promise. Concurrent callers (cross-component
|
|
866
|
-
* SSR consumers, recursive factory reads, parallel `activate()`
|
|
867
|
-
* calls) receive the same promise, ensuring the factory runs once
|
|
868
|
-
* even under contention.
|
|
869
|
-
*/
|
|
870
|
-
readonly activationPromise: Ref<Promise<void> | undefined>;
|
|
871
|
-
/**
|
|
872
|
-
* Idempotent activation entrypoint. Fires the captured function-form
|
|
873
|
-
* `defaultValues` factory on first call and stores the in-flight
|
|
874
|
-
* promise. Subsequent calls return the same promise until the factory
|
|
875
|
-
* settles; thereafter calls return `Promise.resolve()`. Plain-value
|
|
876
|
-
* forms (no factory captured) always return a resolved promise. The
|
|
877
|
-
* public API surface routes all reactive interactions (getters and
|
|
878
|
-
* methods, except `key`) through this entrypoint so the form
|
|
879
|
-
* activates on first use.
|
|
880
|
-
*/
|
|
881
|
-
activate(): Promise<void>;
|
|
882
|
-
/**
|
|
883
|
-
* Re-fire the captured function-form `defaultValues` factory. Throws
|
|
884
|
-
* synchronously when no factory was captured (plain-value form).
|
|
885
|
-
* Resolves after `hydrating` flips back to `false`; consumers can
|
|
886
|
-
* `await form.rehydrate()` to gate UI on the fresh load.
|
|
887
|
-
*
|
|
888
|
-
* Does NOT touch dirty / touched / submit state — chain
|
|
889
|
-
* `form.reset()` if you want a clean baseline.
|
|
890
|
-
*/
|
|
891
|
-
rehydrate(): Promise<void>;
|
|
892
|
-
/**
|
|
893
|
-
* Incremented by every `reset()` call. The submit wrapper captures
|
|
894
|
-
* this at entry and skips writing `submitError` from a catch that
|
|
895
|
-
* fires *after* a reset — otherwise a reset-during-submit would
|
|
896
|
-
* visibly clear `submitError` and then have it reappear when the
|
|
897
|
-
* in-flight promise rejects.
|
|
898
|
-
*/
|
|
899
|
-
readonly submissionGeneration: Ref<number>;
|
|
900
|
-
/**
|
|
901
|
-
* Counts in-flight validation calls across every `validate()` ref and
|
|
902
|
-
* every `validateAsync(...)` / `handleSubmit` pre-check. `validating`
|
|
903
|
-
* on the public API mirrors `activeValidations.value > 0`. Tracked
|
|
904
|
-
* separately from submissions because a validate-while-submitting
|
|
905
|
-
* (e.g. a debounced field check overlapping a submit) needs to show
|
|
906
|
-
* the union of both surfaces.
|
|
907
|
-
*/
|
|
908
|
-
readonly activeValidations: Ref<number>;
|
|
909
|
-
/**
|
|
910
|
-
* `true` once the form has completed at least one validation pass
|
|
911
|
-
* — flips when `activeValidations` returns to 0 from any positive
|
|
912
|
-
* value. Until that happens, `meta.valid` and `field.valid` report
|
|
913
|
-
* `false` even when `schemaErrors.size === 0`, because the absence
|
|
914
|
-
* of errors at frame 1 is just "we haven't checked yet," not "we
|
|
915
|
-
* checked and it's clean."
|
|
916
|
-
*
|
|
917
|
-
* This closes the brief flash window for schemas where the slim
|
|
918
|
-
* default-derivation parse strips refinements (`.refine`,
|
|
919
|
-
* `.superRefine`, async validators): the slim parse passes, no
|
|
920
|
-
* construction-time errors land, and the queued microtask hasn't
|
|
921
|
-
* run yet — so without the gate, frame 1 paints the form as
|
|
922
|
-
* "valid" before the real verdict arrives a tick later.
|
|
923
|
-
*
|
|
924
|
-
* Initialized to `!strict`: non-strict consumers opt out of the
|
|
925
|
-
* validation pipeline by design, so locking them on
|
|
926
|
-
* `firstValidationDone === false` would defeat the opt-out.
|
|
927
|
-
* Reset is left untouched — the post-reset validation flips it
|
|
928
|
-
* back true on completion, same as the construction-time path.
|
|
535
|
+
* `true` once the form has completed at least one validation pass
|
|
536
|
+
* — flips when `activeValidations` returns to 0 from any positive
|
|
537
|
+
* value. Until that happens, `meta.valid` and `field.valid` report
|
|
538
|
+
* `false` even when `schemaErrors.size === 0`, because the absence
|
|
539
|
+
* of errors at frame 1 is just "we haven't checked yet," not "we
|
|
540
|
+
* checked and it's clean."
|
|
541
|
+
*
|
|
542
|
+
* This closes the brief flash window for schemas where the slim
|
|
543
|
+
* default-derivation parse strips refinements (`.refine`,
|
|
544
|
+
* `.superRefine`, async validators): the slim parse passes, no
|
|
545
|
+
* construction-time errors land, and the queued microtask hasn't
|
|
546
|
+
* run yet — so without the gate, frame 1 paints the form as
|
|
547
|
+
* "valid" before the real verdict arrives a tick later.
|
|
548
|
+
*
|
|
549
|
+
* Initialized to `!strict`: non-strict consumers opt out of the
|
|
550
|
+
* validation pipeline by design, so locking them on
|
|
551
|
+
* `firstValidationDone === false` would defeat the opt-out.
|
|
552
|
+
* Reset is left untouched — the post-reset validation flips it
|
|
553
|
+
* back true on completion, same as the construction-time path.
|
|
929
554
|
*/
|
|
930
555
|
readonly firstValidationDone: Ref<boolean>;
|
|
931
556
|
/**
|
|
@@ -1214,132 +839,658 @@ type FormStore<F extends GenericForm, G extends GenericForm = F> = {
|
|
|
1214
839
|
* `transformError`, register `holder` for later abort. Returns the run
|
|
1215
840
|
* token. See `InternalRegisterValue.beginTransform`.
|
|
1216
841
|
*/
|
|
1217
|
-
beginTransform(key: PathKey, holder: TransformAbortHolder): number;
|
|
1218
|
-
/** `true` while `token` is the live async-transform run at `key`. */
|
|
1219
|
-
isCurrentTransform(key: PathKey, token: number): boolean;
|
|
842
|
+
beginTransform(key: PathKey, holder: TransformAbortHolder): number;
|
|
843
|
+
/** `true` while `token` is the live async-transform run at `key`. */
|
|
844
|
+
isCurrentTransform(key: PathKey, token: number): boolean;
|
|
845
|
+
/**
|
|
846
|
+
* Close the run `token` at `key`: release the counters (no-op if the
|
|
847
|
+
* run was already released by a supersede / cancel) and flush settled
|
|
848
|
+
* `settleTransforms` waiters.
|
|
849
|
+
*/
|
|
850
|
+
endTransform(key: PathKey, token: number): void;
|
|
851
|
+
/** Record a per-field normalization failure at `key` (`field.transformError`). */
|
|
852
|
+
setTransformError(key: PathKey, err: Error): void;
|
|
853
|
+
/**
|
|
854
|
+
* Abort + release every in-flight async-transform run (all paths) and
|
|
855
|
+
* clear `transformErrors`. Mirrors `cancelFieldValidation`; called by
|
|
856
|
+
* `reset()` and store teardown.
|
|
857
|
+
*/
|
|
858
|
+
cancelTransforms(): void;
|
|
859
|
+
/**
|
|
860
|
+
* Path-scoped counterpart to `cancelTransforms`: abort + release only
|
|
861
|
+
* the runs at-or-under `prefix`, clearing their `transformError`.
|
|
862
|
+
* Called by `resetField`.
|
|
863
|
+
*/
|
|
864
|
+
cancelTransformsUnder(prefix: Path): void;
|
|
865
|
+
/**
|
|
866
|
+
* Resolve once async transforms are quiescent — globally (`path`
|
|
867
|
+
* omitted) or at-or-under `path`. Resolve-never-reject. See
|
|
868
|
+
* `UseFormReturnType.settleTransforms`.
|
|
869
|
+
*/
|
|
870
|
+
settleTransforms(path?: string | Path): Promise<void>;
|
|
871
|
+
/**
|
|
872
|
+
* Kick off (or schedule) a field-level validation run for `path`. Pass
|
|
873
|
+
* `path = []` to cover the whole form; `applySchemaErrorsForSubtree`
|
|
874
|
+
* then wipes every `schemaErrors` entry and replaces them with the
|
|
875
|
+
* adapter's full async response. Used by persistence's post-hydration
|
|
876
|
+
* revalidation and by the construction-time async-refine seed.
|
|
877
|
+
*
|
|
878
|
+
* `immediate: true` skips the debounce window — the runtime kicks off
|
|
879
|
+
* the adapter call on the next microtask. Internal callsites use this
|
|
880
|
+
* for one-shot triggers; the per-keystroke writers pass `false` to
|
|
881
|
+
* coalesce rapid mutations under the configured debounceMs.
|
|
882
|
+
*
|
|
883
|
+
* `override` carries per-`useForm()`-instance values: when provided,
|
|
884
|
+
* the scheduler honors `override.mode` instead of the store's
|
|
885
|
+
* captured `validateOn`, and `override.debounceMs` instead of the
|
|
886
|
+
* store's captured `debounceMs`. Used so sibling instances sharing a
|
|
887
|
+
* FormStore can each validate on their own cadence.
|
|
888
|
+
*/
|
|
889
|
+
scheduleFieldValidation(path: Path, immediate: boolean, override?: {
|
|
890
|
+
readonly mode?: ValidateOn;
|
|
891
|
+
readonly debounceMs?: number;
|
|
892
|
+
}): void;
|
|
893
|
+
/**
|
|
894
|
+
* Subscribe to every `applyFormReplacement`. Fires synchronously
|
|
895
|
+
* after `form.value` has been swapped to `next` and all field /
|
|
896
|
+
* originals bookkeeping has run. Used by undo/redo to hook the single
|
|
897
|
+
* mutation funnel. The optional `meta` carries the originating call
|
|
898
|
+
* site's intent; subscribers that don't care about meta can ignore the
|
|
899
|
+
* parameter. Returns an unsubscribe function.
|
|
900
|
+
*/
|
|
901
|
+
onFormChange(listener: (next: F, meta?: WriteMeta) => void): () => void;
|
|
902
|
+
/**
|
|
903
|
+
* Subscribe to successful submissions. Fires after the consumer's
|
|
904
|
+
* `onSubmit` callback has resolved — not on validation failure,
|
|
905
|
+
* not on callback throw. The DevTools panel rides this to surface a
|
|
906
|
+
* submit event. Returns an unsubscribe function.
|
|
907
|
+
*/
|
|
908
|
+
onSubmitSuccess(listener: () => void): () => void;
|
|
909
|
+
/**
|
|
910
|
+
* Subscribe to `reset()` calls. Fires AFTER reset has replaced
|
|
911
|
+
* the form and cleared errors + lifecycle, so listeners see the
|
|
912
|
+
* fresh post-reset state. Used by the history module to drop the
|
|
913
|
+
* undo/redo stack on reset. Returns an unsubscribe function.
|
|
914
|
+
*/
|
|
915
|
+
onReset(listener: () => void): () => void;
|
|
916
|
+
/**
|
|
917
|
+
* Internal: notify submit-success subscribers. Called by
|
|
918
|
+
* `handleSubmit` in `process-form.ts` once the user callback has
|
|
919
|
+
* resolved. Consumers shouldn't call this directly.
|
|
920
|
+
*/
|
|
921
|
+
emitSubmitSuccess(): void;
|
|
922
|
+
/**
|
|
923
|
+
* Register a teardown function whose lifetime is bound to the
|
|
924
|
+
* FormStore itself (not a consumer's Vue effect scope). Called by
|
|
925
|
+
* `dispose()` when the last consumer unmounts. Used by persistence /
|
|
926
|
+
* history wiring so their subscribers aren't detached prematurely
|
|
927
|
+
* when only the first consumer unmounts but others remain.
|
|
928
|
+
*/
|
|
929
|
+
registerCleanup(fn: () => void): void;
|
|
930
|
+
/**
|
|
931
|
+
* Register an async drain function. Called by the registry before
|
|
932
|
+
* `dispose()` so async background work — chiefly the persistence
|
|
933
|
+
* layer's debounced storage writes — has a chance to settle without
|
|
934
|
+
* losing the last keystroke. Each registered function is awaited in
|
|
935
|
+
* parallel; failures are swallowed to keep eviction reliable.
|
|
936
|
+
*/
|
|
937
|
+
registerDrain(fn: () => Promise<void>): void;
|
|
938
|
+
/**
|
|
939
|
+
* Drain async work registered via `registerDrain`. Resolves once
|
|
940
|
+
* every registered drain has settled (in parallel). Safe to call
|
|
941
|
+
* repeatedly — registered drains decide their own idempotency.
|
|
942
|
+
*/
|
|
943
|
+
awaitPendingWrites(): Promise<void>;
|
|
944
|
+
/**
|
|
945
|
+
* Cache for per-state modules (history, persistence) that must
|
|
946
|
+
* outlive any single consumer. Subsequent `useForm` / `injectForm`
|
|
947
|
+
* calls for the same key read from this map so the public API shape
|
|
948
|
+
* is identical regardless of mount order. Keyed by a string identifier
|
|
949
|
+
* owned by the caller (e.g. `'history'`).
|
|
950
|
+
*/
|
|
951
|
+
readonly modules: Map<string, unknown>;
|
|
1220
952
|
/**
|
|
1221
|
-
*
|
|
1222
|
-
*
|
|
1223
|
-
* `
|
|
953
|
+
* Resolved schema-coercion index — the merged config from
|
|
954
|
+
* `createAttaform({ defaults: { coerce } })` ∪ `useForm({ coerce })`,
|
|
955
|
+
* keyed by `${input}->${output}` for O(1) per-keystroke dispatch.
|
|
956
|
+
* Empty Map when coercion is disabled. Read at `register()` time
|
|
957
|
+
* by `buildCoerceFn` to bake the per-path coerce closure on
|
|
958
|
+
* `RegisterValue.coerce`.
|
|
1224
959
|
*/
|
|
1225
|
-
|
|
1226
|
-
/** Record a per-field normalization failure at `key` (`field.transformError`). */
|
|
1227
|
-
setTransformError(key: PathKey, err: Error): void;
|
|
960
|
+
readonly coerceIndex: CoercionIndex;
|
|
1228
961
|
/**
|
|
1229
|
-
*
|
|
1230
|
-
*
|
|
1231
|
-
*
|
|
962
|
+
* Tear down non-reactive resources owned by this FormStore. Invoked
|
|
963
|
+
* by the registry when the last consumer unmounts. Cancels pending
|
|
964
|
+
* field-validation timers, drops every subscriber, and fires each
|
|
965
|
+
* cleanup hook registered via `registerCleanup`.
|
|
1232
966
|
*/
|
|
1233
|
-
|
|
967
|
+
dispose(): void;
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Public types for `useWizard` — the multistep-form orchestrator.
|
|
972
|
+
*
|
|
973
|
+
* The wizard is built around an ordered list of step slots. Each slot
|
|
974
|
+
* resolves to a participating form: an existing `useForm` reference, a
|
|
975
|
+
* bare string key (desugared to a noop form so affordance steps
|
|
976
|
+
* participate uniformly), an eagerly-evaluated function slot for
|
|
977
|
+
* runtime branching, or a `lazy()`-wrapped function slot that caches
|
|
978
|
+
* its resolution and re-fires only on its own tracked deps.
|
|
979
|
+
*
|
|
980
|
+
* The wizard surface is loosely keyed (`Record<FormKey, …>`).
|
|
981
|
+
* Cross-component flows threaded through `injectWizard` lose lexical
|
|
982
|
+
* key knowledge anyway, so the public read surface is a string-keyed
|
|
983
|
+
* record. Typed per-form access flows back through the original form
|
|
984
|
+
* refs and through `wizard.handleSubmit`'s `ctx.get(formRef)` accessor.
|
|
985
|
+
*/
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Minimum structural shape the wizard requires from a participating
|
|
989
|
+
* form. Constraining to the full `UseFormReturnType` would force
|
|
990
|
+
* contravariant unification of the storage / read shapes across all
|
|
991
|
+
* steps; the wizard does not care about those — it routes by `key` at
|
|
992
|
+
* runtime and exposes the original form objects untouched.
|
|
993
|
+
*/
|
|
994
|
+
type AnyForm = {
|
|
995
|
+
readonly key: FormKey;
|
|
996
|
+
};
|
|
997
|
+
/**
|
|
998
|
+
* Per-form summary surface — what `wizard.statuses[key]` exposes (and
|
|
999
|
+
* what `defaultStatuses` seeds). Distinct from `form.meta`: `FormStatus`
|
|
1000
|
+
* is the cross-step rollup optimized for template ergonomics
|
|
1001
|
+
* (`{{ wizard.statuses.cargo.valid }}`), while `form.meta` carries the
|
|
1002
|
+
* full per-form lifecycle surface.
|
|
1003
|
+
*
|
|
1004
|
+
* Field semantics:
|
|
1005
|
+
* - `valid` — `form.meta.valid`. `false` while errors exist or while
|
|
1006
|
+
* the first-validation-done gate has not flipped.
|
|
1007
|
+
* - `dirty` — `form.meta.dirty`. `true` once any value differs from
|
|
1008
|
+
* the original defaults.
|
|
1009
|
+
* - `submitted` — `form.meta.submitted`. `true` once a `handleSubmit`
|
|
1010
|
+
* callback has resolved without throwing. A failed submit
|
|
1011
|
+
* (validation or callback rejection) leaves this `false`;
|
|
1012
|
+
* `submissionAttempts > 0` is the "user has tried" signal.
|
|
1013
|
+
* - `errorCount` — `form.meta.errorCount`. Count of active validation
|
|
1014
|
+
* errors (zero when valid).
|
|
1015
|
+
*
|
|
1016
|
+
* Noop forms generated for string slots surface as default-valid
|
|
1017
|
+
* (`{ valid: true, dirty: false, submitted: false, errorCount: 0 }`).
|
|
1018
|
+
*/
|
|
1019
|
+
type FormStatus = {
|
|
1020
|
+
readonly valid: boolean;
|
|
1021
|
+
readonly dirty: boolean;
|
|
1022
|
+
readonly submitted: boolean;
|
|
1023
|
+
readonly errorCount: number;
|
|
1024
|
+
};
|
|
1025
|
+
/**
|
|
1026
|
+
* Flat error shape returned per form by `wizard.allErrors[key]`. Each
|
|
1027
|
+
* entry carries the formKey + path tuple so consumers can route to the
|
|
1028
|
+
* offending field from a wizard-wide error summary.
|
|
1029
|
+
*/
|
|
1030
|
+
type WizardAggregateError = {
|
|
1031
|
+
readonly formKey: FormKey;
|
|
1032
|
+
readonly path: ReadonlyArray<string | number>;
|
|
1033
|
+
readonly message: string;
|
|
1034
|
+
readonly code?: string;
|
|
1035
|
+
};
|
|
1036
|
+
/**
|
|
1037
|
+
* Mirror of `form.values`' call-or-read pattern, one level deep.
|
|
1038
|
+
* Drillable as `wizard.statuses.cargo.valid` (readable), as
|
|
1039
|
+
* `wizard.statuses('cargo')` (callable single-key), or as
|
|
1040
|
+
* `wizard.statuses()` (callable no-arg returns the whole record).
|
|
1041
|
+
*/
|
|
1042
|
+
type WizardStatusesProxy<S extends Record<string, FormStatus>> = ((key?: keyof S) => FormStatus | S) & Readonly<S>;
|
|
1043
|
+
/**
|
|
1044
|
+
* One compiled position in the wizard's flow. The wizard surface
|
|
1045
|
+
* exposes an ordered array of these as `wizard.steps`, plus a
|
|
1046
|
+
* `wizard.forms` record keyed by `step.key` for direct lookup.
|
|
1047
|
+
*
|
|
1048
|
+
* String slots in the source `steps` array desugar to noop forms
|
|
1049
|
+
* before compilation, so every compiled step carries a `form`
|
|
1050
|
+
* regardless of source kind.
|
|
1051
|
+
*/
|
|
1052
|
+
type CompiledStep = {
|
|
1053
|
+
readonly key: FormKey;
|
|
1054
|
+
readonly form: AnyForm;
|
|
1055
|
+
};
|
|
1056
|
+
/**
|
|
1057
|
+
* Shape of a participating form as seen from inside a function slot's
|
|
1058
|
+
* `ctx.forms[key]` lookup. Adds `values` to the structural `AnyForm`
|
|
1059
|
+
* minimum so routing decisions can read live form state.
|
|
1060
|
+
*
|
|
1061
|
+
* Values are typed loose because the wizard does not generically thread
|
|
1062
|
+
* each step's schema through `ctx.forms`. For typed access inside slot
|
|
1063
|
+
* bodies, close over the original form ref instead of routing through
|
|
1064
|
+
* `ctx.forms`.
|
|
1065
|
+
*/
|
|
1066
|
+
type WizardCtxForm = AnyForm & {
|
|
1067
|
+
readonly values: Readonly<Record<string, unknown>>;
|
|
1068
|
+
};
|
|
1069
|
+
/**
|
|
1070
|
+
* Context object passed to function slots in the `steps` array. The
|
|
1071
|
+
* `forms` record exposes the wizard's statically-known forms (every
|
|
1072
|
+
* top-level `AnyForm` slot plus every noop form synthesized for a
|
|
1073
|
+
* top-level string slot). `currentKey` mirrors the live wizard step.
|
|
1074
|
+
*
|
|
1075
|
+
* Function slots re-evaluate reactively when the values they read
|
|
1076
|
+
* mutate (typically `ctx.forms.<key>.values.<path>`). The `forms`
|
|
1077
|
+
* accumulator itself is stable across re-evaluations so the slot's
|
|
1078
|
+
* lookup identity stays referentially equal. Effectful slot bodies
|
|
1079
|
+
* should be avoided; routing decisions live here.
|
|
1080
|
+
*/
|
|
1081
|
+
type WizardCtx = {
|
|
1082
|
+
readonly forms: Readonly<Record<FormKey, WizardCtxForm>>;
|
|
1083
|
+
readonly currentKey: FormKey | undefined;
|
|
1084
|
+
};
|
|
1085
|
+
/**
|
|
1086
|
+
* Internal phantom brand for `LazyMarker`. The runtime brand symbol
|
|
1087
|
+
* lives in `core/wizard-lazy.ts`; this declaration keeps the marker
|
|
1088
|
+
* type unforgeable without circular module imports.
|
|
1089
|
+
*/
|
|
1090
|
+
declare const _lazyBrand: unique symbol;
|
|
1091
|
+
/**
|
|
1092
|
+
* Brand-typed marker returned by `lazy((ctx) => …)`. Wrapping a
|
|
1093
|
+
* function slot in `lazy()` gives that slot its own memoization cache:
|
|
1094
|
+
* the resolver fires once on the first compile pass, and the result
|
|
1095
|
+
* stays cached until one of the resolver's own tracked reactive reads
|
|
1096
|
+
* changes (or `wizard.reset()` invalidates the cache). Heavy or
|
|
1097
|
+
* one-shot lookups (network-backed factories, expensive derivations)
|
|
1098
|
+
* do not re-fire because an unrelated slot's deps changed.
|
|
1099
|
+
*
|
|
1100
|
+
* Construct via the `lazy()` helper exported from the same entry as
|
|
1101
|
+
* `useWizard`. The marker is opaque at the type level; consumers do
|
|
1102
|
+
* not assemble it directly.
|
|
1103
|
+
*/
|
|
1104
|
+
type LazyMarker<Ctx = WizardCtx> = {
|
|
1105
|
+
readonly [_lazyBrand]: true;
|
|
1106
|
+
readonly resolve: (ctx: Ctx) => AnyForm | string | null | undefined;
|
|
1107
|
+
};
|
|
1108
|
+
/**
|
|
1109
|
+
* One position in the source `useWizard({ steps })` array. Each slot
|
|
1110
|
+
* resolves to a compiled `{ key, form }` step, or drops out:
|
|
1111
|
+
*
|
|
1112
|
+
* - `AnyForm` — a form declared via `useForm`. Surfaced as-is.
|
|
1113
|
+
* - `string` — bare key. The wizard generates a noop form
|
|
1114
|
+
* under the hood so the external surface stays
|
|
1115
|
+
* uniform across affordance positions (intro,
|
|
1116
|
+
* terms, congratulations, review surfaces).
|
|
1117
|
+
* - `null` / `undefined` — a literal absence, dropped from the compiled
|
|
1118
|
+
* list. Lets a conditional step read inline as
|
|
1119
|
+
* `cond ? form : null` without pre-filtering the
|
|
1120
|
+
* array.
|
|
1121
|
+
* - function — eager slot, re-evaluates reactively. Returns
|
|
1122
|
+
* one of the above, or `null` / `undefined` to
|
|
1123
|
+
* drop the slot from the compiled list.
|
|
1124
|
+
* - `LazyMarker` — memoized function slot (see `lazy`).
|
|
1125
|
+
*/
|
|
1126
|
+
type StepSlot<Ctx = WizardCtx> = AnyForm | string | null | undefined | ((ctx: Ctx) => AnyForm | string | null | undefined) | LazyMarker<Ctx>;
|
|
1127
|
+
/**
|
|
1128
|
+
* Shape returned by the `restore` callback. Carries the active step's
|
|
1129
|
+
* key; intentionally open-ended (object form) so future additions land
|
|
1130
|
+
* without a callback-signature break.
|
|
1131
|
+
*/
|
|
1132
|
+
type WizardRestoreState = {
|
|
1133
|
+
readonly step?: FormKey;
|
|
1134
|
+
};
|
|
1135
|
+
/**
|
|
1136
|
+
* `restore` callback signature. Invoked at construction and watched
|
|
1137
|
+
* reactively via `watchEffect` so external state changes (browser
|
|
1138
|
+
* back/forward, cross-tab events, route changes) re-apply through the
|
|
1139
|
+
* wizard. Returning `undefined` falls through to the first step.
|
|
1140
|
+
*/
|
|
1141
|
+
type WizardRestoreFn = () => WizardRestoreState | undefined;
|
|
1142
|
+
/**
|
|
1143
|
+
* `persist` callback signature. Invoked whenever `wizard.currentStep`
|
|
1144
|
+
* changes; the wizard diffs against the last persisted value to break
|
|
1145
|
+
* the restore-persist loop, so the callback only fires when the active
|
|
1146
|
+
* step actually moves.
|
|
1147
|
+
*/
|
|
1148
|
+
type WizardPersistFn = (state: WizardRestoreState) => void;
|
|
1149
|
+
/**
|
|
1150
|
+
* Submit context passed to the `onSubmit` callback registered via
|
|
1151
|
+
* `wizard.handleSubmit(onSubmit, onError?)`. `handleSubmit` always
|
|
1152
|
+
* processes the whole step list, so `values` carries every form's
|
|
1153
|
+
* parsed output regardless of which step fired the submit.
|
|
1154
|
+
*
|
|
1155
|
+
* - `values` — namespaced aggregate keyed by form key, mirroring
|
|
1156
|
+
* `wizard.allValues`. Reflects parsed output for every
|
|
1157
|
+
* form whose validation has settled; noops contribute an
|
|
1158
|
+
* empty record.
|
|
1159
|
+
* - `get(form)` — typed accessor that reads the parsed output for a
|
|
1160
|
+
* specific form ref. Works across cross-component graphs
|
|
1161
|
+
* because the form ref carries its schema info.
|
|
1162
|
+
* - `currentKey` — key of the step that fired this submission.
|
|
1163
|
+
* - `isFinal` — `true` when `currentKey` is the last position in
|
|
1164
|
+
* `wizard.steps`. Positional only: it reports where the
|
|
1165
|
+
* submit fired, never what got validated. A user who steps
|
|
1166
|
+
* back, edits, and submits from the middle sees
|
|
1167
|
+
* `isFinal === false`, yet the whole list is still
|
|
1168
|
+
* processed and `done` still latches on success.
|
|
1169
|
+
*/
|
|
1170
|
+
type WizardSubmitContext = {
|
|
1171
|
+
readonly values: Readonly<Record<FormKey, unknown>>;
|
|
1172
|
+
readonly get: <F extends AnyForm>(form: F) => F extends {
|
|
1173
|
+
readonly values: infer V;
|
|
1174
|
+
} ? V : unknown;
|
|
1175
|
+
readonly currentKey: FormKey;
|
|
1176
|
+
readonly isFinal: boolean;
|
|
1177
|
+
};
|
|
1178
|
+
/**
|
|
1179
|
+
* `onSubmit` callback registered via `wizard.handleSubmit`. Sync or
|
|
1180
|
+
* async; the returned promise gates `wizard.submitting`.
|
|
1181
|
+
*/
|
|
1182
|
+
type WizardOnSubmit = (ctx: WizardSubmitContext) => void | Promise<void>;
|
|
1183
|
+
/**
|
|
1184
|
+
* Optional `onError` callback registered via `wizard.handleSubmit`.
|
|
1185
|
+
* Receives the aggregate error list spanning EVERY step (handleSubmit
|
|
1186
|
+
* validates the whole wizard), so a failed submit surfaces every form's
|
|
1187
|
+
* errors at once, not just the active step's. Entries originate from
|
|
1188
|
+
* per-form validation, activation failures (`atta:activation-failed`),
|
|
1189
|
+
* and a submit callback that left errors on a processed step (the
|
|
1190
|
+
* `setErrors(...); return` server-rejection path). Sync or async; the
|
|
1191
|
+
* returned promise gates `wizard.submitting`.
|
|
1192
|
+
*/
|
|
1193
|
+
type WizardOnError = (errors: readonly WizardAggregateError[]) => void | Promise<void>;
|
|
1194
|
+
/**
|
|
1195
|
+
* Options for `useWizard({ steps, … })`. `steps` is the only required
|
|
1196
|
+
* field; the rest are optional and default sensibly for the common
|
|
1197
|
+
* URL-synchronized wizard case.
|
|
1198
|
+
*/
|
|
1199
|
+
type WizardOptions = {
|
|
1234
1200
|
/**
|
|
1235
|
-
*
|
|
1236
|
-
*
|
|
1237
|
-
* Called by `resetField`.
|
|
1201
|
+
* Ordered list of slots that compile into the wizard's positional
|
|
1202
|
+
* step list. See `StepSlot` for the per-slot shape contract.
|
|
1238
1203
|
*/
|
|
1239
|
-
|
|
1204
|
+
readonly steps: ReadonlyArray<StepSlot>;
|
|
1240
1205
|
/**
|
|
1241
|
-
*
|
|
1242
|
-
*
|
|
1243
|
-
*
|
|
1206
|
+
* Identifier used to register the wizard handle in the per-app
|
|
1207
|
+
* registry. Descendant components call `injectWizard(key)` to reach
|
|
1208
|
+
* the same wizard without prop-threading. Anonymous wizards (option
|
|
1209
|
+
* omitted) get a synthetic `__atta:anon-wizard:<id>` key resolved
|
|
1210
|
+
* via `useId()` so SSR-rendered and client-hydrated trees agree on
|
|
1211
|
+
* the same registry entry; the synthetic key is opaque and
|
|
1212
|
+
* descendants reach an anonymous wizard via ambient `injectWizard()`
|
|
1213
|
+
* rather than by key.
|
|
1214
|
+
*
|
|
1215
|
+
* Duplicate-key registration is first-wins-silently (dev-warn on the
|
|
1216
|
+
* second registration) to mirror `useForm`'s shared-key behavior.
|
|
1217
|
+
* The dev-warn fires only for explicit keys — two anonymous wizards
|
|
1218
|
+
* are guaranteed distinct synthetic keys, so the warning never
|
|
1219
|
+
* misfires on independent anonymous wizards on the same page.
|
|
1244
1220
|
*/
|
|
1245
|
-
|
|
1221
|
+
readonly key?: string;
|
|
1246
1222
|
/**
|
|
1247
|
-
*
|
|
1248
|
-
* `
|
|
1249
|
-
*
|
|
1250
|
-
*
|
|
1251
|
-
* revalidation and by the construction-time async-refine seed.
|
|
1223
|
+
* Seed status payload used while a form is pre-resolved (async
|
|
1224
|
+
* `defaultValues` in flight, or wizard-deferred non-current).
|
|
1225
|
+
* Mirrors `defaultValues`' trichotomy: plain object, sync factory,
|
|
1226
|
+
* or async factory.
|
|
1252
1227
|
*
|
|
1253
|
-
*
|
|
1254
|
-
*
|
|
1255
|
-
*
|
|
1256
|
-
*
|
|
1228
|
+
* Status resolution priority per form:
|
|
1229
|
+
* 1. `store.defaultsResolved === true` → derive from `form.meta`
|
|
1230
|
+
* 2. else noop form → built-in always-valid status
|
|
1231
|
+
* 3. else seed value for this key → frozen seed
|
|
1232
|
+
* 4. else → pending sentinel
|
|
1257
1233
|
*
|
|
1258
|
-
*
|
|
1259
|
-
*
|
|
1260
|
-
* captured `validateOn`, and `override.debounceMs` instead of the
|
|
1261
|
-
* store's captured `debounceMs`. Used so sibling instances sharing a
|
|
1262
|
-
* FormStore can each validate on their own cadence.
|
|
1263
|
-
*/
|
|
1264
|
-
scheduleFieldValidation(path: Path, immediate: boolean, override?: {
|
|
1265
|
-
readonly mode?: ValidateOn;
|
|
1266
|
-
readonly debounceMs?: number;
|
|
1267
|
-
}): void;
|
|
1268
|
-
/**
|
|
1269
|
-
* Subscribe to every `applyFormReplacement`. Fires synchronously
|
|
1270
|
-
* after `form.value` has been swapped to `next` and all field /
|
|
1271
|
-
* originals bookkeeping has run. Used by undo/redo to hook the single
|
|
1272
|
-
* mutation funnel. The optional `meta` carries the originating call
|
|
1273
|
-
* site's intent; subscribers that don't care about meta can ignore the
|
|
1274
|
-
* parameter. Returns an unsubscribe function.
|
|
1275
|
-
*/
|
|
1276
|
-
onFormChange(listener: (next: F, meta?: WriteMeta) => void): () => void;
|
|
1277
|
-
/**
|
|
1278
|
-
* Subscribe to successful submissions. Fires after the consumer's
|
|
1279
|
-
* `onSubmit` callback has resolved — not on validation failure,
|
|
1280
|
-
* not on callback throw. The DevTools panel rides this to surface a
|
|
1281
|
-
* submit event. Returns an unsubscribe function.
|
|
1282
|
-
*/
|
|
1283
|
-
onSubmitSuccess(listener: () => void): () => void;
|
|
1284
|
-
/**
|
|
1285
|
-
* Subscribe to `reset()` calls. Fires AFTER reset has replaced
|
|
1286
|
-
* the form and cleared errors + lifecycle, so listeners see the
|
|
1287
|
-
* fresh post-reset state. Used by the history module to drop the
|
|
1288
|
-
* undo/redo stack on reset. Returns an unsubscribe function.
|
|
1289
|
-
*/
|
|
1290
|
-
onReset(listener: () => void): () => void;
|
|
1291
|
-
/**
|
|
1292
|
-
* Internal: notify submit-success subscribers. Called by
|
|
1293
|
-
* `handleSubmit` in `process-form.ts` once the user callback has
|
|
1294
|
-
* resolved. Consumers shouldn't call this directly.
|
|
1295
|
-
*/
|
|
1296
|
-
emitSubmitSuccess(): void;
|
|
1297
|
-
/**
|
|
1298
|
-
* Register a teardown function whose lifetime is bound to the
|
|
1299
|
-
* FormStore itself (not a consumer's Vue effect scope). Called by
|
|
1300
|
-
* `dispose()` when the last consumer unmounts. Used by persistence /
|
|
1301
|
-
* history wiring so their subscribers aren't detached prematurely
|
|
1302
|
-
* when only the first consumer unmounts but others remain.
|
|
1303
|
-
*/
|
|
1304
|
-
registerCleanup(fn: () => void): void;
|
|
1305
|
-
/**
|
|
1306
|
-
* Register an async drain function. Called by the registry before
|
|
1307
|
-
* `dispose()` so async background work — chiefly the persistence
|
|
1308
|
-
* layer's debounced storage writes — has a chance to settle without
|
|
1309
|
-
* losing the last keystroke. Each registered function is awaited in
|
|
1310
|
-
* parallel; failures are swallowed to keep eviction reliable.
|
|
1234
|
+
* Unknown keys in the seed object dev-warn so a stale resume payload
|
|
1235
|
+
* surfaces at construction.
|
|
1311
1236
|
*/
|
|
1312
|
-
|
|
1237
|
+
readonly defaultStatuses?: Record<string, FormStatus> | (() => Record<string, FormStatus>) | (() => Promise<Record<string, FormStatus>>);
|
|
1313
1238
|
/**
|
|
1314
|
-
*
|
|
1315
|
-
*
|
|
1316
|
-
*
|
|
1239
|
+
* Optional progress override. When omitted, the wizard exposes
|
|
1240
|
+
* `progress` as `valid_step_count / count` (normalised to `[0, 1]`).
|
|
1241
|
+
* When provided, the returned number is used as-is — the consumer is
|
|
1242
|
+
* responsible for any normalisation.
|
|
1243
|
+
*
|
|
1244
|
+
* The override is invoked inside a Vue `computed` so it must be
|
|
1245
|
+
* synchronous and may only read reactive sources.
|
|
1317
1246
|
*/
|
|
1318
|
-
|
|
1247
|
+
readonly progress?: (steps: ReadonlyArray<CompiledStep>) => number;
|
|
1319
1248
|
/**
|
|
1320
|
-
*
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1323
|
-
*
|
|
1324
|
-
*
|
|
1249
|
+
* When `wizard.handleSubmit` finds errors, automatically focus the
|
|
1250
|
+
* first failing form: jump to its step and invoke its
|
|
1251
|
+
* `applyInvalidSubmitPolicy()` (focus / scroll per the form's own
|
|
1252
|
+
* `onInvalidSubmit` configuration). Default `true`; pass `false` to
|
|
1253
|
+
* keep the active step where the user left it and handle navigation
|
|
1254
|
+
* manually in the `onError` callback.
|
|
1325
1255
|
*/
|
|
1326
|
-
readonly
|
|
1256
|
+
readonly focusFirstError?: boolean;
|
|
1327
1257
|
/**
|
|
1328
|
-
*
|
|
1329
|
-
*
|
|
1330
|
-
*
|
|
1331
|
-
*
|
|
1332
|
-
*
|
|
1333
|
-
* `RegisterValue.coerce`.
|
|
1258
|
+
* Source of truth for the active step. Invoked at construction and
|
|
1259
|
+
* re-evaluated reactively via `watchEffect`. Default callback reads
|
|
1260
|
+
* `?step=<key>` from the URL via `wizard-history.ts`; pass `false`
|
|
1261
|
+
* to disable URL sync, or provide a custom callback for non-router
|
|
1262
|
+
* persistence (localStorage, broadcast channel, etc.).
|
|
1334
1263
|
*/
|
|
1335
|
-
readonly
|
|
1264
|
+
readonly restore?: WizardRestoreFn | false;
|
|
1336
1265
|
/**
|
|
1337
|
-
*
|
|
1338
|
-
*
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1266
|
+
* Destination for the active step. Invoked whenever `currentStep`
|
|
1267
|
+
* changes, with a diff check to break the restore-persist loop.
|
|
1268
|
+
* Default callback writes `?step=<key>` via `wizard-history.ts`;
|
|
1269
|
+
* pass `false` to disable persistence, or provide a custom callback
|
|
1270
|
+
* to scope the param name or write to alternate storage.
|
|
1341
1271
|
*/
|
|
1342
|
-
|
|
1272
|
+
readonly persist?: WizardPersistFn | false;
|
|
1273
|
+
};
|
|
1274
|
+
/**
|
|
1275
|
+
* True when a single slot is guaranteed to contribute exactly one
|
|
1276
|
+
* compiled step: a bare form or affordance string that is neither
|
|
1277
|
+
* nullish nor a (maybe-absent) function / `lazy()` slot. One of these
|
|
1278
|
+
* anywhere in the tuple proves the compiled list is non-empty.
|
|
1279
|
+
*/
|
|
1280
|
+
type IsGuaranteedStep<T> = [null] extends [T] ? false : [undefined] extends [T] ? false : T extends LazyMarker | ((...args: unknown[]) => unknown) ? false : T extends string ? true : T extends {
|
|
1281
|
+
readonly key: string;
|
|
1282
|
+
} ? true : false;
|
|
1283
|
+
/**
|
|
1284
|
+
* Predicate: is the steps tuple statically guaranteed to compile to a
|
|
1285
|
+
* non-empty list? True when at least one element is a guaranteed step
|
|
1286
|
+
* (see `IsGuaranteedStep`). Function / `lazy()` slots may resolve to
|
|
1287
|
+
* nothing, and `null` / `undefined` (a literal absence or a
|
|
1288
|
+
* `cond ? form : null` union) is an explicit drop, so none of those
|
|
1289
|
+
* prove non-emptiness; a tuple of only maybe-absent slots stays
|
|
1290
|
+
* honestly `| undefined`.
|
|
1291
|
+
*
|
|
1292
|
+
* Used to narrow `currentStep` / `activeForm` to their non-`undefined`
|
|
1293
|
+
* shapes in the common-case wizard, while keeping the honest union
|
|
1294
|
+
* everywhere a runtime drop is reachable.
|
|
1295
|
+
*/
|
|
1296
|
+
type StaticallyNonEmpty<S> = S extends readonly [infer First, ...infer Rest] ? IsGuaranteedStep<First> extends true ? true : StaticallyNonEmpty<Rest> : false;
|
|
1297
|
+
/** Active step's key, narrowed to `string` when `S` is statically safe. */
|
|
1298
|
+
type CurrentStepOf<S> = StaticallyNonEmpty<S> extends true ? FormKey : FormKey | undefined;
|
|
1299
|
+
/**
|
|
1300
|
+
* Active step's form handle, schema-erased to `UseFormReturnType<GenericForm>`
|
|
1301
|
+
* and narrowed to non-`undefined` when `S` is statically safe. The runtime
|
|
1302
|
+
* value is a live facade over the active step, so reads (`.values`, `.meta`,
|
|
1303
|
+
* `.history`) and `.handleSubmit` always target the current step. Values are
|
|
1304
|
+
* loose here because the active step's schema is not statically known; reach
|
|
1305
|
+
* for the original form ref or `ctx.get(ref)` when you need typed per-step
|
|
1306
|
+
* values.
|
|
1307
|
+
*/
|
|
1308
|
+
type ActiveFormOf<S> = StaticallyNonEmpty<S> extends true ? UseFormReturnType<GenericForm> : UseFormReturnType<GenericForm> | undefined;
|
|
1309
|
+
/**
|
|
1310
|
+
* Per-slot contribution to {@link FormsRecordOf}. Strips a `null` /
|
|
1311
|
+
* `undefined` (a literal absence or a `cond ? form : null` union) off the
|
|
1312
|
+
* slot first, so a conditionally-present form still maps to its key and
|
|
1313
|
+
* stays concretely typed on `wizard.forms`; a purely nullish slot
|
|
1314
|
+
* contributes nothing.
|
|
1315
|
+
*/
|
|
1316
|
+
type SlotRecord<First, F = Exclude<First, null | undefined>> = [F] extends [never] ? unknown : F extends string ? {
|
|
1317
|
+
readonly [P in F]: AnyForm;
|
|
1318
|
+
} : F extends {
|
|
1319
|
+
readonly key: infer K extends string;
|
|
1320
|
+
} ? {
|
|
1321
|
+
readonly [P in K]: F;
|
|
1322
|
+
} : unknown;
|
|
1323
|
+
/**
|
|
1324
|
+
* Recursive tuple walk that builds the static portion of
|
|
1325
|
+
* `wizard.forms`. Each step slot contributes to the record:
|
|
1326
|
+
*
|
|
1327
|
+
* - **String slot** (`'review'`): the literal becomes the record key
|
|
1328
|
+
* and the value is `AnyForm` (the noop form synthesized for the
|
|
1329
|
+
* affordance position is opaque at the type level).
|
|
1330
|
+
* - **Form slot** (a `useForm` reference with a literal `key` field):
|
|
1331
|
+
* the form's own `key` becomes the record key, and the value is
|
|
1332
|
+
* the concrete form handle type — so drilling
|
|
1333
|
+
* `wizard.forms.shipping.values.address` carries the schema-derived
|
|
1334
|
+
* field types through. A form kept behind a `cond ? form : null`
|
|
1335
|
+
* conditional still maps to its key.
|
|
1336
|
+
* - **`null` / `undefined` slot**: contributes nothing.
|
|
1337
|
+
* - **Function / `lazy()` slot**: contributes nothing to the static
|
|
1338
|
+
* map. Runtime-resolved forms are still reachable via the
|
|
1339
|
+
* catch-all index signature on `WizardForms` (typed as `AnyForm`).
|
|
1340
|
+
*
|
|
1341
|
+
* Recursion is bounded by the tuple length; real-world wizards land
|
|
1342
|
+
* well below the TS instantiation budget.
|
|
1343
|
+
*/
|
|
1344
|
+
type FormsRecordOf<S> = S extends readonly [
|
|
1345
|
+
infer First,
|
|
1346
|
+
...infer Rest extends ReadonlyArray<StepSlot>
|
|
1347
|
+
] ? SlotRecord<First> & FormsRecordOf<Rest> : unknown;
|
|
1348
|
+
/**
|
|
1349
|
+
* `wizard.forms` typed view. Combines the static per-step type map
|
|
1350
|
+
* with a catch-all `Record<FormKey, AnyForm>` fallback so:
|
|
1351
|
+
*
|
|
1352
|
+
* - Statically known slot keys → concrete form type via `FormsRecordOf`
|
|
1353
|
+
* - Any other string key → `AnyForm` via the index signature
|
|
1354
|
+
*
|
|
1355
|
+
* The intersection collapses to the concrete form for statically
|
|
1356
|
+
* known keys (because the concrete form type extends `AnyForm`) and
|
|
1357
|
+
* to `AnyForm` for unknown keys.
|
|
1358
|
+
*/
|
|
1359
|
+
type WizardForms<S> = FormsRecordOf<S> & Readonly<Record<FormKey, AnyForm>>;
|
|
1360
|
+
/**
|
|
1361
|
+
* Return shape of `useWizard({ steps, … })`. Every reactive read is a
|
|
1362
|
+
* plain getter (no `.value`) — `wizard.currentStep`, `wizard.progress`,
|
|
1363
|
+
* `wizard.allValues` track inside `computed` / template effects
|
|
1364
|
+
* directly.
|
|
1365
|
+
*
|
|
1366
|
+
* Parameterized by the steps tuple `S` so active-position fields
|
|
1367
|
+
* (`currentStep`, `activeForm`) narrow to non-undefined for the common
|
|
1368
|
+
* case (all positional Form / string slots) and stay as honest unions
|
|
1369
|
+
* when a function or `lazy()` slot can drop the compiled position at
|
|
1370
|
+
* runtime. The `const` type parameter on `useWizard` preserves literal
|
|
1371
|
+
* tuple types without consumer-side `as const`, so the narrowing
|
|
1372
|
+
* happens automatically from the call site.
|
|
1373
|
+
*
|
|
1374
|
+
* - `currentStep` — key of the active step. Narrows to `string` when
|
|
1375
|
+
* the steps tuple is statically guaranteed to
|
|
1376
|
+
* compile to a non-empty list (all positional
|
|
1377
|
+
* Form / string slots, no function or `lazy()`
|
|
1378
|
+
* slots). Otherwise reads as `string | undefined`
|
|
1379
|
+
* so the degenerate case (empty list at runtime)
|
|
1380
|
+
* surfaces honestly.
|
|
1381
|
+
* - `activeForm` — a LIVE view of the active step's form. Operating
|
|
1382
|
+
* through it always targets the current step, so a
|
|
1383
|
+
* handler captured once at setup
|
|
1384
|
+
* (`wizard.activeForm.handleSubmit(() =>
|
|
1385
|
+
* wizard.next())`) retargets as the wizard advances.
|
|
1386
|
+
* No longer `===` the raw per-step handle; reach for
|
|
1387
|
+
* `wizard.forms[key]` when you need raw identity.
|
|
1388
|
+
* Same `undefined` narrowing as `currentStep`. Noop
|
|
1389
|
+
* forms cover string slots in the normal path.
|
|
1390
|
+
* - `activeIndex` — 0-based position of the active step.
|
|
1391
|
+
* - `isFinalStep` — `true` when `currentStep === steps[count - 1].key`.
|
|
1392
|
+
* - `steps` — ordered list of compiled `{ key, form }` slots.
|
|
1393
|
+
* - `forms` — record indexable by step key; the value is the
|
|
1394
|
+
* full form handle resolved for that slot.
|
|
1395
|
+
* - `count` — `steps.length`.
|
|
1396
|
+
* - `statuses` — callable readonly proxy over the per-key
|
|
1397
|
+
* `FormStatus` record. Noop-form keys always read
|
|
1398
|
+
* as default-valid.
|
|
1399
|
+
* - `allValues` — namespaced aggregate of each form's values, keyed
|
|
1400
|
+
* by step key.
|
|
1401
|
+
* - `allErrors` — namespaced aggregate of each form's validation
|
|
1402
|
+
* errors, keyed by step key. Noop forms map to an
|
|
1403
|
+
* empty list.
|
|
1404
|
+
* - `progress` — normalised step-validity ratio (or the consumer's
|
|
1405
|
+
* `progress` override). Forward-looking: noops count
|
|
1406
|
+
* as always-valid.
|
|
1407
|
+
* - `canAdvance` — `true` when `activeIndex < count - 1`. Pure
|
|
1408
|
+
* positional check; navigation never gates on
|
|
1409
|
+
* validity.
|
|
1410
|
+
* - `canGoBack` — `true` when `activeIndex > 0`.
|
|
1411
|
+
* - `complete` — `isFinalStep && every step's form is valid`.
|
|
1412
|
+
* Forward-looking; reactive to current form
|
|
1413
|
+
* validity. Gates "Finish button enable" style UI.
|
|
1414
|
+
* - `done` — monotonic latch: flips `true` the first time a
|
|
1415
|
+
* `handleSubmit` resolves without throwing AND leaves
|
|
1416
|
+
* no errors set on any step, and stays `true` through
|
|
1417
|
+
* subsequent edits or invalidations. A callback that
|
|
1418
|
+
* calls `setErrors` and returns (the documented
|
|
1419
|
+
* server-rejection path) is a failed submit, so it
|
|
1420
|
+
* does not flip `done`. Only `reset()` flips it back.
|
|
1421
|
+
* Gates "show success card" style UI that should
|
|
1422
|
+
* reflect submission history rather than current
|
|
1423
|
+
* validity.
|
|
1424
|
+
* - `submitting` — `true` while a `wizard.handleSubmit` call is in
|
|
1425
|
+
* flight. Global re-entrance guard: every
|
|
1426
|
+
* navigation method also refuses while this is on.
|
|
1427
|
+
* - `submissionAttempts` — count of `wizard.handleSubmit` invocations
|
|
1428
|
+
* (success or failure). Always bumps, including on
|
|
1429
|
+
* noop-form steps.
|
|
1430
|
+
* - `submitError` — the error THROWN by the most recent
|
|
1431
|
+
* `wizard.handleSubmit` callback (or its `onError`),
|
|
1432
|
+
* coerced to a real `Error`. Mirrors
|
|
1433
|
+
* `form.meta.submitError`: this is the unexpected-throw
|
|
1434
|
+
* channel, so an expected rejection handled via
|
|
1435
|
+
* `setErrors` (no throw) surfaces through the error
|
|
1436
|
+
* surface and `onError` instead, leaving this `null`.
|
|
1437
|
+
* Cleared at submit entry and by `reset()`, parked here
|
|
1438
|
+
* rather than re-thrown, so the handler resolves and
|
|
1439
|
+
* never manufactures a `window` unhandledrejection.
|
|
1440
|
+
* `null` on success.
|
|
1441
|
+
* - `visited` — append-only breadcrumb of navigated step keys.
|
|
1442
|
+
* `back()` does not pop; the trail is the audit
|
|
1443
|
+
* log, not the back-stack.
|
|
1444
|
+
* - `next/back/goTo` — pure navigation. Refuses while `submitting`.
|
|
1445
|
+
* - `tryNext()` — validate the active step, then advance iff it
|
|
1446
|
+
* passed; invalid input keeps the pin put under the
|
|
1447
|
+
* form's standard error reveal (first error focused,
|
|
1448
|
+
* display state advanced). The inline-bindable
|
|
1449
|
+
* shorthand for `activeForm.handleSubmit(() =>
|
|
1450
|
+
* next())`. Resolves to whether the pin moved. No-ops
|
|
1451
|
+
* to `false` on a degenerate or final-step wizard
|
|
1452
|
+
* (finish via `handleSubmit`).
|
|
1453
|
+
* - `handleSubmit(onSubmit, onError?)` — always validates the entire
|
|
1454
|
+
* step list, from any step, and never advances the
|
|
1455
|
+
* pin: on success it latches `done`; on any error it
|
|
1456
|
+
* focuses the first failing step and fires `onError`
|
|
1457
|
+
* with errors spanning every step. To gate advancing
|
|
1458
|
+
* a step on its own validity, compose with the active
|
|
1459
|
+
* form's submit: `activeForm.handleSubmit(() =>
|
|
1460
|
+
* wizard.next())`. Returns an event handler suitable
|
|
1461
|
+
* for `<form @submit>` or imperative use.
|
|
1462
|
+
* - `reset()` — zeros wizard lifecycle (`submissionAttempts`,
|
|
1463
|
+
* `visited`), resets every form, returns
|
|
1464
|
+
* `currentStep` to `steps[0].key`, and invokes
|
|
1465
|
+
* `persist` with the cleared state.
|
|
1466
|
+
*/
|
|
1467
|
+
type UseWizardReturnType<S extends ReadonlyArray<StepSlot> = ReadonlyArray<StepSlot>> = {
|
|
1468
|
+
readonly key: string;
|
|
1469
|
+
readonly currentStep: CurrentStepOf<S>;
|
|
1470
|
+
readonly activeForm: ActiveFormOf<S>;
|
|
1471
|
+
readonly activeIndex: number;
|
|
1472
|
+
readonly isFinalStep: boolean;
|
|
1473
|
+
readonly steps: ReadonlyArray<CompiledStep>;
|
|
1474
|
+
readonly forms: WizardForms<S>;
|
|
1475
|
+
readonly count: number;
|
|
1476
|
+
readonly statuses: WizardStatusesProxy<Record<string, FormStatus>>;
|
|
1477
|
+
readonly allValues: Readonly<Record<FormKey, unknown>>;
|
|
1478
|
+
readonly allErrors: Readonly<Record<FormKey, readonly WizardAggregateError[]>>;
|
|
1479
|
+
readonly progress: number;
|
|
1480
|
+
readonly canAdvance: boolean;
|
|
1481
|
+
readonly canGoBack: boolean;
|
|
1482
|
+
readonly complete: boolean;
|
|
1483
|
+
readonly done: boolean;
|
|
1484
|
+
readonly submitting: boolean;
|
|
1485
|
+
readonly submissionAttempts: number;
|
|
1486
|
+
readonly submitError: Error | null;
|
|
1487
|
+
readonly visited: readonly FormKey[];
|
|
1488
|
+
readonly next: () => Promise<void>;
|
|
1489
|
+
readonly back: () => void;
|
|
1490
|
+
readonly goTo: (key: string) => void;
|
|
1491
|
+
readonly tryNext: () => Promise<boolean>;
|
|
1492
|
+
readonly handleSubmit: (onSubmit: WizardOnSubmit, onError?: WizardOnError) => (event?: Event) => Promise<void>;
|
|
1493
|
+
readonly reset: () => void;
|
|
1343
1494
|
};
|
|
1344
1495
|
|
|
1345
1496
|
/**
|
|
@@ -1482,220 +1633,69 @@ type AttaformRegistry = {
|
|
|
1482
1633
|
* is enqueued AND not skipped.
|
|
1483
1634
|
* @internal
|
|
1484
1635
|
*/
|
|
1485
|
-
readonly shouldPrefetch: (key: FormKey) => boolean;
|
|
1486
|
-
/**
|
|
1487
|
-
* Wait for all pending persistence writes across every live form
|
|
1488
|
-
* to settle. Useful for SSR shutdown and integration tests that
|
|
1489
|
-
* need a deterministic teardown.
|
|
1490
|
-
* @internal
|
|
1491
|
-
*/
|
|
1492
|
-
readonly shutdown: () => Promise<void>;
|
|
1493
|
-
};
|
|
1494
|
-
/**
|
|
1495
|
-
* The Vue `InjectionKey` under which the registry is provided on the
|
|
1496
|
-
* app. Most consumers never need this — `useForm` and
|
|
1497
|
-
* `injectForm` resolve the registry automatically.
|
|
1498
|
-
*/
|
|
1499
|
-
declare const kAttaformRegistry: InjectionKey<AttaformRegistry>;
|
|
1500
|
-
declare module 'vue' {
|
|
1501
|
-
interface App {
|
|
1502
|
-
/** @internal */
|
|
1503
|
-
_attaform?: AttaformRegistry;
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
/** Options for `createRegistry`. */
|
|
1507
|
-
type CreateRegistryOptions = SSRDetectOptions & {
|
|
1508
|
-
/**
|
|
1509
|
-
* App-level defaults applied to every `useForm` call. Per-form
|
|
1510
|
-
* options always win. Omitted is equivalent to `{}`.
|
|
1511
|
-
*/
|
|
1512
|
-
defaults?: AttaformDefaults;
|
|
1513
|
-
};
|
|
1514
|
-
/**
|
|
1515
|
-
* Create a fresh `AttaformRegistry`. `createAttaform()` calls
|
|
1516
|
-
* this internally — most consumers never need to call it directly.
|
|
1517
|
-
* Use it when building a custom plugin that doesn't want the
|
|
1518
|
-
* `createAttaform` plugin's auto-install behaviour (e.g. test
|
|
1519
|
-
* harnesses, embedded apps).
|
|
1520
|
-
*/
|
|
1521
|
-
declare function createRegistry(options?: CreateRegistryOptions): AttaformRegistry;
|
|
1522
|
-
/**
|
|
1523
|
-
* Look up the current app's registry from inside a component's
|
|
1524
|
-
* `setup()` (or any synchronous code on the setup call stack).
|
|
1525
|
-
*
|
|
1526
|
-
* Most consumers don't need this — `useForm` and `injectForm`
|
|
1527
|
-
* call it on your behalf. Reach for it directly when building
|
|
1528
|
-
* custom integrations that need the raw registry.
|
|
1529
|
-
*
|
|
1530
|
-
* Throws:
|
|
1531
|
-
* - `OutsideSetupError` when called outside a Vue setup context
|
|
1532
|
-
* (e.g. from an event handler or async callback). Move the call
|
|
1533
|
-
* into setup, or trigger it from a child component.
|
|
1534
|
-
* - `RegistryNotInstalledError` when called inside setup but the
|
|
1535
|
-
* plugin wasn't installed. Add
|
|
1536
|
-
* `app.use(createAttaform())` to your app entry.
|
|
1537
|
-
*/
|
|
1538
|
-
declare function useRegistry(): AttaformRegistry;
|
|
1539
|
-
/**
|
|
1540
|
-
* Look up a Vue app's registry by `App` reference. Used by
|
|
1541
|
-
* SSR helpers (`renderAttaformState`, `hydrateAttaformState`) that
|
|
1542
|
-
* run outside a component setup context.
|
|
1543
|
-
*
|
|
1544
|
-
* Throws `RegistryNotInstalledError` when the app hasn't been wired
|
|
1545
|
-
* with `createAttaform()`.
|
|
1546
|
-
*/
|
|
1547
|
-
declare function getRegistryFromApp(app: App): AttaformRegistry;
|
|
1548
|
-
|
|
1549
|
-
/**
|
|
1550
|
-
* Options accepted by `injectForm` when passing an object instead of
|
|
1551
|
-
* a bare key string. `__ssrAccessed: true` is set by the Phase 3
|
|
1552
|
-
* `attaform-vite` transform on descendant calls whose template reads
|
|
1553
|
-
* the injected form's reactive state — it tells the runtime to
|
|
1554
|
-
* enqueue the form for SSR prefetch and register the descendant's
|
|
1555
|
-
* `onServerPrefetch` hook. Consumers may set it manually as the
|
|
1556
|
-
* escape hatch when the transform isn't installed or doesn't see
|
|
1557
|
-
* the reference.
|
|
1558
|
-
*/
|
|
1559
|
-
type InjectFormInput = {
|
|
1560
|
-
readonly key?: FormKey;
|
|
1561
|
-
/**
|
|
1562
|
-
* Set by the Vite transform when this `injectForm` call site sits in
|
|
1563
|
-
* a component whose template / script reads the form's reactive
|
|
1564
|
-
* state. On the server, this enqueues the form for SSR prefetch and
|
|
1565
|
-
* wires `onServerPrefetch` so the descendant awaits the activation
|
|
1566
|
-
* promise before its render emits HTML.
|
|
1567
|
-
*
|
|
1568
|
-
* @internal Transform-emitted. Manual use is the documented escape
|
|
1569
|
-
* hatch when the transform can't reach the reference (dynamic
|
|
1570
|
-
* property access, untransformed bundlers).
|
|
1571
|
-
*/
|
|
1572
|
-
readonly __ssrAccessed?: boolean;
|
|
1573
|
-
};
|
|
1574
|
-
/**
|
|
1575
|
-
* Access an existing form from a descendant component without passing
|
|
1576
|
-
* it through props. Counterpart to `useForm` — `useForm` creates and
|
|
1577
|
-
* provides; `injectForm` looks up via Vue's inject mechanism.
|
|
1578
|
-
*
|
|
1579
|
-
* Three ways to call it:
|
|
1580
|
-
*
|
|
1581
|
-
* ```ts
|
|
1582
|
-
* // Reach the nearest ancestor's anonymous useForm() call.
|
|
1583
|
-
* const form = injectForm<SignupShape>()
|
|
1584
|
-
*
|
|
1585
|
-
* // Reach a specific form by its key — works from anywhere in the app.
|
|
1586
|
-
* const cart = injectForm<CartShape>('cart')
|
|
1587
|
-
*
|
|
1588
|
-
* // Options form. The Vite transform emits this with `__ssrAccessed: true`
|
|
1589
|
-
* // when the descendant's template / script reads the form's reactive
|
|
1590
|
-
* // state, so the descendant participates in SSR prefetch coordination.
|
|
1591
|
-
* const cart = injectForm<CartShape>({ key: 'cart', __ssrAccessed: true })
|
|
1592
|
-
* ```
|
|
1593
|
-
*
|
|
1594
|
-
* Resolution rules (no-key form):
|
|
1595
|
-
* - Closest ambient ancestor wins.
|
|
1596
|
-
* - Only anonymous `useForm()` (no `key`) fills the ambient slot;
|
|
1597
|
-
* keyed forms are reachable only via `injectForm(key)`.
|
|
1598
|
-
* - Inherits the resolved ancestor's `formInstanceId`.
|
|
1599
|
-
*
|
|
1600
|
-
* Resolution rules (keyed form): registry lookup by string key,
|
|
1601
|
-
* independent of component-tree position.
|
|
1602
|
-
*
|
|
1603
|
-
* Returns `null` when no matching form exists (no ambient ancestor, or
|
|
1604
|
-
* the named key isn't registered yet). A dev-mode warning points at the
|
|
1605
|
-
* call site, lists the registered keys, and flags the mount-timing case
|
|
1606
|
-
* (a form created by a child or sibling isn't registered until its own
|
|
1607
|
-
* setup runs). Always narrow before using:
|
|
1608
|
-
*
|
|
1609
|
-
* ```ts
|
|
1610
|
-
* const form = injectForm<Shape>('signup')
|
|
1611
|
-
* if (!form) return
|
|
1612
|
-
* form.register('email')
|
|
1613
|
-
* ```
|
|
1614
|
-
*
|
|
1615
|
-
* Pass the `Form` generic explicitly — Vue's provide/inject erases
|
|
1616
|
-
* generics, so the library can't recover the shape automatically.
|
|
1617
|
-
*
|
|
1618
|
-
* The form is kept alive for this component's lifetime; once every
|
|
1619
|
-
* consumer unmounts, the form is cleaned up automatically.
|
|
1636
|
+
readonly shouldPrefetch: (key: FormKey) => boolean;
|
|
1637
|
+
/**
|
|
1638
|
+
* Wait for all pending persistence writes across every live form
|
|
1639
|
+
* to settle. Useful for SSR shutdown and integration tests that
|
|
1640
|
+
* need a deterministic teardown.
|
|
1641
|
+
* @internal
|
|
1642
|
+
*/
|
|
1643
|
+
readonly shutdown: () => Promise<void>;
|
|
1644
|
+
};
|
|
1645
|
+
/**
|
|
1646
|
+
* The Vue `InjectionKey` under which the registry is provided on the
|
|
1647
|
+
* app. Most consumers never need this — `useForm` and
|
|
1648
|
+
* `injectForm` resolve the registry automatically.
|
|
1620
1649
|
*/
|
|
1621
|
-
declare
|
|
1622
|
-
|
|
1650
|
+
declare const kAttaformRegistry: InjectionKey<AttaformRegistry>;
|
|
1651
|
+
declare module 'vue' {
|
|
1652
|
+
interface App {
|
|
1653
|
+
/** @internal */
|
|
1654
|
+
_attaform?: AttaformRegistry;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
/** Options for `createRegistry`. */
|
|
1658
|
+
type CreateRegistryOptions = SSRDetectOptions & {
|
|
1659
|
+
/**
|
|
1660
|
+
* App-level defaults applied to every `useForm` call. Per-form
|
|
1661
|
+
* options always win. Omitted is equivalent to `{}`.
|
|
1662
|
+
*/
|
|
1663
|
+
defaults?: AttaformDefaults;
|
|
1664
|
+
};
|
|
1623
1665
|
/**
|
|
1624
|
-
*
|
|
1625
|
-
*
|
|
1626
|
-
*
|
|
1627
|
-
*
|
|
1628
|
-
*
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
*
|
|
1633
|
-
*
|
|
1634
|
-
* <script setup lang="ts">
|
|
1635
|
-
* import { useRegister } from 'attaform'
|
|
1636
|
-
* const rv = useRegister()
|
|
1637
|
-
* // rv.path / rv.segments / rv.formKey / rv.formInstanceId / rv.innerRef
|
|
1638
|
-
* // are all reachable directly — no `.value` unwrap.
|
|
1639
|
-
* </script>
|
|
1640
|
-
*
|
|
1641
|
-
* <template>
|
|
1642
|
-
* <label class="field">
|
|
1643
|
-
* <span>Email</span>
|
|
1644
|
-
* <input v-register="rv" />
|
|
1645
|
-
* </label>
|
|
1646
|
-
* </template>
|
|
1647
|
-
* ```
|
|
1648
|
-
*
|
|
1649
|
-
* Returns a hybrid Proxy: it answers `__v_isRef` / `.value` like a
|
|
1650
|
-
* Vue `Ref<RegisterValue | undefined>` (so templates auto-unwrap
|
|
1651
|
-
* correctly and `v-register="rv"` feeds the underlying RV to the
|
|
1652
|
-
* directive — preserving the directive's path-migration diff across
|
|
1653
|
-
* renders), AND every other property read pierces to the captured
|
|
1654
|
-
* RV's field (so `rv.path` works directly in script setup). Reads
|
|
1655
|
-
* inside reactive scopes (`computed` / `watchEffect`) track the
|
|
1656
|
-
* underlying `shallowRef`, so `rv.path` re-runs when the parent
|
|
1657
|
-
* rebinds to a different path.
|
|
1658
|
-
*
|
|
1659
|
-
* Unbound state: when the parent didn't pass `v-register`, every
|
|
1660
|
-
* piercing read returns `undefined` at runtime, and the return type
|
|
1661
|
-
* surfaces this honestly as `UseRegisterReturn<V> | undefined`.
|
|
1662
|
-
* Consumers defend with optional chaining (`rv?.formKey`,
|
|
1663
|
-
* `rv?.segments`); the directive accepts `undefined` peacefully (its
|
|
1664
|
-
* binding value type is already `RegisterValue<V> | undefined`), so
|
|
1665
|
-
* `v-register="rv"` works whether or not a parent has bound. The
|
|
1666
|
-
* composable's `onMounted` warn fires once per instance to surface
|
|
1667
|
-
* the misuse case at runtime.
|
|
1666
|
+
* Create a fresh `AttaformRegistry`. `createAttaform()` calls
|
|
1667
|
+
* this internally — most consumers never need to call it directly.
|
|
1668
|
+
* Use it when building a custom plugin that doesn't want the
|
|
1669
|
+
* `createAttaform` plugin's auto-install behaviour (e.g. test
|
|
1670
|
+
* harnesses, embedded apps).
|
|
1671
|
+
*/
|
|
1672
|
+
declare function createRegistry(options?: CreateRegistryOptions): AttaformRegistry;
|
|
1673
|
+
/**
|
|
1674
|
+
* Look up the current app's registry from inside a component's
|
|
1675
|
+
* `setup()` (or any synchronous code on the setup call stack).
|
|
1668
1676
|
*
|
|
1669
|
-
*
|
|
1670
|
-
*
|
|
1671
|
-
*
|
|
1672
|
-
* binding is conclusive misuse. The warn does NOT fire on every read
|
|
1673
|
-
* of the proxy, and is intentionally silent under SSR
|
|
1674
|
-
* (`renderToString` skips `onMounted`); the CSR hydration pass
|
|
1675
|
-
* surfaces the same diagnostic without double-counting through Nuxt's
|
|
1676
|
-
* `dev:ssr-logs` channel.
|
|
1677
|
+
* Most consumers don't need this — `useForm` and `injectForm`
|
|
1678
|
+
* call it on your behalf. Reach for it directly when building
|
|
1679
|
+
* custom integrations that need the raw registry.
|
|
1677
1680
|
*
|
|
1678
|
-
*
|
|
1679
|
-
*
|
|
1680
|
-
*
|
|
1681
|
-
*
|
|
1681
|
+
* Throws:
|
|
1682
|
+
* - `OutsideSetupError` when called outside a Vue setup context
|
|
1683
|
+
* (e.g. from an event handler or async callback). Move the call
|
|
1684
|
+
* into setup, or trigger it from a child component.
|
|
1685
|
+
* - `RegistryNotInstalledError` when called inside setup but the
|
|
1686
|
+
* plugin wasn't installed. Add
|
|
1687
|
+
* `app.use(createAttaform())` to your app entry.
|
|
1682
1688
|
*/
|
|
1683
|
-
|
|
1689
|
+
declare function useRegistry(): AttaformRegistry;
|
|
1684
1690
|
/**
|
|
1685
|
-
*
|
|
1686
|
-
*
|
|
1687
|
-
*
|
|
1688
|
-
* template auto-unwrap surfaces the underlying RV to `v-register`
|
|
1689
|
-
* and the directive's path-migration diff sees the real RV across
|
|
1690
|
-
* renders).
|
|
1691
|
+
* Look up a Vue app's registry by `App` reference. Used by
|
|
1692
|
+
* SSR helpers (`renderAttaformState`, `hydrateAttaformState`) that
|
|
1693
|
+
* run outside a component setup context.
|
|
1691
1694
|
*
|
|
1692
|
-
*
|
|
1693
|
-
*
|
|
1694
|
-
* the hybrid's only `.value`. Older code that read `rv.value?.path`
|
|
1695
|
-
* keeps working; new code can write `rv.path` directly.
|
|
1695
|
+
* Throws `RegistryNotInstalledError` when the app hasn't been wired
|
|
1696
|
+
* with `createAttaform()`.
|
|
1696
1697
|
*/
|
|
1697
|
-
|
|
1698
|
-
declare function useRegister<V = unknown>(): UseRegisterReturn<V> | undefined;
|
|
1698
|
+
declare function getRegistryFromApp(app: App): AttaformRegistry;
|
|
1699
1699
|
|
|
1700
1700
|
/**
|
|
1701
1701
|
* Multistep-form orchestrator built around an ordered list of step slots.
|
|
@@ -1894,5 +1894,392 @@ declare const AttaformErrorCode: {
|
|
|
1894
1894
|
};
|
|
1895
1895
|
type AttaformErrorCode = (typeof AttaformErrorCode)[keyof typeof AttaformErrorCode];
|
|
1896
1896
|
|
|
1897
|
-
|
|
1898
|
-
|
|
1897
|
+
/**
|
|
1898
|
+
* Options for `createAttaform()`.
|
|
1899
|
+
*/
|
|
1900
|
+
type AttaformPluginOptions = SSRDetectOptions & {
|
|
1901
|
+
/**
|
|
1902
|
+
* Whether to install the Vue DevTools integration. Default `true`.
|
|
1903
|
+
* The integration is dev-only: the `import('./devtools')` sits behind
|
|
1904
|
+
* the `__DEV__` flag, so a consumer's production build folds it out
|
|
1905
|
+
* entirely (no devtools chunk shipped, no fetch attempted). In
|
|
1906
|
+
* development the DevTools peer is loaded lazily and a missing peer
|
|
1907
|
+
* fails silently. Pass `false` to skip the integration even in dev.
|
|
1908
|
+
*/
|
|
1909
|
+
devtools?: boolean;
|
|
1910
|
+
/**
|
|
1911
|
+
* App-level defaults applied to every `useForm` call in this app.
|
|
1912
|
+
* Per-form options always win. See `AttaformDefaults` for
|
|
1913
|
+
* the supported option set and the merge rules.
|
|
1914
|
+
*
|
|
1915
|
+
* ```ts
|
|
1916
|
+
* app.use(
|
|
1917
|
+
* createAttaform({
|
|
1918
|
+
* defaults: { debounceMs: 100 },
|
|
1919
|
+
* })
|
|
1920
|
+
* )
|
|
1921
|
+
* ```
|
|
1922
|
+
*/
|
|
1923
|
+
defaults?: AttaformDefaults;
|
|
1924
|
+
};
|
|
1925
|
+
/**
|
|
1926
|
+
* Create the Vue plugin that installs the form library on a Vue
|
|
1927
|
+
* application. Required only when you want app-wide options
|
|
1928
|
+
* (`defaults`, `devtools: false`, `ssr: true`) — for the default
|
|
1929
|
+
* setup, `useForm` / `injectForm` / `useRegister` lazy-install the
|
|
1930
|
+
* registry on first use.
|
|
1931
|
+
*
|
|
1932
|
+
* ```ts
|
|
1933
|
+
* import { createApp } from 'vue'
|
|
1934
|
+
* import { createAttaform } from 'attaform'
|
|
1935
|
+
*
|
|
1936
|
+
* createApp(App)
|
|
1937
|
+
* .use(createAttaform({ defaults: { debounceMs: 100 } }))
|
|
1938
|
+
* .mount('#app')
|
|
1939
|
+
* ```
|
|
1940
|
+
*
|
|
1941
|
+
* Under SSR with bare Vue 3, install explicitly with `{ ssr: true }`
|
|
1942
|
+
* from your server entry — the SSR serialization helpers
|
|
1943
|
+
* (`renderAttaformState` / `hydrateAttaformState`) require an
|
|
1944
|
+
* already-attached registry and don't trigger lazy install. Under
|
|
1945
|
+
* Nuxt, install via `attaform/nuxt` instead — the Nuxt module wires
|
|
1946
|
+
* both server and client automatically.
|
|
1947
|
+
*
|
|
1948
|
+
* Installing more than once on the same app is a no-op (the second
|
|
1949
|
+
* call logs a dev-mode warning).
|
|
1950
|
+
*/
|
|
1951
|
+
declare function createAttaform(options?: AttaformPluginOptions): Plugin;
|
|
1952
|
+
|
|
1953
|
+
/**
|
|
1954
|
+
* Serialised snapshot of every form in a Vue app, produced by
|
|
1955
|
+
* `renderAttaformState` and consumed by `hydrateAttaformState`.
|
|
1956
|
+
*
|
|
1957
|
+
* JSON-safe — pass to `JSON.stringify`, `devalue`, or any other
|
|
1958
|
+
* serialiser before embedding in your SSR payload.
|
|
1959
|
+
*/
|
|
1960
|
+
type SerializedAttaformState = {
|
|
1961
|
+
/** Tuples of `[formKey, snapshot]` for every form in the app. */
|
|
1962
|
+
readonly forms: ReadonlyArray<readonly [FormKey, SerializedFormData]>;
|
|
1963
|
+
};
|
|
1964
|
+
/**
|
|
1965
|
+
* Snapshot every form on a Vue app for SSR. Call from your server
|
|
1966
|
+
* entry after rendering the app:
|
|
1967
|
+
*
|
|
1968
|
+
* ```ts
|
|
1969
|
+
* import { renderToString } from '@vue/server-renderer'
|
|
1970
|
+
* import { renderAttaformState, escapeForInlineScript } from 'attaform'
|
|
1971
|
+
*
|
|
1972
|
+
* const html = await renderToString(app)
|
|
1973
|
+
* const state = renderAttaformState(app)
|
|
1974
|
+
* const payload = escapeForInlineScript(JSON.stringify(state))
|
|
1975
|
+
*
|
|
1976
|
+
* return `
|
|
1977
|
+
* ${html}
|
|
1978
|
+
* <script>window.__ATTAFORM_STATE__ = ${payload}</script>
|
|
1979
|
+
* `
|
|
1980
|
+
* ```
|
|
1981
|
+
*
|
|
1982
|
+
* Pair with `hydrateAttaformState` on the client to restore the
|
|
1983
|
+
* forms in their server-rendered state. Nuxt users don't need this —
|
|
1984
|
+
* `attaform/nuxt` wires SSR automatically.
|
|
1985
|
+
*/
|
|
1986
|
+
declare function renderAttaformState(app: App): SerializedAttaformState;
|
|
1987
|
+
/**
|
|
1988
|
+
* Restore forms from a server-rendered snapshot on the client. Call
|
|
1989
|
+
* from your client entry before mounting:
|
|
1990
|
+
*
|
|
1991
|
+
* ```ts
|
|
1992
|
+
* import { createApp } from 'vue'
|
|
1993
|
+
* import { createAttaform, hydrateAttaformState } from 'attaform'
|
|
1994
|
+
*
|
|
1995
|
+
* const app = createApp(App).use(createAttaform())
|
|
1996
|
+
* hydrateAttaformState(app, window.__ATTAFORM_STATE__)
|
|
1997
|
+
* app.mount('#app')
|
|
1998
|
+
* ```
|
|
1999
|
+
*
|
|
2000
|
+
* The next `useForm({ key })` call for each serialised form picks up
|
|
2001
|
+
* the snapshot transparently — no further action is required.
|
|
2002
|
+
*/
|
|
2003
|
+
declare function hydrateAttaformState(app: App, payload: SerializedAttaformState): void;
|
|
2004
|
+
|
|
2005
|
+
/**
|
|
2006
|
+
* Escape a JSON string so it's safe to embed inside an inline
|
|
2007
|
+
* `<script>` tag during SSR. Plain `JSON.stringify` is not safe — a
|
|
2008
|
+
* form value containing the literal substring `</script>` would
|
|
2009
|
+
* break out of the script tag.
|
|
2010
|
+
*
|
|
2011
|
+
* ```ts
|
|
2012
|
+
* const payload = escapeForInlineScript(JSON.stringify(renderAttaformState(app)))
|
|
2013
|
+
* // `<script>window.__ATTAFORM_STATE__ = ${payload}</script>` is safe.
|
|
2014
|
+
* ```
|
|
2015
|
+
*
|
|
2016
|
+
* Output remains valid JSON — `JSON.parse` round-trips back to the
|
|
2017
|
+
* original value on the client.
|
|
2018
|
+
*/
|
|
2019
|
+
declare function escapeForInlineScript(json: string): string;
|
|
2020
|
+
|
|
2021
|
+
/**
|
|
2022
|
+
* Symbol slot used by custom directive integrations to install an
|
|
2023
|
+
* assigner on the bound element. Read by the v-register directive
|
|
2024
|
+
* when a DOM event fires:
|
|
2025
|
+
*
|
|
2026
|
+
* ```ts
|
|
2027
|
+
* import { assignKey } from 'attaform'
|
|
2028
|
+
* el[assignKey] = (value) => myCustomWriter(value)
|
|
2029
|
+
* ```
|
|
2030
|
+
*
|
|
2031
|
+
* Most consumers never need this — the built-in directives wire
|
|
2032
|
+
* default assigners for text inputs, checkboxes, radios, and selects.
|
|
2033
|
+
*/
|
|
2034
|
+
declare const assignKey: unique symbol;
|
|
2035
|
+
|
|
2036
|
+
/**
|
|
2037
|
+
* The `v-register` directive. Bind a form field to a native input,
|
|
2038
|
+
* select, textarea, checkbox, or radio:
|
|
2039
|
+
*
|
|
2040
|
+
* ```vue
|
|
2041
|
+
* <input v-register="form.register('email')" />
|
|
2042
|
+
* <select v-register="form.register('country')">
|
|
2043
|
+
* <option value="us">US</option>
|
|
2044
|
+
* <option value="uk">UK</option>
|
|
2045
|
+
* </select>
|
|
2046
|
+
* ```
|
|
2047
|
+
*
|
|
2048
|
+
* The directive picks the right binding strategy automatically based
|
|
2049
|
+
* on the element's `tagName` and `type`. Registered globally by
|
|
2050
|
+
* `createAttaform()`. Most consumers never import it directly, but
|
|
2051
|
+
* it's exposed for advanced integrations that wire directives
|
|
2052
|
+
* manually.
|
|
2053
|
+
*/
|
|
2054
|
+
declare const vRegister: RegisterModelDynamicCustomDirective;
|
|
2055
|
+
|
|
2056
|
+
/**
|
|
2057
|
+
* Type guard for a `RegisterValue`. Returns `true` when `val` looks
|
|
2058
|
+
* like the object returned from `form.register(path)`.
|
|
2059
|
+
*
|
|
2060
|
+
* ```ts
|
|
2061
|
+
* if (isRegisterValue(slotValue)) {
|
|
2062
|
+
* // slotValue.innerRef is now a Ref<unknown>
|
|
2063
|
+
* }
|
|
2064
|
+
* ```
|
|
2065
|
+
*
|
|
2066
|
+
* Useful when building wrapper components that accept either a
|
|
2067
|
+
* `RegisterValue` or a plain ref via the same prop.
|
|
2068
|
+
*/
|
|
2069
|
+
declare function isRegisterValue<Value = unknown>(val: unknown): val is RegisterValue<Value>;
|
|
2070
|
+
|
|
2071
|
+
/**
|
|
2072
|
+
* Shared building blocks for Attaform's two devtools surfaces — the Vue
|
|
2073
|
+
* DevTools (Chrome-extension) inspector wired up in `./devtools.ts`, and
|
|
2074
|
+
* the Nuxt DevTools (overlay) panel wired up via `../../nuxt.ts` +
|
|
2075
|
+
* `../pages/_attaform_devtools.vue`.
|
|
2076
|
+
*
|
|
2077
|
+
* Houses the window-bridge contract both surfaces consume so a new
|
|
2078
|
+
* bridge field lands in one file. Both surfaces render RAW form values
|
|
2079
|
+
* by design — DevTools is a dev-only surface, and redaction across every
|
|
2080
|
+
* place a value surfaces is impractical security theater rather than a
|
|
2081
|
+
* real safeguard.
|
|
2082
|
+
*/
|
|
2083
|
+
|
|
2084
|
+
/**
|
|
2085
|
+
* Property key on `window` that the Nuxt-side dev plugin attaches the
|
|
2086
|
+
* bridge object to. The iframe-mounted overlay panel reads
|
|
2087
|
+
* `window.parent[DEVTOOLS_WINDOW_KEY]` to reach the host app's registry.
|
|
2088
|
+
*
|
|
2089
|
+
* Underscored + namespaced to make accidental collision with consumer
|
|
2090
|
+
* globals vanishingly unlikely. Stable across versions — bumping it
|
|
2091
|
+
* would silently disconnect older library builds from newer overlay
|
|
2092
|
+
* panels in the same browser tab during a library upgrade.
|
|
2093
|
+
*/
|
|
2094
|
+
declare const DEVTOOLS_WINDOW_KEY = "__attaform_devtools__";
|
|
2095
|
+
/**
|
|
2096
|
+
* Shape of the object the host plugin attaches to `window` in dev mode.
|
|
2097
|
+
* The iframe overlay panel reads this to discover the live registry and
|
|
2098
|
+
* render its forms.
|
|
2099
|
+
*
|
|
2100
|
+
* Single-registry assumption: the latest `createAttaform()` install
|
|
2101
|
+
* wins. Multi-app pages (rare; typically only seen in micro-frontend
|
|
2102
|
+
* setups) will only see one app's forms in the panel. Documented but
|
|
2103
|
+
* not actively supported — the alternative (a Set of registries with
|
|
2104
|
+
* union-rendering) is a future call if a real consumer hits it.
|
|
2105
|
+
*/
|
|
2106
|
+
interface AttaformDevtoolsBridge {
|
|
2107
|
+
registry: AttaformRegistry;
|
|
2108
|
+
/**
|
|
2109
|
+
* The library version, surfaced in the panel's footer for support /
|
|
2110
|
+
* bug-report context. Read from `package.json` at host-plugin init.
|
|
2111
|
+
*/
|
|
2112
|
+
version: string;
|
|
2113
|
+
}
|
|
2114
|
+
declare global {
|
|
2115
|
+
interface Window {
|
|
2116
|
+
[DEVTOOLS_WINDOW_KEY]?: AttaformDevtoolsBridge;
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
/**
|
|
2121
|
+
* Typed error classes thrown by the form library. Each one signals a
|
|
2122
|
+
* distinct misuse so calling code can branch on `instanceof` instead
|
|
2123
|
+
* of pattern-matching error messages.
|
|
2124
|
+
*
|
|
2125
|
+
* Every class extends `AttaformError`, so consumers can write a single
|
|
2126
|
+
* polymorphic catch (`catch (e) { if (e instanceof AttaformError) ... }`)
|
|
2127
|
+
* instead of OR-chaining checks for each subclass. `AttaformError` itself
|
|
2128
|
+
* extends the standard `Error`, so existing `instanceof Error` usage
|
|
2129
|
+
* keeps working.
|
|
2130
|
+
*/
|
|
2131
|
+
|
|
2132
|
+
/**
|
|
2133
|
+
* Base for every error class thrown by `attaform`. Sets
|
|
2134
|
+
* `this.name` from the constructor's `new.target.name`, so subclasses
|
|
2135
|
+
* don't have to redeclare their own name override.
|
|
2136
|
+
*/
|
|
2137
|
+
declare class AttaformError extends Error {
|
|
2138
|
+
constructor(message: string, options?: ErrorOptions);
|
|
2139
|
+
}
|
|
2140
|
+
/**
|
|
2141
|
+
* Thrown when a path string is malformed — typically a dotted path
|
|
2142
|
+
* with empty segments (e.g. `'a..b'`, leading or trailing dots).
|
|
2143
|
+
* Use array form (`['a', 'b']`) for keys that contain literal dots.
|
|
2144
|
+
*/
|
|
2145
|
+
declare class InvalidPathError extends AttaformError {
|
|
2146
|
+
}
|
|
2147
|
+
/**
|
|
2148
|
+
* Thrown when `useForm` receives an invalid configuration — most often
|
|
2149
|
+
* a schema passed directly as the first argument, or no argument at
|
|
2150
|
+
* all. The configuration is an options bag; the schema is one of
|
|
2151
|
+
* several fields, even when it's the only one in use.
|
|
2152
|
+
*
|
|
2153
|
+
* ```ts
|
|
2154
|
+
* // ✗ Crashes deep inside the validator with an opaque message:
|
|
2155
|
+
* const form = useForm(z.object({ ... }))
|
|
2156
|
+
* // ✗ Same:
|
|
2157
|
+
* const form = useForm()
|
|
2158
|
+
* // ✓ Pass the schema as the `schema` field:
|
|
2159
|
+
* const form = useForm({ schema: z.object({ ... }) })
|
|
2160
|
+
* ```
|
|
2161
|
+
*
|
|
2162
|
+
* The same shape applies to every entry point: `attaform/zod`,
|
|
2163
|
+
* `attaform/zod-v3`, `attaform/zod-v4`, and the schema-agnostic
|
|
2164
|
+
* `attaform` root.
|
|
2165
|
+
*/
|
|
2166
|
+
declare class InvalidUseFormConfigError extends AttaformError {
|
|
2167
|
+
constructor();
|
|
2168
|
+
}
|
|
2169
|
+
/**
|
|
2170
|
+
* Thrown when a `handleSubmit`-supplied `onError` callback itself
|
|
2171
|
+
* throws or rejects. Wraps the inner failure so both the original
|
|
2172
|
+
* cause (via `error.cause`) and the propagation site are visible.
|
|
2173
|
+
*/
|
|
2174
|
+
declare class SubmitErrorHandlerError extends AttaformError {
|
|
2175
|
+
}
|
|
2176
|
+
/**
|
|
2177
|
+
* Thrown when an `attaform` API needs the registry attached to a Vue
|
|
2178
|
+
* app but it isn't there yet. Component-level entry points (`useForm`,
|
|
2179
|
+
* `injectForm`, `useRegister`) lazy-install the registry on first use,
|
|
2180
|
+
* so this error is mostly reached by SSR helpers — `renderAttaformState`
|
|
2181
|
+
* and `hydrateAttaformState` — which run outside a setup context and
|
|
2182
|
+
* have no current instance to install against.
|
|
2183
|
+
*
|
|
2184
|
+
* Fix: add `app.use(createAttaform())` (or `app.use(createAttaform({
|
|
2185
|
+
* ssr: true }))` on the server) to your SSR entry, before
|
|
2186
|
+
* `renderToString` / hydration. Under Nuxt, `attaform/nuxt` already
|
|
2187
|
+
* does this; the error usually points at a non-Nuxt SSR setup that
|
|
2188
|
+
* hasn't installed explicitly.
|
|
2189
|
+
*/
|
|
2190
|
+
declare class RegistryNotInstalledError extends AttaformError {
|
|
2191
|
+
constructor();
|
|
2192
|
+
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Thrown when `useForm` / `injectForm` is called outside of a
|
|
2195
|
+
* Vue `setup()` function — typically from an event handler, watcher,
|
|
2196
|
+
* or async callback that runs after mount.
|
|
2197
|
+
*
|
|
2198
|
+
* Fix: move the call into `setup()`, or trigger it from a child
|
|
2199
|
+
* component whose `setup()` runs the composable.
|
|
2200
|
+
*/
|
|
2201
|
+
declare class OutsideSetupError extends AttaformError {
|
|
2202
|
+
constructor();
|
|
2203
|
+
}
|
|
2204
|
+
/**
|
|
2205
|
+
* Thrown when a `useForm({ key })` call uses a key starting with
|
|
2206
|
+
* `__atta:`. That prefix is reserved for keys the library generates
|
|
2207
|
+
* internally (e.g. for anonymous `useForm()` calls without an
|
|
2208
|
+
* explicit key). Pick a different prefix for your form.
|
|
2209
|
+
*/
|
|
2210
|
+
declare class ReservedFormKeyError extends AttaformError {
|
|
2211
|
+
constructor(key: string);
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
/**
|
|
2215
|
+
* Anti-flash timing for the library-default display reducer.
|
|
2216
|
+
*
|
|
2217
|
+
* - `showDelay` — how long in-flight work (a validation run or an async
|
|
2218
|
+
* register transform) may run before its spinner is allowed to show. Work
|
|
2219
|
+
* that settles inside this window never reveals `'pending'` at all, so a
|
|
2220
|
+
* fast (often synchronous) check does not flash a spinner on every keystroke.
|
|
2221
|
+
* - `minVisible` — once shown, the minimum time the spinner stays up. Work
|
|
2222
|
+
* that lands just past `showDelay` is held here so the spinner does not
|
|
2223
|
+
* itself flash on and immediately off.
|
|
2224
|
+
*
|
|
2225
|
+
* Both are milliseconds.
|
|
2226
|
+
*/
|
|
2227
|
+
type DisplayTimings = {
|
|
2228
|
+
readonly showDelay: number;
|
|
2229
|
+
readonly minVisible: number;
|
|
2230
|
+
};
|
|
2231
|
+
/**
|
|
2232
|
+
* Library-default anti-flash timings. `showDelay: 120` cleanly swallows
|
|
2233
|
+
* synchronous, microtask-resolved, and tiny-async work (validation runs and
|
|
2234
|
+
* async register transforms alike) so none of them flash a spinner;
|
|
2235
|
+
* `minVisible: 120` keeps a shown spinner snappy once one does land.
|
|
2236
|
+
* Retune via {@link makeDefaultDisplayState} without touching the engine.
|
|
2237
|
+
*/
|
|
2238
|
+
declare const DEFAULT_TIMINGS: DisplayTimings;
|
|
2239
|
+
/**
|
|
2240
|
+
* Build a default `getDisplayState` reducer with custom anti-flash timing.
|
|
2241
|
+
* Power users who want a tighter or looser spinner than {@link DEFAULT_TIMINGS}
|
|
2242
|
+
* pass their own `{ showDelay, minVisible }`:
|
|
2243
|
+
*
|
|
2244
|
+
* ```ts
|
|
2245
|
+
* import { makeDefaultDisplayState } from 'attaform'
|
|
2246
|
+
*
|
|
2247
|
+
* useForm({
|
|
2248
|
+
* schema,
|
|
2249
|
+
* getDisplayState: makeDefaultDisplayState({ showDelay: 50, minVisible: 200 }),
|
|
2250
|
+
* })
|
|
2251
|
+
* ```
|
|
2252
|
+
*
|
|
2253
|
+
* The returned reducer is pure: the engine injects `now` and threads the
|
|
2254
|
+
* previous machine, so the same `(prev, ctx)` always yields the same next
|
|
2255
|
+
* machine. It shapes only the display projection — `errors`, `valid`,
|
|
2256
|
+
* `validating`, and the underlying validation run exactly as before.
|
|
2257
|
+
*/
|
|
2258
|
+
declare function makeDefaultDisplayState({ showDelay, minVisible, }: DisplayTimings): GetDisplayState;
|
|
2259
|
+
/**
|
|
2260
|
+
* Library-default `getDisplayState` reducer. Resolves every path's
|
|
2261
|
+
* `field.displayState` — and thus `field.show*` and the `form.meta`
|
|
2262
|
+
* rollups — whenever the consumer has not configured an override at the
|
|
2263
|
+
* per-form or plugin level. Built from {@link DEFAULT_TIMINGS}; publicly
|
|
2264
|
+
* re-exported so an override can compose with it (a layered reducer that
|
|
2265
|
+
* special-cases a subtree but otherwise defers picks up future
|
|
2266
|
+
* refinements for free):
|
|
2267
|
+
*
|
|
2268
|
+
* ```ts
|
|
2269
|
+
* import { defaultDisplayState } from 'attaform'
|
|
2270
|
+
*
|
|
2271
|
+
* useForm({
|
|
2272
|
+
* schema,
|
|
2273
|
+
* getDisplayState: (prev, ctx) => {
|
|
2274
|
+
* const next = defaultDisplayState(prev, ctx)
|
|
2275
|
+
* return next.display === 'success' && ctx.field.path[0] === 'username'
|
|
2276
|
+
* ? { display: 'idle' }
|
|
2277
|
+
* : next
|
|
2278
|
+
* },
|
|
2279
|
+
* })
|
|
2280
|
+
* ```
|
|
2281
|
+
*/
|
|
2282
|
+
declare const defaultDisplayState: GetDisplayState;
|
|
2283
|
+
|
|
2284
|
+
export { useWizard as $, createRegistry as B, DEFAULT_TIMINGS as D, defaultCoercionRules as E, defaultDisplayState as G, defineCoercion as H, escapeForInlineScript as J, getRegistryFromApp as K, hydrateAttaformState as M, injectForm as N, OutsideSetupError as O, injectWizard as P, isRegisterValue as Q, RegistryNotInstalledError as R, kAttaformRegistry as T, lazy as V, makeDefaultDisplayState as X, renderAttaformState as Y, useRegister as Z, useRegistry as _, vRegister as a0, AttaformError as c, AttaformErrorCode as d, DEVTOOLS_WINDOW_KEY as f, InvalidPathError as h, InvalidUseFormConfigError as i, ReservedFormKeyError as j, SubmitErrorHandlerError as m, assignKey as y, createAttaform as z };
|
|
2285
|
+
export type { AttaformRegistry as A, CompiledStep as C, FormStatus as F, InjectWizardInput as I, LazyMarker as L, SerializedAttaformState as S, UseRegisterReturn as U, WizardAggregateError as W, AnyForm as a, AttaformDevtoolsBridge as b, AttaformPluginOptions as e, DisplayTimings as g, SerializedFormData as k, StepSlot as l, UseWizardReturnType as n, WizardCtx as o, WizardCtxForm as p, WizardOnError as q, WizardOnSubmit as r, WizardOptions as s, WizardPersistFn as t, WizardRestoreFn as u, WizardRestoreState as v, WizardStatusesProxy as w, WizardSubmitContext as x };
|