attaform 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abstract.d.mts +15 -21
- package/dist/abstract.mjs +3 -3
- package/dist/dev/abstract.mjs +4 -4
- package/dist/dev/chunks/devtools.mjs +4 -4
- package/dist/dev/directive.mjs +1 -1
- package/dist/dev/history.mjs +2 -2
- package/dist/dev/index.mjs +5 -5
- package/dist/dev/runtime/plugins/attaform.mjs +1 -1
- package/dist/dev/shared/{attaform-dev.DUZckyV8.mjs → attaform-dev.B1q-x9R9.mjs} +4 -4
- package/dist/dev/shared/{attaform-dev.BcYT6tFh.mjs → attaform-dev.CZz4GYZ6.mjs} +21 -9
- package/dist/dev/shared/{attaform-dev.BisFstxh.mjs → attaform-dev.Caibt7sb.mjs} +16 -15
- package/dist/dev/shared/{attaform-dev.DTrHonpc.mjs → attaform-dev.CtyqGia7.mjs} +176 -260
- package/dist/dev/shared/{attaform-dev.BK6Ge2qR.mjs → attaform-dev.DJ3nsWw6.mjs} +86 -122
- package/dist/dev/shared/{attaform-dev.BcNtyOmf.mjs → attaform-dev.DlaT4xGd.mjs} +4 -4
- package/dist/dev/shared/{attaform-dev.D6CLvaQT.mjs → attaform-dev.k5X47GMD.mjs} +520 -395
- package/dist/dev/shared/{attaform-dev.CQ2rJiAX.mjs → attaform-dev.y9x9Vp9F.mjs} +324 -234
- package/dist/dev/shared/{attaform-dev.BQc5QV3Y.mjs → attaform-dev.zlvDr0WB.mjs} +2 -2
- package/dist/dev/zod-v3.mjs +6 -6
- package/dist/dev/zod-v4.mjs +6 -6
- package/dist/dev/zod.mjs +5 -5
- package/dist/directive.d.mts +4 -4
- package/dist/directive.mjs +1 -1
- package/dist/esbuild.d.mts +11 -0
- package/dist/esbuild.mjs +1 -1
- package/dist/history.d.mts +3 -32
- package/dist/history.mjs +2 -2
- package/dist/index.d.mts +67 -70
- package/dist/index.mjs +4 -4
- package/dist/nuxt.d.mts +4 -3
- package/dist/nuxt.mjs +3 -6
- package/dist/rollup.d.mts +11 -0
- package/dist/rollup.mjs +1 -1
- package/dist/rspack.d.mts +19 -11
- package/dist/rspack.mjs +1 -1
- package/dist/runtime/components/AttaformDevtoolsPanel.vue +15 -15
- package/dist/runtime/components/DevtoolsValueTree.vue +2 -2
- package/dist/runtime/plugins/attaform.mjs +1 -1
- package/dist/shared/{attaform.BRDDQeHV.mjs → attaform.8mfKZCwH.mjs} +2 -2
- package/dist/shared/{attaform.DRUT703x.d.mts → attaform.B-Tb62qV.d.mts} +12 -12
- package/dist/shared/{attaform.v0fQtWyA.mjs → attaform.B1rH9ht6.mjs} +1 -1
- package/dist/shared/{attaform.DhnPb6nK.mjs → attaform.BAf3UNn4.mjs} +1 -1
- package/dist/shared/{attaform.BXneBxEj.mjs → attaform.BWUn8Kjy.mjs} +85 -121
- package/dist/shared/{attaform.WiGdIqgx.mjs → attaform.C4shmwyJ.mjs} +2 -2
- package/dist/shared/{attaform.Rc9u4Vfw.d.mts → attaform.CRVe8s82.d.mts} +83 -95
- package/dist/shared/attaform.D9RqPgGu.d.mts +42 -0
- package/dist/shared/attaform.DAo0QBmS.d.mts +2173 -0
- package/dist/shared/{attaform.BK0cgu2K.mjs → attaform.DBCbxlyk.mjs} +62 -4
- package/dist/shared/{attaform.CUfrMK7t.mjs → attaform.DL2nl_QR.mjs} +15 -14
- package/dist/shared/{attaform.DNJOWjQw.mjs → attaform.DNPJPOO7.mjs} +299 -218
- package/dist/shared/attaform.DZnrDrNb.d.mts +4657 -0
- package/dist/shared/{attaform.B3resMQ1.mjs → attaform.DxKQsqFZ.mjs} +496 -371
- package/dist/shared/{attaform.ByWP6kSx.mjs → attaform.L011tHgJ.mjs} +3 -3
- package/dist/shared/{attaform.CJ8DpRGj.mjs → attaform.LUPC-2Qg.mjs} +175 -257
- package/dist/transforms.d.mts +153 -26
- package/dist/transforms.mjs +1 -1
- package/dist/vite.d.mts +29 -52
- package/dist/vite.mjs +3 -3
- package/dist/webpack.d.mts +20 -12
- package/dist/webpack.mjs +1 -1
- package/dist/zod-v3.d.mts +90 -96
- package/dist/zod-v3.mjs +5 -5
- package/dist/zod-v4.d.mts +120 -118
- package/dist/zod-v4.mjs +5 -5
- package/dist/zod.d.mts +5 -5
- package/dist/zod.mjs +4 -4
- package/package.json +7 -6
- package/skills/attaform/SKILL.md +1 -1
- package/skills/attaform/references/custom-components.md +4 -0
- package/dist/shared/attaform.Clz0ArIi.d.mts +0 -44
- package/dist/shared/attaform.UGyOK83T.d.mts +0 -2366
- package/dist/shared/attaform.bKLzMU0o.d.mts +0 -4761
|
@@ -0,0 +1,2173 @@
|
|
|
1
|
+
import { G as GenericForm, F as FormKey, a as UseFormReturnType, _ as RegisterValue, v as Path, w as PathKey, aq as DisplayCtx, ar as GetDisplayState, as as DisplayMachine, at as FieldRecord, au as ErrorCell, ac as ValidationError, av as OriginalsRecord, A as AbstractSchema, ag as WriteMeta, d as DeepPartial, ah as WriteShape, aw as AttaformDomBinding, ax as TransformAbortHolder } from './attaform.DZnrDrNb.mjs';
|
|
2
|
+
import { Ref, ComputedRef, ShallowRef, App, InjectionKey, Plugin } from 'vue';
|
|
3
|
+
|
|
4
|
+
/** Options accepted by `injectForm` in place of a bare key string. */
|
|
5
|
+
type InjectFormInput = {
|
|
6
|
+
readonly key?: FormKey;
|
|
7
|
+
/**
|
|
8
|
+
* Set by the Vite transform when this `injectForm` call site sits in
|
|
9
|
+
* a component whose template / script reads the form's reactive
|
|
10
|
+
* state. On the server, this enqueues the form for SSR prefetch and
|
|
11
|
+
* wires `onServerPrefetch` so the descendant awaits the activation
|
|
12
|
+
* promise before its render emits HTML.
|
|
13
|
+
*
|
|
14
|
+
* @internal Transform-emitted. Manual use is the documented escape
|
|
15
|
+
* hatch when the transform can't reach the reference (dynamic
|
|
16
|
+
* property access, untransformed bundlers).
|
|
17
|
+
*/
|
|
18
|
+
readonly __ssrAccessed?: boolean;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Reach an existing form from a descendant component without threading
|
|
22
|
+
* it through props. The counterpart to `useForm`: `useForm` creates and
|
|
23
|
+
* provides, `injectForm` looks up.
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* // The nearest ancestor's anonymous useForm() call.
|
|
27
|
+
* const form = injectForm<SignupShape>()
|
|
28
|
+
*
|
|
29
|
+
* // A specific form by key, from anywhere in the app.
|
|
30
|
+
* const cart = injectForm<CartShape>('cart')
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* A keyed call is a registry lookup, so it is independent of
|
|
34
|
+
* component-tree position. A no-key call takes the closest ambient
|
|
35
|
+
* ancestor and inherits its `formInstanceId`; only an anonymous
|
|
36
|
+
* `useForm()` fills that ambient slot, so a keyed form is reachable
|
|
37
|
+
* only by its key.
|
|
38
|
+
*
|
|
39
|
+
* Returns `null` when nothing matches, so narrow before use:
|
|
40
|
+
*
|
|
41
|
+
* ```ts
|
|
42
|
+
* const form = injectForm<Shape>('signup')
|
|
43
|
+
* if (!form) return
|
|
44
|
+
* form.register('email')
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* A keyed miss warns in dev with the registered keys and the call site.
|
|
48
|
+
* Pass the `Form` generic explicitly, since Vue's provide/inject erases
|
|
49
|
+
* generics. The form stays alive for this component's lifetime and is
|
|
50
|
+
* cleaned up once every consumer has unmounted.
|
|
51
|
+
*
|
|
52
|
+
* Components whose nearest `useForm`-calling ancestor is the same one get
|
|
53
|
+
* the same form object for the same form: `injectForm('cart')` in two of
|
|
54
|
+
* its descendants returns one form, with one set of field caches.
|
|
55
|
+
*/
|
|
56
|
+
declare function injectForm<Form extends GenericForm, GetValueFormType extends GenericForm = Form>(input?: FormKey | InjectFormInput): UseFormReturnType<Form, GetValueFormType> | null;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Return type of `useRegister()`. Hybrid of `RegisterValue<V>` (so
|
|
60
|
+
* `rv.path` / `rv.segments` / `rv.formKey` etc. work directly in
|
|
61
|
+
* script setup) and `Ref<RegisterValue<V> | undefined>` (so Vue's
|
|
62
|
+
* template auto-unwrap surfaces the underlying RV to `v-register`
|
|
63
|
+
* and the directive's path-migration diff sees the real RV across
|
|
64
|
+
* renders).
|
|
65
|
+
*
|
|
66
|
+
* The two surfaces don't clash at the type level: `RegisterValue`
|
|
67
|
+
* doesn't carry a `value` field, and `Ref<T>`'s `value: T` becomes
|
|
68
|
+
* the hybrid's only `.value`. Older code that read `rv.value?.path`
|
|
69
|
+
* keeps working; new code can write `rv.path` directly.
|
|
70
|
+
*/
|
|
71
|
+
type UseRegisterReturn<V = unknown> = RegisterValue<V> & Ref<RegisterValue<V> | undefined> & {
|
|
72
|
+
/**
|
|
73
|
+
* Whether a parent has actually bound this wrapper. `false` until a
|
|
74
|
+
* `RegisterValue` lands, `true` once one has, and reactive in both
|
|
75
|
+
* directions: a parent that binds on a later render flips it, and
|
|
76
|
+
* so does one that stops binding.
|
|
77
|
+
*
|
|
78
|
+
* This is the test a dual-mode wrapper reaches for, and the reason
|
|
79
|
+
* it exists as a field rather than a truthiness check on the
|
|
80
|
+
* return: the composable cannot answer "is this bound" at setup
|
|
81
|
+
* time. A parent is free to bind on a later render
|
|
82
|
+
* (`v-register="ready ? form.register('x') : undefined"`), and
|
|
83
|
+
* `useRegister` has to keep serving that, so it can never return a
|
|
84
|
+
* plain `undefined` for "unbound right now". Script-setup code
|
|
85
|
+
* reading `rv` directly sees the hybrid Proxy, which is an object
|
|
86
|
+
* and therefore always truthy; a template sees the unwrapped ref,
|
|
87
|
+
* which IS `undefined` when unbound. `rv?.isBound` reads the same
|
|
88
|
+
* on both sides (#620).
|
|
89
|
+
*/
|
|
90
|
+
readonly isBound: boolean;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Options for `useRegister`.
|
|
94
|
+
*/
|
|
95
|
+
type UseRegisterOptions = {
|
|
96
|
+
/**
|
|
97
|
+
* Declares that this wrapper is used both with and without a form, so
|
|
98
|
+
* a render with no parent binding is intended rather than a mistake.
|
|
99
|
+
* Silences the unbound diagnostic; nothing else changes.
|
|
100
|
+
*
|
|
101
|
+
* The diagnostic exists because a single-mode wrapper whose parent
|
|
102
|
+
* forgot `v-register` renders a field that looks fine and stores
|
|
103
|
+
* nothing, which is an expensive afternoon. Once a wrapper is
|
|
104
|
+
* deliberately dual-mode, the same signal fires on correct code and
|
|
105
|
+
* tells the author to do something wrong, so this is the one fact
|
|
106
|
+
* that separates the two cases and only the author has it (#620).
|
|
107
|
+
*
|
|
108
|
+
* Pair it with `rv?.isBound` to branch on what actually happened.
|
|
109
|
+
*/
|
|
110
|
+
readonly optional?: boolean;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Re-bind a parent's `v-register` onto an inner native element. Use it
|
|
114
|
+
* inside a component that wraps a single form field whose root is NOT
|
|
115
|
+
* the input itself, such as a labelled row that renders `<label>`
|
|
116
|
+
* around the input.
|
|
117
|
+
*
|
|
118
|
+
* ```vue
|
|
119
|
+
* <!-- Parent -->
|
|
120
|
+
* <MyInput v-register="form.register('email')" />
|
|
121
|
+
*
|
|
122
|
+
* <!-- MyInput.vue -->
|
|
123
|
+
* <script setup lang="ts">
|
|
124
|
+
* import { useRegister } from 'attaform'
|
|
125
|
+
* const rv = useRegister()
|
|
126
|
+
* </script>
|
|
127
|
+
*
|
|
128
|
+
* <template>
|
|
129
|
+
* <label class="field">
|
|
130
|
+
* <span>Email</span>
|
|
131
|
+
* <input v-register="rv" />
|
|
132
|
+
* </label>
|
|
133
|
+
* </template>
|
|
134
|
+
* ```
|
|
135
|
+
*
|
|
136
|
+
* The return is a hybrid: `v-register="rv"` hands the directive the
|
|
137
|
+
* parent's own `RegisterValue`, and `rv.path` / `rv.segments` /
|
|
138
|
+
* `rv.formKey` / `rv.innerRef` read directly in script setup with no
|
|
139
|
+
* `.value` unwrap. Reads inside a `computed` or `watchEffect` re-run
|
|
140
|
+
* when the parent rebinds to a different path.
|
|
141
|
+
*
|
|
142
|
+
* Modifiers on the parent's binding apply at the inner control, so
|
|
143
|
+
* `<MyInput v-register.trim="form.register('email')" />` trims there,
|
|
144
|
+
* together with any the wrapper writes on its own `v-register`.
|
|
145
|
+
*
|
|
146
|
+
* When no parent bound, every field reads `undefined`, so reach for
|
|
147
|
+
* `rv?.path`. `v-register="rv"` is still safe. A wrapper that is meant
|
|
148
|
+
* to render both with and without a form declares it with
|
|
149
|
+
* `useRegister({ optional: true })` and branches on `rv?.isBound`.
|
|
150
|
+
*
|
|
151
|
+
* When the wrapper's root IS the input, Vue's attribute fallthrough
|
|
152
|
+
* already binds it and `useRegister` is unnecessary. For a wrapper
|
|
153
|
+
* that binds several fields, use `injectForm<Form>(key?)` and call
|
|
154
|
+
* `ctx.register(...)` directly.
|
|
155
|
+
*/
|
|
156
|
+
declare function useRegister<V = unknown>(options?: UseRegisterOptions): UseRegisterReturn<V> | undefined;
|
|
157
|
+
|
|
158
|
+
type DynamicPathSweep = {
|
|
159
|
+
/**
|
|
160
|
+
* Note that a cache has just stored an entry for `segments` under
|
|
161
|
+
* `key`. Paths a schema shape bounds are ignored, so only the
|
|
162
|
+
* candidates the sweep could ever drop are walked.
|
|
163
|
+
*/
|
|
164
|
+
readonly track: (segments: Path, key: PathKey) => void;
|
|
165
|
+
/**
|
|
166
|
+
* Register a cache's own eviction. It receives the canonical
|
|
167
|
+
* `PathKey` of a path the form no longer has, and deletes whatever it
|
|
168
|
+
* files under that path. A cache keying by more than the path (a
|
|
169
|
+
* shape sigil, say) deletes every variant; a key it never held is a
|
|
170
|
+
* no-op. Returns the release, which a registrant that does not live
|
|
171
|
+
* as long as the form must call.
|
|
172
|
+
*/
|
|
173
|
+
readonly onEvict: (evict: (key: PathKey) => void) => () => void;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Per-form display engine: owns the clock and the timers the display
|
|
178
|
+
* reducer's timing policy needs, so the reducer itself stays a
|
|
179
|
+
* deterministic `(prev, ctx) => next` function.
|
|
180
|
+
*
|
|
181
|
+
* It keeps a `Map` of the machines that are still *active* (a spinner is
|
|
182
|
+
* showing, a verdict is being held, or a `reviewAt` deadline is pending),
|
|
183
|
+
* a single reactive `tick` ref, and a single `setTimeout` aimed at the
|
|
184
|
+
* nearest `reviewAt` across every active field. When a field-state
|
|
185
|
+
* computed reads `resolve`, it subscribes to `tick`; when the timer
|
|
186
|
+
* fires, `tick` bumps and every dependent computed re-runs the reducer,
|
|
187
|
+
* so a field whose deadline elapsed transitions (verdict → spinner, or
|
|
188
|
+
* spinner → settled verdict) without any per-field watcher.
|
|
189
|
+
*
|
|
190
|
+
* Eviction: a machine is dropped once it reaches a terminal *idle* state
|
|
191
|
+
* with no pending review. Error / success / pending machines are retained
|
|
192
|
+
* so the reducer can hold the prior verdict under the show-delay window of
|
|
193
|
+
* the *next* validation streak (no success → idle → success flicker). The
|
|
194
|
+
* retained set is bounded by the rendered non-idle fields; none of them
|
|
195
|
+
* arm a timer, so retention costs memory, never CPU.
|
|
196
|
+
*
|
|
197
|
+
* The `setTimeout` invariants on a returned `reviewAt` are enforced here
|
|
198
|
+
* rather than trusted from the reducer. A non-finite deadline is ignored
|
|
199
|
+
* instead of handed to `setTimeout`, where it coerces to 0 and spins; an
|
|
200
|
+
* over-large finite deadline is clamped below the 32-bit `setTimeout`
|
|
201
|
+
* overflow; and the timer refuses to re-arm for a deadline it just fired
|
|
202
|
+
* that has also already passed. That last one is load-bearing, not
|
|
203
|
+
* belt-and-braces: Attaform's own reducer re-emits ONE fixed instant for
|
|
204
|
+
* the length of a min-visible hold, which is exactly the shape that can
|
|
205
|
+
* strand a field on `'pending'` forever (see `rearm`).
|
|
206
|
+
*
|
|
207
|
+
* Background tabs: `setTimeout` is throttled to >= 1s while a tab is hidden,
|
|
208
|
+
* so a min-visible hold can overshoot. A `visibilitychange` listener bumps
|
|
209
|
+
* the clock on return to the foreground, so any overdue deadline resolves at
|
|
210
|
+
* once instead of lingering.
|
|
211
|
+
*
|
|
212
|
+
* SSR: no clock, no timers, no listener. With `now` frozen and nothing
|
|
213
|
+
* validating at render, the reducer returns the plain verdict (never
|
|
214
|
+
* pending) and the engine stores nothing, so the server HTML and the
|
|
215
|
+
* client's first render agree, with no hydration mismatch on the display
|
|
216
|
+
* projection.
|
|
217
|
+
*/
|
|
218
|
+
type DisplayEngine = {
|
|
219
|
+
/**
|
|
220
|
+
* Resolve a path's next `DisplayMachine`. Subscribes the calling
|
|
221
|
+
* computed to the engine clock, threads the path's previous machine
|
|
222
|
+
* through `reducer`, persists or evicts the result, and re-arms the
|
|
223
|
+
* single timer to the nearest deadline.
|
|
224
|
+
*/
|
|
225
|
+
resolve(key: PathKey, ctx: DisplayCtx, reducer: GetDisplayState): DisplayMachine;
|
|
226
|
+
/** Drop every retained machine and cancel the timer (used by `reset()`). */
|
|
227
|
+
clear(): void;
|
|
228
|
+
/** Tear down for good: `clear()` plus detaching the visibility listener. */
|
|
229
|
+
dispose(): void;
|
|
230
|
+
/** Introspection for tests: count of retained machines. Dev builds only. */
|
|
231
|
+
size?(): number;
|
|
232
|
+
/** Introspection for tests: whether a path has a retained machine. Dev builds only. */
|
|
233
|
+
has?(key: PathKey): boolean;
|
|
234
|
+
/** Introspection for tests: whether a deadline timer is armed. Dev builds only. */
|
|
235
|
+
hasTimer?(): boolean;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The error stores, indexed by path prefix.
|
|
240
|
+
*
|
|
241
|
+
* `aggregateErrorsAt(state, prefix)` answers "every active error at or
|
|
242
|
+
* under this path". Scanning all three stores per call makes a table's
|
|
243
|
+
* reads O(rows x errors) where the answer is O(errors), so keys are
|
|
244
|
+
* collected once per change, resolved to segments once and sorted, and
|
|
245
|
+
* a query binary-searches its own window.
|
|
246
|
+
*
|
|
247
|
+
* A `PathKey` is `JSON.stringify(segments)`, which is what makes a
|
|
248
|
+
* sorted array enough. Every key under `["rows",0]` begins
|
|
249
|
+
* `["rows",0,`: the parent's key minus its closing bracket, plus a
|
|
250
|
+
* comma. Call that the marker. Every descendant is >= the marker, and
|
|
251
|
+
* every descendant is <= the parent's own key, because the two agree
|
|
252
|
+
* up to the last character and `,` (0x2C) sorts before `]` (0x5D). So
|
|
253
|
+
* `[marker, parentKey]` is a contiguous window holding every
|
|
254
|
+
* descendant and the parent itself.
|
|
255
|
+
*
|
|
256
|
+
* The window is a SUPERSET: callers still apply `isPathPrefix`, so this
|
|
257
|
+
* encoding argument cannot quietly become the definition of a prefix.
|
|
258
|
+
* The root has no marker (`[,` begins nothing) and matches everything,
|
|
259
|
+
* so it is answered with the whole index.
|
|
260
|
+
*/
|
|
261
|
+
|
|
262
|
+
/** One indexed path: its key, and the segments that key decodes to. */
|
|
263
|
+
type ErrorPathEntry = {
|
|
264
|
+
readonly key: PathKey;
|
|
265
|
+
readonly segments: Path;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The per-form kernel each `useForm` call owns: form value, field records,
|
|
270
|
+
* element references, the error stores and the submission, validation and
|
|
271
|
+
* transform lifecycles, all keyed by `(formKey, path)` so two forms cannot
|
|
272
|
+
* collide over shared DOM state.
|
|
273
|
+
*
|
|
274
|
+
* Not a singleton. Every `useForm` call builds one; the registry supplies SSR
|
|
275
|
+
* hydration, and otherwise the state is per component per form.
|
|
276
|
+
*/
|
|
277
|
+
type FormStore<F extends GenericForm, G extends GenericForm = F> = {
|
|
278
|
+
/**
|
|
279
|
+
* The form's single liveness sweep over every per-path cache. It lives on
|
|
280
|
+
* the store, not on `buildFormApi`, because the store's own maps (`fields`,
|
|
281
|
+
* `originals`, `authoredPaths`, `fieldValidationState`) are the largest
|
|
282
|
+
* thing it evicts and they exist whether or not a form API was ever built
|
|
283
|
+
* around them. Read surfaces register their evictions into it, so one
|
|
284
|
+
* subscription and one walk serve all of them. See `dynamic-path-sweep.ts`.
|
|
285
|
+
*/
|
|
286
|
+
readonly pathSweep: DynamicPathSweep;
|
|
287
|
+
readonly formKey: FormKey;
|
|
288
|
+
readonly form: Ref<F>;
|
|
289
|
+
readonly fields: Map<PathKey, FieldRecord>;
|
|
290
|
+
/**
|
|
291
|
+
* The tagged error store: one cell per error-bearing path, each segregating
|
|
292
|
+
* its two sources. The `schema` side is written only by the validation
|
|
293
|
+
* pipeline (`scheduleFieldValidation`, `handleSubmit`, the construction
|
|
294
|
+
* seed, history restore, hydration) and cleared by `reset` / `resetField`
|
|
295
|
+
* and a successful submit. The `user` side is written only by `setErrors` /
|
|
296
|
+
* `clearErrors` (plus history / hydration replay) and survives both schema
|
|
297
|
+
* revalidation and a successful submit, so the consumer owns its lifetime.
|
|
298
|
+
* A key exists iff a side is non-empty, and cells are replaced rather than
|
|
299
|
+
* mutated so Vue's per-key Map tracking fires for either side. Derived blank
|
|
300
|
+
* entries are not stored here; they synthesize at read
|
|
301
|
+
* (`derivedBlankErrors`).
|
|
302
|
+
*
|
|
303
|
+
* A method, not a property, because it is also the gate on the
|
|
304
|
+
* construction seed: the first call runs it (see `createErrorCells`). A
|
|
305
|
+
* getter on the store object would gate a property read the same way,
|
|
306
|
+
* but V8 builds an object literal holding an accessor in dictionary mode,
|
|
307
|
+
* and every kernel read of the store would then pay a hash lookup.
|
|
308
|
+
*/
|
|
309
|
+
readonly errorCells: () => Map<PathKey, ErrorCell>;
|
|
310
|
+
/**
|
|
311
|
+
* Reactively-derived "No value supplied" errors: a pure function of
|
|
312
|
+
* `(blankPaths, schema.isRequiredAtPath)` with no writers and no clears.
|
|
313
|
+
* Membership follows `blankPaths`, so typing into a blank required numeric
|
|
314
|
+
* field makes the error vanish and clearing the input brings it back. The
|
|
315
|
+
* `errors` proxy and `getErrorsForPath` merge this alongside the schema and
|
|
316
|
+
* user sides, so the "this required field is empty" error is true the moment
|
|
317
|
+
* it is true, with no `validate()` / `handleSubmit` call.
|
|
318
|
+
*
|
|
319
|
+
* Most entries are `number` / `bigint` leaves, where the side channel is the
|
|
320
|
+
* only way to tell "user typed 0" from "user supplied nothing". String and
|
|
321
|
+
* boolean leaves land here only when the consumer opts in with the `unset`
|
|
322
|
+
* sentinel. See `docs/validation/blank.md`.
|
|
323
|
+
*/
|
|
324
|
+
readonly derivedBlankErrors: ComputedRef<ReadonlyMap<PathKey, ValidationError[]>>;
|
|
325
|
+
/**
|
|
326
|
+
* Every path carrying an error at or under `prefix`, sorted by key.
|
|
327
|
+
* `aggregateErrorsAt` takes its candidates from here rather than rescanning
|
|
328
|
+
* all three error stores per call, which is what keeps a table of N rows at
|
|
329
|
+
* O(errors) instead of O(N x errors). See `error-path-index.ts`.
|
|
330
|
+
*
|
|
331
|
+
* Going through this rather than the form-global index is also what isolates
|
|
332
|
+
* containers from each other: the index is one value for the whole form, so
|
|
333
|
+
* any path gaining or losing an error gives it a fresh identity. Each prefix
|
|
334
|
+
* gets a memoised `computed` that hands back its previous array when its own
|
|
335
|
+
* window is unchanged, so the global change stops there. See `errorWindowAt`.
|
|
336
|
+
*/
|
|
337
|
+
readonly errorWindowAt: (prefix: Path, prefixKey: PathKey) => readonly ErrorPathEntry[];
|
|
338
|
+
readonly originals: Map<PathKey, OriginalsRecord>;
|
|
339
|
+
/**
|
|
340
|
+
* Reactive set of paths whose displayed state should be EMPTY even though
|
|
341
|
+
* storage holds a real, schema-conformant value (the slim default). It
|
|
342
|
+
* records storage / display divergence, the case where storage alone cannot
|
|
343
|
+
* tell "user typed 0" from "user supplied nothing".
|
|
344
|
+
*
|
|
345
|
+
* `number` / `bigint` are the reason it exists: storage holds `0` / `0n`
|
|
346
|
+
* while the DOM input shows `''`, so the directive's input listener marks
|
|
347
|
+
* the path on clear. Strings and booleans need no mark (`''` storage is `''`
|
|
348
|
+
* display, `false` is unchecked), so they are never auto-marked. Consumers
|
|
349
|
+
* can still mark any primitive leaf through the `unset` sentinel
|
|
350
|
+
* (`defaultValues: { x: unset }`, `setValue('x', unset)`,
|
|
351
|
+
* `reset({ x: unset })`), where the mark is declared intent rather than
|
|
352
|
+
* runtime inference.
|
|
353
|
+
*
|
|
354
|
+
* Reads (the `displayValue` computed, `fields.<path>.blank`,
|
|
355
|
+
* `derivedBlankErrors`) track through Vue's reactive Set handlers; writes
|
|
356
|
+
* happen in `setValueAtPath` (`blank: true` meta adds, any other write
|
|
357
|
+
* removes) and `reset`.
|
|
358
|
+
*
|
|
359
|
+
* Storage never reflects this set: reads against `form.value` see the slim
|
|
360
|
+
* default. It is a display / intent channel that `derivedBlankErrors`
|
|
361
|
+
* consults. See `docs/validation/blank.md`.
|
|
362
|
+
*/
|
|
363
|
+
readonly blankPaths: Set<PathKey>;
|
|
364
|
+
/**
|
|
365
|
+
* Snapshot of `blankPaths` taken at construction and re-taken on
|
|
366
|
+
* `reset(args)`. Dirty calculation compares against it, so a path whose
|
|
367
|
+
* membership diverges is dirty even when storage matches the original.
|
|
368
|
+
* Populated eagerly, so there is no "dirty on first read" window.
|
|
369
|
+
*/
|
|
370
|
+
readonly originalBlankPaths: Set<PathKey>;
|
|
371
|
+
readonly schema: AbstractSchema<F, G>;
|
|
372
|
+
/**
|
|
373
|
+
* Server-side flag, plumbed in from `registry.ssr`.
|
|
374
|
+
* `markConnectedOptimistically()` reads it before flipping
|
|
375
|
+
* `connected: true`; on the client it is a no-op, leaving the directive
|
|
376
|
+
* lifecycle as the source of truth.
|
|
377
|
+
*/
|
|
378
|
+
readonly ssr: boolean;
|
|
379
|
+
/**
|
|
380
|
+
* Per-form display engine. It owns the clock and the single timer the timed
|
|
381
|
+
* display policy needs, which is what keeps the reducer a pure
|
|
382
|
+
* `(prev, ctx) => next`. Field-state computeds route every `displayState`
|
|
383
|
+
* read through `displayEngine.resolve(...)`, which threads the path's
|
|
384
|
+
* previous machine, persists or evicts the result, and re-arms the
|
|
385
|
+
* nearest-deadline timer. Torn down through `registerCleanup` on eviction.
|
|
386
|
+
*/
|
|
387
|
+
readonly displayEngine: DisplayEngine;
|
|
388
|
+
readonly submitting: Ref<boolean>;
|
|
389
|
+
readonly activeSubmissions: Ref<number>;
|
|
390
|
+
readonly submissionAttempts: Ref<number>;
|
|
391
|
+
/**
|
|
392
|
+
* `true` once a `handleSubmit` callback resolved without throwing.
|
|
393
|
+
* Independent of `submissionAttempts`: a failed submit increments attempts
|
|
394
|
+
* and leaves `submitted` at `false`. Cleared by `reset()`.
|
|
395
|
+
*/
|
|
396
|
+
readonly submitted: Ref<boolean>;
|
|
397
|
+
readonly submitError: Ref<Error | null>;
|
|
398
|
+
readonly departAttempts: Ref<number>;
|
|
399
|
+
/**
|
|
400
|
+
* Effective data-freeze state: `true` when either the form's own
|
|
401
|
+
* `disabled` config resolves truthy or `externalLock` is set. Read at
|
|
402
|
+
* the write chokepoint (`setValueAtPath`), the register value's
|
|
403
|
+
* `disabled`, and the field display predicate.
|
|
404
|
+
*/
|
|
405
|
+
readonly effectiveDisabled: ComputedRef<boolean>;
|
|
406
|
+
/**
|
|
407
|
+
* Wizard-driven freeze channel. `useWizard` sets it `true` for a locked
|
|
408
|
+
* step's member form, and `effectiveDisabled` ORs it in, so a member form
|
|
409
|
+
* cannot escape the lock by passing `disabled: false`. Default `false`.
|
|
410
|
+
*/
|
|
411
|
+
readonly externalLock: Ref<boolean>;
|
|
412
|
+
/**
|
|
413
|
+
* `true` while a function-form `defaultValues` factory is in flight, and
|
|
414
|
+
* always `false` for plain-value `defaultValues`. Shared across every
|
|
415
|
+
* `useForm({ key })` call resolving to this store, so the second caller
|
|
416
|
+
* sees the first caller's hydration state.
|
|
417
|
+
*/
|
|
418
|
+
readonly hydrating: Ref<boolean>;
|
|
419
|
+
/**
|
|
420
|
+
* Error from the most recent function-form `defaultValues` factory.
|
|
421
|
+
* Normalized to a `ValidationError` (code `atta:hydration-failed`) so the
|
|
422
|
+
* shape matches `form.errors` / `form.meta.errors` entries. `null` when
|
|
423
|
+
* no factory has fired or the last one succeeded.
|
|
424
|
+
*/
|
|
425
|
+
readonly hydrateError: Ref<ValidationError | null>;
|
|
426
|
+
/**
|
|
427
|
+
* The function-form `defaultValues` factory, captured at the first
|
|
428
|
+
* `useForm({ key })` call that wired this store. `undefined` for
|
|
429
|
+
* plain-value forms. Read by `form.rehydrate()`.
|
|
430
|
+
*/
|
|
431
|
+
readonly defaultValuesFactory: Ref<(() => unknown | Promise<unknown>) | undefined>;
|
|
432
|
+
/**
|
|
433
|
+
* `true` when this store carries an SSR prefetch queue, the server path
|
|
434
|
+
* where `activate()` must enqueue intent before deciding whether to fire.
|
|
435
|
+
* `buildFormApi` uses it to skip the lazy activation gate for forms with
|
|
436
|
+
* neither a factory nor a prefetch, the common client case where `gated()`
|
|
437
|
+
* is pure overhead on every public method call.
|
|
438
|
+
*/
|
|
439
|
+
readonly hasSsrPrefetch: boolean;
|
|
440
|
+
/**
|
|
441
|
+
* `true` once the form's effective defaults have been applied, whether from
|
|
442
|
+
* a sync `defaultValues` at construction or an async factory that settled.
|
|
443
|
+
* Stays `false` for a dormant lazy form until it activates. `useWizard`
|
|
444
|
+
* reads it to decide between surfacing seed status and live meta.
|
|
445
|
+
*/
|
|
446
|
+
readonly defaultsResolved: Ref<boolean>;
|
|
447
|
+
/**
|
|
448
|
+
* `true` once the captured async factory has been kicked off, set
|
|
449
|
+
* synchronously by `activate()` before the factory resolves. Paired with
|
|
450
|
+
* `defaultsResolved` (which flips only on settle) so the API surface can
|
|
451
|
+
* tell "started" from "done".
|
|
452
|
+
*/
|
|
453
|
+
readonly activated: Ref<boolean>;
|
|
454
|
+
/**
|
|
455
|
+
* In-flight activation promise. Concurrent callers (cross-component SSR
|
|
456
|
+
* consumers, recursive factory reads, parallel `activate()` calls) all
|
|
457
|
+
* receive this one, so the factory runs once under contention.
|
|
458
|
+
*/
|
|
459
|
+
readonly activationPromise: Ref<Promise<void> | undefined>;
|
|
460
|
+
/**
|
|
461
|
+
* Idempotent activation entrypoint. Fires the captured function-form
|
|
462
|
+
* `defaultValues` factory on the first call and publishes the in-flight
|
|
463
|
+
* promise; later calls return that promise until it settles and
|
|
464
|
+
* `Promise.resolve()` afterwards. A plain-value form always resolves
|
|
465
|
+
* immediately. Every public getter and method except `key` routes through
|
|
466
|
+
* here, so the form activates on first use.
|
|
467
|
+
*/
|
|
468
|
+
activate(): Promise<void>;
|
|
469
|
+
/**
|
|
470
|
+
* Re-fire the captured function-form `defaultValues` factory. Throws
|
|
471
|
+
* synchronously on a plain-value form, where no factory was captured.
|
|
472
|
+
* Resolves once `hydrating` is back to `false`, so `await form.rehydrate()`
|
|
473
|
+
* can gate UI on the fresh load.
|
|
474
|
+
*
|
|
475
|
+
* Leaves touched / submit state alone; chain `form.reset()` for a clean
|
|
476
|
+
* surface. It does re-seat the defaults (see `adoptResolvedDefaults`), so
|
|
477
|
+
* afterwards dirty means "differs from what the factory just returned" and
|
|
478
|
+
* `form.reset()` lands on the fresh values instead of discarding them.
|
|
479
|
+
*/
|
|
480
|
+
rehydrate(): Promise<void>;
|
|
481
|
+
/**
|
|
482
|
+
* Adopt a resolved async-defaults value as the form's durable defaults and
|
|
483
|
+
* re-seed the dirty baseline from it. Called by the activate / rehydrate
|
|
484
|
+
* orchestrator once its factory settles. It hangs off the store rather than
|
|
485
|
+
* being imported because `form-activation` imports this module, and a value
|
|
486
|
+
* import back the other way would close a cycle.
|
|
487
|
+
*/
|
|
488
|
+
adoptResolvedDefaults(value: unknown): void;
|
|
489
|
+
/**
|
|
490
|
+
* Incremented by every `reset()`. The submit wrapper captures it at entry
|
|
491
|
+
* and skips writing `submitError` from a catch that fires after a reset,
|
|
492
|
+
* which is what stops a reset-during-submit clearing `submitError` only for
|
|
493
|
+
* it to reappear when the in-flight promise rejects.
|
|
494
|
+
*/
|
|
495
|
+
readonly submissionGeneration: Ref<number>;
|
|
496
|
+
/**
|
|
497
|
+
* Counts in-flight validation calls across every `validate()` ref and every
|
|
498
|
+
* committing `parse(...)` / `handleSubmit` pre-check; public `validating`
|
|
499
|
+
* mirrors `> 0`. Separate from the submission counters because a debounced
|
|
500
|
+
* field check overlapping a submit must show the union of both.
|
|
501
|
+
*/
|
|
502
|
+
readonly activeValidations: Ref<number>;
|
|
503
|
+
/**
|
|
504
|
+
* `true` once the form has completed at least one validation pass, flipping
|
|
505
|
+
* when `activeValidations` returns to 0 from a positive value. Until then
|
|
506
|
+
* `meta.valid` and `field.valid` report `false` even with an empty schema
|
|
507
|
+
* side, because no errors at frame 1 means "not checked yet", not "checked
|
|
508
|
+
* and clean".
|
|
509
|
+
*
|
|
510
|
+
* The window it closes: a schema with async work gets no verdict from the
|
|
511
|
+
* construction parse (v4 skips it for an async refine or transform; v3 skips
|
|
512
|
+
* it for an async transform and mounts clean when it throws over an async
|
|
513
|
+
* refine), so no construction errors land, and until the queued async pass
|
|
514
|
+
* runs, frame 1 would paint the form valid a tick before the real verdict.
|
|
515
|
+
* A sync-only schema's construction parse seeds its verdict synchronously,
|
|
516
|
+
* before the first read of its errors returns, so its gate starts open. v3
|
|
517
|
+
* reports every `.refine` as async, so a v3 schema with a sync refine
|
|
518
|
+
* starts gated and seeded.
|
|
519
|
+
*
|
|
520
|
+
* `reset()` restores it to the same construction-time seed, so the post-reset
|
|
521
|
+
* window gates exactly like the post-mount one.
|
|
522
|
+
*/
|
|
523
|
+
readonly firstValidationDone: Ref<boolean>;
|
|
524
|
+
/**
|
|
525
|
+
* Async-gate verdict for a path whose canonical key the caller already
|
|
526
|
+
* holds. `key` is required and must correspond to `segments`; skipping the
|
|
527
|
+
* `canonicalizePath` round-trip is what lets a descendant walk, whose Map
|
|
528
|
+
* iteration already yields the key, avoid a canonicalize per leaf.
|
|
529
|
+
*/
|
|
530
|
+
pathHasAsyncValidationByKey(key: PathKey, segments: Path): boolean;
|
|
531
|
+
/**
|
|
532
|
+
* Per-path counter of in-flight field-level validation runs;
|
|
533
|
+
* `FieldState.validating` mirrors `> 0`. Incremented and decremented in
|
|
534
|
+
* lockstep with `activeValidations` inside `scheduleFieldValidation`'s `run`
|
|
535
|
+
* closure, so the two are co-extensive on the field-scheduled branch.
|
|
536
|
+
* Whole-form `validate()` / `parse()` runs have no single path and touch
|
|
537
|
+
* only `activeValidations`.
|
|
538
|
+
*
|
|
539
|
+
* A counter rather than a Set because two runs at one path can overlap: an
|
|
540
|
+
* aborted run's `.finally` lands after its replacement's increment, and
|
|
541
|
+
* `> 0` keeps the field validating across that boundary.
|
|
542
|
+
*
|
|
543
|
+
* Reactive Map, so `.get()` / `.has()` / `.size` track per key and a
|
|
544
|
+
* FieldState computed re-runs only for its own key.
|
|
545
|
+
*/
|
|
546
|
+
readonly fieldValidationCounts: Map<PathKey, number>;
|
|
547
|
+
/**
|
|
548
|
+
* Per-path `Date.now()` stamp for the start of the field's latest validation
|
|
549
|
+
* run, re-anchored on every run start and deleted on the edge back to 0. The
|
|
550
|
+
* display reducer reads it as `ctx.validatingSince` and times the anti-flash
|
|
551
|
+
* spinner off `now - validatingSince`.
|
|
552
|
+
*
|
|
553
|
+
* Re-anchoring per run, not per streak, is the point: a burst of keystrokes
|
|
554
|
+
* keeps pushing the stamp forward and the spinner stays suppressed until the
|
|
555
|
+
* user pauses. Anchoring at the streak start would pin it to the first
|
|
556
|
+
* keystroke, because at `debounceMs: 0` an aborted run's decrement lands
|
|
557
|
+
* after the next run's increment and the count never reaches 0 between fast
|
|
558
|
+
* keystrokes. The container walk takes the descendant minimum, so a row
|
|
559
|
+
* spinner anchors at its earliest still-active leaf.
|
|
560
|
+
*
|
|
561
|
+
* Reactive, and that is load-bearing: the display computed reads this stamp
|
|
562
|
+
* but not the `validating` flag, and a long run that settles on an unchanged
|
|
563
|
+
* verdict leaves `errors` / `valid` untouched. Without reactivity a held
|
|
564
|
+
* `pending` spinner would strand after the run ended, waiting on some
|
|
565
|
+
* unrelated change to re-run the computed. Runtime-only, never hydrated.
|
|
566
|
+
*/
|
|
567
|
+
readonly fieldValidatingSince: Map<PathKey, number>;
|
|
568
|
+
/**
|
|
569
|
+
* Per-path counter of in-flight async-transform runs, the async branch of
|
|
570
|
+
* the `register({ transforms })` pipeline; `> 0` drives
|
|
571
|
+
* `field.transforming` / `field.busy`. A counter for the same overlap reason
|
|
572
|
+
* as `fieldValidationCounts`, except a superseding input releases the prior
|
|
573
|
+
* run synchronously first, so in practice the value is 0 or 1.
|
|
574
|
+
*/
|
|
575
|
+
readonly fieldTransformCounts: Map<PathKey, number>;
|
|
576
|
+
/**
|
|
577
|
+
* Per-path `ssr ? 0 : Date.now()` stamp for the opening of the path's latest
|
|
578
|
+
* async transform, read by the display reducer as `ctx.transformingSince` to
|
|
579
|
+
* time the gated busy spinner. Mirrors `fieldValidatingSince` exactly, down
|
|
580
|
+
* to why it is reactive.
|
|
581
|
+
*/
|
|
582
|
+
readonly fieldTransformingSince: Map<PathKey, number>;
|
|
583
|
+
/**
|
|
584
|
+
* Per-path latest async-transform failure, either a rejected transform or a
|
|
585
|
+
* resolved value the write gate refused, surfaced as `field.transformError`.
|
|
586
|
+
* Cleared when a fresh run opens at the path and on `reset()`. A channel of
|
|
587
|
+
* its own, separate from validation `errors`.
|
|
588
|
+
*/
|
|
589
|
+
readonly transformErrors: Map<PathKey, Error | null>;
|
|
590
|
+
/**
|
|
591
|
+
* Form-wide count of in-flight async-transform runs, behind the
|
|
592
|
+
* `settleTransforms` quiescence guard and the `handleSubmit` drain barrier.
|
|
593
|
+
* Clamped at 0 on release, so a run's own late `endTransform` after a
|
|
594
|
+
* synchronous cancel cannot drive it negative.
|
|
595
|
+
*/
|
|
596
|
+
readonly activeTransforms: Ref<number>;
|
|
597
|
+
/**
|
|
598
|
+
* Replace the form value wholesale. `meta` is forwarded to every
|
|
599
|
+
* `onFormChange` listener so each can decide whether this write is one it
|
|
600
|
+
* cares about, the way history tags a hydration replay.
|
|
601
|
+
*/
|
|
602
|
+
applyFormReplacement(next: F, meta?: WriteMeta): void;
|
|
603
|
+
/**
|
|
604
|
+
* Set a single path's value. `meta` reaches listeners through
|
|
605
|
+
* `applyFormReplacement`; public `form.setValue` passes none.
|
|
606
|
+
*
|
|
607
|
+
* Returns `false` when the slim-primitive gate rejects the write, meaning
|
|
608
|
+
* the value's primitive shape does not match the schema's slim shape at the
|
|
609
|
+
* path. The store is unchanged in that case.
|
|
610
|
+
*/
|
|
611
|
+
setValueAtPath(path: Path, value: unknown, meta?: WriteMeta): boolean;
|
|
612
|
+
getValueAtPath(path: Path): unknown;
|
|
613
|
+
/**
|
|
614
|
+
* Stable identity for the array element at `path`, the token the array
|
|
615
|
+
* engine maintains across structural mutations. Empty for anything that is
|
|
616
|
+
* not an array element (a record entry, a fixed-object field, a container,
|
|
617
|
+
* the root). Backs `FieldState.key`.
|
|
618
|
+
*/
|
|
619
|
+
arrayElementKey(path: Path): string;
|
|
620
|
+
reset(nextDefaultValues?: DeepPartial<WriteShape<F>>): void;
|
|
621
|
+
resetField(path: Path): void;
|
|
622
|
+
setSchemaErrorsForPath(path: Path, errors: ValidationError[]): void;
|
|
623
|
+
setAllSchemaErrors(errors: readonly ValidationError[]): void;
|
|
624
|
+
clearSchemaErrors(path?: Path): void;
|
|
625
|
+
/**
|
|
626
|
+
* Replace the schema side under `path` with `errors`, keying each error by
|
|
627
|
+
* its OWN absolute path. The validation pipelines
|
|
628
|
+
* (`scheduleFieldValidation`, the committing parse, `handleSubmit`, `reset`)
|
|
629
|
+
* commit a parse result through here: entries missing from the new pass drop
|
|
630
|
+
* out of the subtree, and surviving keys update in place so insertion order
|
|
631
|
+
* holds. Pass `[]` for whole-form scope.
|
|
632
|
+
*/
|
|
633
|
+
applySchemaErrorsForSubtree(path: Path, errors: ValidationError[]): void;
|
|
634
|
+
setAllUserErrors(errors: readonly ValidationError[]): void;
|
|
635
|
+
setUserErrorsForPath(path: Path, errors: readonly ValidationError[]): void;
|
|
636
|
+
clearUserErrors(path?: Path): void;
|
|
637
|
+
/**
|
|
638
|
+
* Rebuild the whole tagged store from a snapshot's `[key, cell]` entries,
|
|
639
|
+
* the history ring buffer's restore road. Entry arrays are cloned in so the
|
|
640
|
+
* snapshot stays detached from the live store, and a cell with both sides
|
|
641
|
+
* empty is skipped.
|
|
642
|
+
*/
|
|
643
|
+
restoreErrorCells(entries: ReadonlyArray<readonly [PathKey, ErrorCell]>): void;
|
|
644
|
+
/**
|
|
645
|
+
* Merged read of the cell at `path`, in schema -> blank -> user order:
|
|
646
|
+
* structural validation first, the synthesized blank entry between, user
|
|
647
|
+
* entries last. Same order as `getFirstErrorElement` and the top-level
|
|
648
|
+
* `errors` proxy iterate in.
|
|
649
|
+
*/
|
|
650
|
+
getErrorsForPath(path: Path): ValidationError[];
|
|
651
|
+
/**
|
|
652
|
+
* A stable schema-declaration ordinal for `key`, assigned on first sight.
|
|
653
|
+
* `form.meta.errors` sorts by it, so the aggregate is a function of the SET
|
|
654
|
+
* of errors present rather than of the order their Map keys were last `set`.
|
|
655
|
+
* The construction seed walks every leaf of the schema's slim default;
|
|
656
|
+
* runtime paths (a second DU variant, a dynamic array index, a refine
|
|
657
|
+
* targeting a cross-field path) take first-encounter ordinals and keep them
|
|
658
|
+
* for the form's lifetime.
|
|
659
|
+
*/
|
|
660
|
+
ensurePathOrdinal(key: PathKey): number;
|
|
661
|
+
/**
|
|
662
|
+
* The store's DOM slice, `null` until the directive cluster or `useRegister`
|
|
663
|
+
* arms it through `RegisterValue.ensureDomBinding`. Element registration,
|
|
664
|
+
* host anchors and first-error focus resolution live behind it; eager
|
|
665
|
+
* readers treat `null` as an empty registry.
|
|
666
|
+
*/
|
|
667
|
+
readonly domBinding: ShallowRef<AttaformDomBinding | null>;
|
|
668
|
+
/**
|
|
669
|
+
* Field-record connect transition, driven by the DOM binding on element
|
|
670
|
+
* attach or host connect, and by the SSR-only
|
|
671
|
+
* `markConnectedOptimistically`. Sets `connected: true` and lifts
|
|
672
|
+
* `focused` / `blurred` from `null` to optimistic booleans only while they
|
|
673
|
+
* are null, so a boolean from an early focus event is never clobbered.
|
|
674
|
+
*/
|
|
675
|
+
noteDomConnected(path: Path): void;
|
|
676
|
+
/**
|
|
677
|
+
* Field-record disconnect transition, driven by the DOM binding when a
|
|
678
|
+
* path's last element detaches or its host disconnects. `connected: false`,
|
|
679
|
+
* and `focused` / `blurred` back to `null`, since those describe an element
|
|
680
|
+
* that no longer exists. Interaction history stays.
|
|
681
|
+
*/
|
|
682
|
+
noteDomDisconnected(path: Path): void;
|
|
683
|
+
/**
|
|
684
|
+
* `meta.instance` carries per-`useForm()`-instance overrides for
|
|
685
|
+
* `validateOn` / `debounceMs`, so the blur trigger honours the caller's own
|
|
686
|
+
* config when sibling instances share a FormStore.
|
|
687
|
+
*/
|
|
688
|
+
markFocused(path: Path, focused: boolean, meta?: {
|
|
689
|
+
readonly instance?: WriteMeta['instance'];
|
|
690
|
+
}): void;
|
|
691
|
+
/**
|
|
692
|
+
* Flip the sticky `interacted` bit on a leaf. Driven by the directive's
|
|
693
|
+
* input listeners through `RegisterValue.markInteracted`. Idempotent, and
|
|
694
|
+
* never set by a programmatic write.
|
|
695
|
+
*/
|
|
696
|
+
markInteracted(path: Path): void;
|
|
697
|
+
/**
|
|
698
|
+
* Walk every active-variant leaf under `segments` and flip `touched: true`.
|
|
699
|
+
* Powers `form.touch(path?)`. Idempotent, and touches neither value nor
|
|
700
|
+
* `focused` / `blurred`, nor does it trigger validation.
|
|
701
|
+
*/
|
|
702
|
+
touchAtPath(segments: Path): void;
|
|
703
|
+
/**
|
|
704
|
+
* Walk every active-variant leaf under `segments` and flip the whole
|
|
705
|
+
* interaction ladder (`touched` / `interacted` / `blurredAfterInteraction`),
|
|
706
|
+
* as though the user had focused, edited and left each one. Powers
|
|
707
|
+
* `form.interact(path?)`. Idempotent, and leaves value / `focused` /
|
|
708
|
+
* `blurred` alone. Returns whether any leaf resolved, so the caller can skip
|
|
709
|
+
* validation and dev-warn on an empty path.
|
|
710
|
+
*/
|
|
711
|
+
interactAtPath(segments: Path): boolean;
|
|
712
|
+
/**
|
|
713
|
+
* SSR-only optimistic mark: `connected: true` with no DOM element behind it.
|
|
714
|
+
* The `vRegisterHintTransform` compile-time transform calls it through
|
|
715
|
+
* `RegisterValue.markConnectedOptimistically()` for every element rendered
|
|
716
|
+
* with `v-register`. Idempotent, and a no-op on the client, where the
|
|
717
|
+
* directive's `created` hook is authoritative.
|
|
718
|
+
*/
|
|
719
|
+
markConnectedOptimistically(path: Path): void;
|
|
720
|
+
/**
|
|
721
|
+
* Pristine verdict for a path whose canonical key the caller already holds.
|
|
722
|
+
* `key` is required and must correspond to `segments`; skipping the
|
|
723
|
+
* `canonicalizePath` round-trip is what lets a descendant walk, whose Map
|
|
724
|
+
* iteration already yields the key, avoid a canonicalize per leaf.
|
|
725
|
+
*/
|
|
726
|
+
isPristineAtPathByKey(key: PathKey, segments: Path): boolean;
|
|
727
|
+
/**
|
|
728
|
+
* Whether any tracked array under `path` has changed shape (a reorder,
|
|
729
|
+
* insert or removal) against its construction / reset baseline. The
|
|
730
|
+
* structural half of `dirty`: per-element baselines travel with their
|
|
731
|
+
* element across a mutation, so a positional value comparison alone cannot
|
|
732
|
+
* see the shape change.
|
|
733
|
+
*/
|
|
734
|
+
hasStructuralChangeUnder(path: Path): boolean;
|
|
735
|
+
/**
|
|
736
|
+
* Whether a baseline-present container under `path` was replaced wholesale
|
|
737
|
+
* by a non-container (`setValue('profile', undefined)` and the like) and is
|
|
738
|
+
* still absent. The other half of removal-driven `dirty`: every leaf in such
|
|
739
|
+
* a subtree vanishes at once, so neither the present-leaf walk nor the array
|
|
740
|
+
* tracker sees the loss. Self-filters by current liveness, so a refilled
|
|
741
|
+
* path stops counting.
|
|
742
|
+
*/
|
|
743
|
+
hasRemovedSubtreeUnder(path: Path): boolean;
|
|
744
|
+
getFieldRecord(path: Path): FieldRecord | undefined;
|
|
745
|
+
/**
|
|
746
|
+
* Cancel every in-flight field-level validation run: clear the timers of
|
|
747
|
+
* debounced 'change' runs that have not fired, and latch `aborted` on runs
|
|
748
|
+
* whose async parse is in flight. Called by `handleSubmit` at entry, where
|
|
749
|
+
* submit validation is authoritative, and by `reset()`.
|
|
750
|
+
*/
|
|
751
|
+
cancelFieldValidation(): void;
|
|
752
|
+
/**
|
|
753
|
+
* Open an async-transform run at `key`: bump the run token, increment the
|
|
754
|
+
* in-flight counters, stamp `transformingSince`, clear any prior
|
|
755
|
+
* `transformError` and register `holder` for a later abort. Returns the run
|
|
756
|
+
* token. See `InternalRegisterValue.beginTransform`.
|
|
757
|
+
*/
|
|
758
|
+
beginTransform(key: PathKey, holder: TransformAbortHolder): number;
|
|
759
|
+
/** `true` while `token` is the live async-transform run at `key`. */
|
|
760
|
+
isCurrentTransform(key: PathKey, token: number): boolean;
|
|
761
|
+
/**
|
|
762
|
+
* Close the run `token` at `key`: release the counters, a no-op when a
|
|
763
|
+
* supersede or cancel already released them, and flush settled
|
|
764
|
+
* `settleTransforms` waiters.
|
|
765
|
+
*/
|
|
766
|
+
endTransform(key: PathKey, token: number): void;
|
|
767
|
+
/** Record a per-field normalization failure at `key` (`field.transformError`). */
|
|
768
|
+
setTransformError(key: PathKey, err: Error): void;
|
|
769
|
+
/**
|
|
770
|
+
* Path-scoped counterpart to `cancelTransforms`: abort + release only
|
|
771
|
+
* the runs at-or-under `prefix`, clearing their `transformError`.
|
|
772
|
+
* Called by `resetField`.
|
|
773
|
+
*/
|
|
774
|
+
cancelTransformsUnder(prefix: Path): void;
|
|
775
|
+
/**
|
|
776
|
+
* Resolve once async transforms are quiescent, either form-wide (`path`
|
|
777
|
+
* omitted) or at and under `path`. Resolves, never rejects. See
|
|
778
|
+
* `UseFormReturnType.settleTransforms`.
|
|
779
|
+
*/
|
|
780
|
+
settleTransforms(path?: string | Path): Promise<void>;
|
|
781
|
+
/**
|
|
782
|
+
* Kick off, or schedule, a field-level validation run for `path`. Pass `[]`
|
|
783
|
+
* for whole-form scope, where `applySchemaErrorsForSubtree` replaces every
|
|
784
|
+
* schema-side entry with the adapter's full response. That is the road the
|
|
785
|
+
* construction-time async-refine seed takes.
|
|
786
|
+
*
|
|
787
|
+
* `immediate: true` skips the debounce window and calls the adapter on the
|
|
788
|
+
* next microtask; one-shot triggers use it, while the per-keystroke writers
|
|
789
|
+
* pass `false` so rapid mutations coalesce under `debounceMs`.
|
|
790
|
+
*
|
|
791
|
+
* `instance` carries per-`useForm()`-instance overrides for `validateOn` and
|
|
792
|
+
* `debounceMs`, so sibling instances sharing a FormStore each validate on
|
|
793
|
+
* their own cadence.
|
|
794
|
+
*/
|
|
795
|
+
scheduleFieldValidation(path: Path, immediate: boolean, instance?: WriteMeta['instance']): void;
|
|
796
|
+
/**
|
|
797
|
+
* Subscribe to every `applyFormReplacement`. Fires synchronously once
|
|
798
|
+
* `form.value` holds `next` and all field / originals bookkeeping has run,
|
|
799
|
+
* which is how undo/redo hooks the single mutation funnel. `meta` carries
|
|
800
|
+
* the originating call site's intent and can be ignored. Returns an
|
|
801
|
+
* unsubscribe function.
|
|
802
|
+
*/
|
|
803
|
+
onFormChange(listener: (next: F, meta?: WriteMeta) => void): () => void;
|
|
804
|
+
/**
|
|
805
|
+
* Subscribe to successful submissions. Fires once the consumer's `onSubmit`
|
|
806
|
+
* callback has resolved, so neither a validation failure nor a callback
|
|
807
|
+
* throw reaches it. The DevTools panel rides it to surface a submit event.
|
|
808
|
+
* Returns an unsubscribe function.
|
|
809
|
+
*/
|
|
810
|
+
onSubmitSuccess(listener: () => void): () => void;
|
|
811
|
+
/**
|
|
812
|
+
* Subscribe to `reset()`. Fires after reset has replaced the form and
|
|
813
|
+
* cleared errors and lifecycle, so a listener sees the post-reset state.
|
|
814
|
+
* The history module uses it to drop the undo/redo stack. Returns an
|
|
815
|
+
* unsubscribe function.
|
|
816
|
+
*/
|
|
817
|
+
onReset(listener: () => void): () => void;
|
|
818
|
+
/**
|
|
819
|
+
* Notify submit-success subscribers. `handleSubmit` in `process-form.ts`
|
|
820
|
+
* calls it once the consumer callback has resolved. Not for consumer use.
|
|
821
|
+
*/
|
|
822
|
+
emitSubmitSuccess(): void;
|
|
823
|
+
/**
|
|
824
|
+
* Register a teardown bound to the FormStore's own lifetime rather than to a
|
|
825
|
+
* consumer's Vue effect scope; `dispose()` runs it when the last consumer
|
|
826
|
+
* unmounts. The history wiring needs this, so its subscribers survive one
|
|
827
|
+
* consumer unmounting while others remain.
|
|
828
|
+
*/
|
|
829
|
+
registerCleanup(fn: () => void): void;
|
|
830
|
+
/**
|
|
831
|
+
* Cache for per-state modules, chiefly history, that must outlive any single
|
|
832
|
+
* consumer. Later `useForm` / `injectForm` calls for the same key read from
|
|
833
|
+
* here, so the public API shape does not depend on mount order. Keyed by a
|
|
834
|
+
* caller-owned string such as `'history'`.
|
|
835
|
+
*/
|
|
836
|
+
readonly modules: Map<string, unknown>;
|
|
837
|
+
/**
|
|
838
|
+
* Whether schema-driven coercion runs for this form, `false` only when the
|
|
839
|
+
* consumer passed `useForm({ coerce: false })`. `buildCoerceFn` reads it at
|
|
840
|
+
* `register()` time to bake the per-path closure on `RegisterValue.coerce`.
|
|
841
|
+
*/
|
|
842
|
+
readonly coerceEnabled: boolean;
|
|
843
|
+
/**
|
|
844
|
+
* Tear down the non-reactive resources this FormStore owns. The registry
|
|
845
|
+
* calls it when the last consumer unmounts: pending field-validation timers
|
|
846
|
+
* are cancelled, every subscriber is dropped, and each `registerCleanup`
|
|
847
|
+
* hook fires.
|
|
848
|
+
*/
|
|
849
|
+
dispose(): void;
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Public types for `useWizard`, the multistep-form orchestrator.
|
|
854
|
+
*
|
|
855
|
+
* A wizard is an ordered list of step slots. Each slot resolves to a
|
|
856
|
+
* participating form: a `useForm` reference, a bare string key
|
|
857
|
+
* (desugared to a noop form so affordance steps participate
|
|
858
|
+
* uniformly), a function slot for runtime branching, or a `lazy()`
|
|
859
|
+
* function slot that caches its resolution.
|
|
860
|
+
*
|
|
861
|
+
* The read surface is string-keyed (`Record<FormKey, ...>`), because a
|
|
862
|
+
* flow threaded through `injectWizard` has no lexical key knowledge.
|
|
863
|
+
* Typed per-form access flows back through the original form refs and
|
|
864
|
+
* through `wizard.handleSubmit`'s `ctx.get(formRef)` accessor.
|
|
865
|
+
*/
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Minimum shape the wizard requires from a participating form. The
|
|
869
|
+
* wizard routes by `key` at runtime and hands back the original form
|
|
870
|
+
* objects untouched, so it never constrains their value shapes.
|
|
871
|
+
*/
|
|
872
|
+
type AnyForm = {
|
|
873
|
+
readonly key: FormKey;
|
|
874
|
+
};
|
|
875
|
+
/**
|
|
876
|
+
* Per-form summary at `wizard.statuses[key]`, and what `defaultStatuses`
|
|
877
|
+
* seeds. This is the cross-step rollup built for templates
|
|
878
|
+
* (`{{ wizard.statuses.cargo.valid }}`); `form.meta` carries the full
|
|
879
|
+
* per-form lifecycle surface.
|
|
880
|
+
*
|
|
881
|
+
* A noop form generated for a string slot reads as default-valid,
|
|
882
|
+
* unless that slot is a `gate('...')`, whose `gate` tracks the
|
|
883
|
+
* in-session acknowledgment.
|
|
884
|
+
*/
|
|
885
|
+
type FormStatus = {
|
|
886
|
+
/** `form.meta.valid`. `false` while errors exist, or before the first validation pass. */
|
|
887
|
+
readonly valid: boolean;
|
|
888
|
+
/** `form.meta.dirty`. `true` once any value differs from the original defaults. */
|
|
889
|
+
readonly dirty: boolean;
|
|
890
|
+
/**
|
|
891
|
+
* `form.meta.submitted`. `true` once a `handleSubmit` callback has
|
|
892
|
+
* resolved without throwing. A failed submit leaves it `false`;
|
|
893
|
+
* `submissionAttempts > 0` is the "user has tried" signal.
|
|
894
|
+
*/
|
|
895
|
+
readonly submitted: boolean;
|
|
896
|
+
/** `form.meta.errorCount`. Active validation errors, zero when valid. */
|
|
897
|
+
readonly errorCount: number;
|
|
898
|
+
/**
|
|
899
|
+
* `true` when this step sits after an uncleared gate: its form is
|
|
900
|
+
* frozen and navigation to it is refused, so a stepper can render it
|
|
901
|
+
* as unreachable.
|
|
902
|
+
*
|
|
903
|
+
* Orthogonal to `gate`. The first uncleared gate reads
|
|
904
|
+
* `locked: false`, since you must reach it to clear it; a later gate
|
|
905
|
+
* sealed behind it reads both `gate: 'uncleared'` and `locked: true`.
|
|
906
|
+
*/
|
|
907
|
+
readonly locked: boolean;
|
|
908
|
+
/**
|
|
909
|
+
* This step's own role as a hard prerequisite. `null` when the step is
|
|
910
|
+
* not a `gate()`, `'uncleared'` while its form is unconfirmed (so it
|
|
911
|
+
* seals every later step), `'cleared'` once confirmed by a clean
|
|
912
|
+
* submit or seeded via `defaultStatuses`.
|
|
913
|
+
*
|
|
914
|
+
* Reflects the live compiled shape, so a conditional `gate()` that a
|
|
915
|
+
* function slot drops resolves to `null`.
|
|
916
|
+
*/
|
|
917
|
+
readonly gate: 'cleared' | 'uncleared' | null;
|
|
918
|
+
};
|
|
919
|
+
/**
|
|
920
|
+
* Seed accepted by `useWizard({ defaultStatuses })`. An omitted field
|
|
921
|
+
* falls back to the pending sentinel on read.
|
|
922
|
+
*
|
|
923
|
+
* `locked` is not accepted: it is derived live from the wizard's gates
|
|
924
|
+
* and overlaid on every status read, so a seeded value would be ignored.
|
|
925
|
+
*/
|
|
926
|
+
type FormStatusSeed = {
|
|
927
|
+
readonly valid?: boolean;
|
|
928
|
+
readonly dirty?: boolean;
|
|
929
|
+
readonly submitted?: boolean;
|
|
930
|
+
readonly errorCount?: number;
|
|
931
|
+
/**
|
|
932
|
+
* Write-only, and not echoed back on read. `'cleared'` latches the
|
|
933
|
+
* gate's cleared state once at construction, for restoring a
|
|
934
|
+
* confirmed prerequisite; the live `gate` overlay then owns the read.
|
|
935
|
+
* `'uncleared'` is the default and seeds nothing. Only a step that
|
|
936
|
+
* compiles to a `gate()` honors it.
|
|
937
|
+
*/
|
|
938
|
+
readonly gate?: 'cleared' | 'uncleared';
|
|
939
|
+
};
|
|
940
|
+
/**
|
|
941
|
+
* One error from `wizard.allErrors[key]`. Carries the formKey and path
|
|
942
|
+
* so a wizard-wide error summary can route to the offending field.
|
|
943
|
+
*/
|
|
944
|
+
type WizardAggregateError = {
|
|
945
|
+
readonly formKey: FormKey;
|
|
946
|
+
readonly path: ReadonlyArray<string | number>;
|
|
947
|
+
readonly message: string;
|
|
948
|
+
readonly code?: string;
|
|
949
|
+
};
|
|
950
|
+
/**
|
|
951
|
+
* The per-key `FormStatus` record, readable three ways:
|
|
952
|
+
* `wizard.statuses.cargo.valid`, `wizard.statuses('cargo')`, or
|
|
953
|
+
* `wizard.statuses()` for the whole record.
|
|
954
|
+
*/
|
|
955
|
+
type WizardStatusesProxy<S extends Record<string, FormStatus>> = ((key?: keyof S) => FormStatus | S) & Readonly<S>;
|
|
956
|
+
/**
|
|
957
|
+
* One compiled position in the flow, as exposed by `wizard.steps`.
|
|
958
|
+
* String slots desugar to noop forms before compilation, so every
|
|
959
|
+
* compiled step carries a `form` whatever its source kind.
|
|
960
|
+
*/
|
|
961
|
+
type CompiledStep = {
|
|
962
|
+
readonly key: FormKey;
|
|
963
|
+
readonly form: AnyForm;
|
|
964
|
+
};
|
|
965
|
+
/**
|
|
966
|
+
* A participating form as seen from a function slot's `ctx.forms[key]`.
|
|
967
|
+
* Adds `values` to `AnyForm` so routing decisions can read live state.
|
|
968
|
+
*
|
|
969
|
+
* Values are loose here. For typed access inside a slot body, close
|
|
970
|
+
* over the original form ref instead of routing through `ctx.forms`.
|
|
971
|
+
*/
|
|
972
|
+
type WizardCtxForm = AnyForm & {
|
|
973
|
+
readonly values: Readonly<Record<string, unknown>>;
|
|
974
|
+
};
|
|
975
|
+
/**
|
|
976
|
+
* Context passed to function slots in the `steps` array. `forms` holds
|
|
977
|
+
* the statically-known forms: every top-level `AnyForm` slot plus every
|
|
978
|
+
* noop form synthesized for a top-level string slot.
|
|
979
|
+
*
|
|
980
|
+
* Function slots re-evaluate reactively when the values they read
|
|
981
|
+
* mutate, typically `ctx.forms.<key>.values.<path>`. The `forms`
|
|
982
|
+
* accumulator keeps its identity across re-evaluations. Keep slot
|
|
983
|
+
* bodies free of side effects; they are routing decisions.
|
|
984
|
+
*/
|
|
985
|
+
type WizardCtx = {
|
|
986
|
+
readonly forms: Readonly<Record<FormKey, WizardCtxForm>>;
|
|
987
|
+
readonly currentKey: FormKey | undefined;
|
|
988
|
+
};
|
|
989
|
+
/**
|
|
990
|
+
* What a function slot or a `lazy()` / `gate()` resolver may yield: a
|
|
991
|
+
* participating form, a bare affordance key, a nested `lazy()` /
|
|
992
|
+
* `gate()` wrapper (so the two compose in either order), or `null` /
|
|
993
|
+
* `undefined` to drop the position from the compiled list.
|
|
994
|
+
*/
|
|
995
|
+
type SlotResolution<Ctx = WizardCtx> = AnyForm | string | null | undefined | LazyMarker<Ctx> | GateMarker;
|
|
996
|
+
/**
|
|
997
|
+
* Phantom brand for `LazyMarker`. The runtime brand symbol lives in
|
|
998
|
+
* `core/wizard-lazy.ts`; this declaration keeps the marker unforgeable
|
|
999
|
+
* without a circular import.
|
|
1000
|
+
*/
|
|
1001
|
+
declare const _lazyBrand: unique symbol;
|
|
1002
|
+
/**
|
|
1003
|
+
* Marker returned by `lazy((ctx) => ...)`, which gives a function slot
|
|
1004
|
+
* its own memoization cache: the resolver fires once on the first
|
|
1005
|
+
* compile pass and stays cached until one of its own tracked reactive
|
|
1006
|
+
* reads changes, or `wizard.reset()` invalidates it. An unrelated
|
|
1007
|
+
* slot's deps changing does not re-fire it, so heavy or one-shot
|
|
1008
|
+
* lookups stay one-shot.
|
|
1009
|
+
*
|
|
1010
|
+
* Construct via the `lazy()` helper exported alongside `useWizard`. The
|
|
1011
|
+
* marker is opaque; consumers do not assemble it directly.
|
|
1012
|
+
*/
|
|
1013
|
+
type LazyMarker<Ctx = WizardCtx> = {
|
|
1014
|
+
readonly [_lazyBrand]: true;
|
|
1015
|
+
readonly resolve: (ctx: Ctx) => SlotResolution<Ctx>;
|
|
1016
|
+
};
|
|
1017
|
+
/**
|
|
1018
|
+
* Phantom brand for `GateMarker`. The runtime brand symbol lives in
|
|
1019
|
+
* `core/wizard-gate.ts`; this declaration keeps the marker unforgeable
|
|
1020
|
+
* without a circular import.
|
|
1021
|
+
*/
|
|
1022
|
+
declare const _gateBrand: unique symbol;
|
|
1023
|
+
/**
|
|
1024
|
+
* Marker returned by `gate(step)`, which makes a slot a hard
|
|
1025
|
+
* prerequisite: an uncleared gate seals every step after it, and the
|
|
1026
|
+
* gate's own form is the only way past. A gate clears on a member
|
|
1027
|
+
* form's clean submit (confirmation), never on a value merely going
|
|
1028
|
+
* valid (intent), and its form freezes once cleared so a back
|
|
1029
|
+
* navigation is a read-only review.
|
|
1030
|
+
*
|
|
1031
|
+
* `Inner` carries the wrapped slot's static type, so `gate` is
|
|
1032
|
+
* transparent to the type machinery: `gate(form)` still contributes the
|
|
1033
|
+
* form's key to `wizard.forms` and still counts as a guaranteed step,
|
|
1034
|
+
* while `gate(lazy(fn))` stays maybe-absent exactly like the lazy slot
|
|
1035
|
+
* it wraps. See {@link UnwrapGate}.
|
|
1036
|
+
*
|
|
1037
|
+
* Construct via the `gate()` helper exported alongside `useWizard`. The
|
|
1038
|
+
* marker is opaque; consumers do not assemble it directly.
|
|
1039
|
+
*/
|
|
1040
|
+
type GateMarker<Inner = unknown> = {
|
|
1041
|
+
readonly [_gateBrand]: true;
|
|
1042
|
+
readonly inner: Inner;
|
|
1043
|
+
};
|
|
1044
|
+
/**
|
|
1045
|
+
* Strip every `gate()` wrapper off a slot type. A gate changes runtime
|
|
1046
|
+
* reachability, never the compiled type, so the forms map and the
|
|
1047
|
+
* non-empty-tuple predicate both unwrap before inspecting a slot.
|
|
1048
|
+
* Recursive, which is what makes `gate(lazy(s))` and
|
|
1049
|
+
* `lazy((ctx) => gate(s))` agree at the type level as they do at
|
|
1050
|
+
* runtime.
|
|
1051
|
+
*/
|
|
1052
|
+
type UnwrapGate<T> = T extends GateMarker<infer Inner> ? UnwrapGate<Inner> : T;
|
|
1053
|
+
/**
|
|
1054
|
+
* One position in the source `useWizard({ steps })` array. Each slot
|
|
1055
|
+
* compiles to a `{ key, form }` step, or drops out:
|
|
1056
|
+
*
|
|
1057
|
+
* - `AnyForm`: a form declared via `useForm`. Surfaced as-is.
|
|
1058
|
+
* - `string`: a bare key. The wizard generates a noop form so
|
|
1059
|
+
* affordance positions (intro, terms, review, congratulations) look
|
|
1060
|
+
* the same from outside as real steps.
|
|
1061
|
+
* - `null` / `undefined`: dropped from the compiled list, so a
|
|
1062
|
+
* conditional step reads inline as `cond ? form : null`.
|
|
1063
|
+
* - function: eager slot, re-evaluated reactively. Returns a
|
|
1064
|
+
* `SlotResolution`, or nullish to drop the position.
|
|
1065
|
+
* - `LazyMarker`: memoized function slot, see `lazy`.
|
|
1066
|
+
* - `GateMarker`: hard-prerequisite wrapper, see `gate`. Wraps any of
|
|
1067
|
+
* the above.
|
|
1068
|
+
*/
|
|
1069
|
+
type StepSlot<Ctx = WizardCtx> = AnyForm | string | null | undefined | ((ctx: Ctx) => SlotResolution<Ctx>) | LazyMarker<Ctx> | GateMarker;
|
|
1070
|
+
/** What the `restore` callback returns: the key of the step to activate. */
|
|
1071
|
+
type WizardRestoreState = {
|
|
1072
|
+
readonly step?: FormKey;
|
|
1073
|
+
};
|
|
1074
|
+
/**
|
|
1075
|
+
* `restore` callback. Invoked at construction and watched reactively,
|
|
1076
|
+
* so browser back/forward, cross-tab events and route changes re-apply
|
|
1077
|
+
* through the wizard. Returning `undefined` falls through to the first
|
|
1078
|
+
* step.
|
|
1079
|
+
*/
|
|
1080
|
+
type WizardRestoreFn = () => WizardRestoreState | undefined;
|
|
1081
|
+
/**
|
|
1082
|
+
* `persist` callback. Invoked when `wizard.currentStep` actually moves;
|
|
1083
|
+
* the wizard diffs against the last persisted value to break the
|
|
1084
|
+
* restore-persist loop.
|
|
1085
|
+
*/
|
|
1086
|
+
type WizardPersistFn = (state: WizardRestoreState) => void;
|
|
1087
|
+
/**
|
|
1088
|
+
* Context passed to the `onSubmit` registered via
|
|
1089
|
+
* `wizard.handleSubmit`. That call always processes the whole step
|
|
1090
|
+
* list, so `values` carries every form's parsed output no matter which
|
|
1091
|
+
* step fired the submit.
|
|
1092
|
+
*/
|
|
1093
|
+
type WizardSubmitContext = {
|
|
1094
|
+
/**
|
|
1095
|
+
* Namespaced aggregate keyed by form key, mirroring
|
|
1096
|
+
* `wizard.allValues`. Noop forms contribute an empty record.
|
|
1097
|
+
*/
|
|
1098
|
+
readonly values: Readonly<Record<FormKey, unknown>>;
|
|
1099
|
+
/**
|
|
1100
|
+
* Typed parsed output for a specific form ref. Works across
|
|
1101
|
+
* cross-component graphs, because the ref carries its own schema.
|
|
1102
|
+
*/
|
|
1103
|
+
readonly get: <F extends AnyForm>(form: F) => F extends {
|
|
1104
|
+
readonly values: infer V;
|
|
1105
|
+
} ? V : unknown;
|
|
1106
|
+
/** Key of the step that fired this submission. */
|
|
1107
|
+
readonly currentKey: FormKey;
|
|
1108
|
+
/**
|
|
1109
|
+
* `true` when `currentKey` is the last position in `wizard.steps`.
|
|
1110
|
+
* Positional only: it reports where the submit fired, never what got
|
|
1111
|
+
* validated. A user who steps back, edits and submits from the middle
|
|
1112
|
+
* sees `false`, yet the whole list is still processed and `done` still
|
|
1113
|
+
* latches on success.
|
|
1114
|
+
*/
|
|
1115
|
+
readonly isFinal: boolean;
|
|
1116
|
+
};
|
|
1117
|
+
/**
|
|
1118
|
+
* `onSubmit` registered via `wizard.handleSubmit`. Sync or async; the
|
|
1119
|
+
* returned promise gates `wizard.submitting`.
|
|
1120
|
+
*/
|
|
1121
|
+
type WizardOnSubmit = (ctx: WizardSubmitContext) => void | Promise<void>;
|
|
1122
|
+
/**
|
|
1123
|
+
* Optional `onError` registered via `wizard.handleSubmit`. Receives
|
|
1124
|
+
* errors spanning EVERY step, since `handleSubmit` validates the whole
|
|
1125
|
+
* wizard, so a failed submit surfaces every form's errors at once
|
|
1126
|
+
* rather than only the active step's. Entries come from per-form
|
|
1127
|
+
* validation, activation failures (`atta:activation-failed`), and a
|
|
1128
|
+
* submit callback that left errors on a processed step via the
|
|
1129
|
+
* `setErrors(...); return` server-rejection path. Sync or async; the
|
|
1130
|
+
* returned promise gates `wizard.submitting`.
|
|
1131
|
+
*/
|
|
1132
|
+
type WizardOnError = (errors: readonly WizardAggregateError[]) => void | Promise<void>;
|
|
1133
|
+
/** Options for `useWizard({ steps, ... })`. Only `steps` is required. */
|
|
1134
|
+
type WizardOptions = {
|
|
1135
|
+
/**
|
|
1136
|
+
* Ordered list of slots that compile into the positional step list.
|
|
1137
|
+
* See `StepSlot` for the per-slot contract.
|
|
1138
|
+
*/
|
|
1139
|
+
readonly steps: ReadonlyArray<StepSlot>;
|
|
1140
|
+
/**
|
|
1141
|
+
* Identifier this wizard registers under, so descendants reach it
|
|
1142
|
+
* with `injectWizard(key)` instead of prop-threading. Omit it and the
|
|
1143
|
+
* wizard gets a synthetic key from `useId()`, so the server-rendered
|
|
1144
|
+
* and client-hydrated trees agree on one registry entry; that key is
|
|
1145
|
+
* opaque, and descendants reach an anonymous wizard through ambient
|
|
1146
|
+
* `injectWizard()`.
|
|
1147
|
+
*
|
|
1148
|
+
* Registering a key twice is first-wins, with a dev warning on the
|
|
1149
|
+
* second, mirroring `useForm`. The warning fires only for explicit
|
|
1150
|
+
* keys, since two anonymous wizards are always given distinct keys.
|
|
1151
|
+
*/
|
|
1152
|
+
readonly key?: string;
|
|
1153
|
+
/**
|
|
1154
|
+
* Seed statuses used while a form is still pre-resolved: async
|
|
1155
|
+
* `defaultValues` in flight, or a wizard-deferred non-current step.
|
|
1156
|
+
* Mirrors `defaultValues`' trichotomy of plain object, sync factory
|
|
1157
|
+
* or async factory.
|
|
1158
|
+
*
|
|
1159
|
+
* Per form, the first of these that applies wins:
|
|
1160
|
+
* 1. defaults resolved, so derive from `form.meta`
|
|
1161
|
+
* 2. noop form, so the built-in always-valid status
|
|
1162
|
+
* 3. a seed for this key
|
|
1163
|
+
* 4. the pending sentinel
|
|
1164
|
+
*
|
|
1165
|
+
* `gate: 'cleared'` is honored as a one-shot seed of the gate's
|
|
1166
|
+
* cleared latch at construction, for restoring a confirmed
|
|
1167
|
+
* prerequisite, and only on a step that compiles to a `gate()`. See
|
|
1168
|
+
* `FormStatusSeed` for what the seed does and does not carry.
|
|
1169
|
+
*
|
|
1170
|
+
* SSR note: an async factory resolves after hydration, so a gate
|
|
1171
|
+
* seeded through it clears client-side. For a gate that must render
|
|
1172
|
+
* open on the first server byte, use the plain or sync-factory form
|
|
1173
|
+
* and resolve server truth in the page's `setup` await.
|
|
1174
|
+
*
|
|
1175
|
+
* Unknown keys dev-warn, so a stale resume payload surfaces at
|
|
1176
|
+
* construction.
|
|
1177
|
+
*
|
|
1178
|
+
* A factory that throws, or a promise that rejects, seeds nothing and
|
|
1179
|
+
* reports once in development, the same as omitting the option.
|
|
1180
|
+
*/
|
|
1181
|
+
readonly defaultStatuses?: Record<string, FormStatusSeed> | (() => Record<string, FormStatusSeed>) | (() => Promise<Record<string, FormStatusSeed>>);
|
|
1182
|
+
/**
|
|
1183
|
+
* Override for `wizard.progress`. Omitted, progress is
|
|
1184
|
+
* `valid_step_count / count`, normalised to `[0, 1]`. Provided, the
|
|
1185
|
+
* returned number is used as-is and normalising it is yours to do.
|
|
1186
|
+
*
|
|
1187
|
+
* Invoked inside a `computed`, so it must be synchronous and may only
|
|
1188
|
+
* read reactive sources. A throw falls back to the built-in ratio and
|
|
1189
|
+
* reports once in development.
|
|
1190
|
+
*/
|
|
1191
|
+
readonly progress?: (steps: ReadonlyArray<CompiledStep>) => number;
|
|
1192
|
+
/**
|
|
1193
|
+
* When `wizard.handleSubmit` finds errors, jump to the first failing
|
|
1194
|
+
* form's step and invoke its `applyInvalidSubmitPolicy()`, which
|
|
1195
|
+
* honors that form's own `focusOnInvalidSubmit` choice. Default
|
|
1196
|
+
* `true`. Pass `false` to leave the active step where the user left
|
|
1197
|
+
* it and navigate yourself from `onError`.
|
|
1198
|
+
*/
|
|
1199
|
+
readonly focusFirstError?: boolean;
|
|
1200
|
+
/**
|
|
1201
|
+
* Source of truth for the active step, invoked at construction and
|
|
1202
|
+
* re-evaluated reactively. The default reads `?step=<key>` from the
|
|
1203
|
+
* URL. Pass `false` to disable URL sync, or your own callback for
|
|
1204
|
+
* non-router persistence.
|
|
1205
|
+
*
|
|
1206
|
+
* A throw yields no step, so the wizard stays where it is, and reports
|
|
1207
|
+
* once in development.
|
|
1208
|
+
*/
|
|
1209
|
+
readonly restore?: WizardRestoreFn | false;
|
|
1210
|
+
/**
|
|
1211
|
+
* Destination for the active step, invoked whenever `currentStep`
|
|
1212
|
+
* moves. The default writes `?step=<key>`. Pass `false` to disable
|
|
1213
|
+
* persistence, or your own callback to scope the param name or write
|
|
1214
|
+
* elsewhere.
|
|
1215
|
+
*
|
|
1216
|
+
* A throw does not block the navigation that triggered it, so the step
|
|
1217
|
+
* is live but unrecorded, and reports once in development.
|
|
1218
|
+
*/
|
|
1219
|
+
readonly persist?: WizardPersistFn | false;
|
|
1220
|
+
};
|
|
1221
|
+
/**
|
|
1222
|
+
* True when a slot is guaranteed to contribute exactly one compiled
|
|
1223
|
+
* step: a bare form or affordance string, neither nullish nor a
|
|
1224
|
+
* maybe-absent function / `lazy()` slot. One of these anywhere in the
|
|
1225
|
+
* tuple proves the compiled list is non-empty.
|
|
1226
|
+
*/
|
|
1227
|
+
type IsGuaranteedStep<T, U = UnwrapGate<T>> = [null] extends [U] ? false : [undefined] extends [U] ? false : U extends LazyMarker | ((...args: unknown[]) => unknown) ? false : U extends string ? true : U extends {
|
|
1228
|
+
readonly key: string;
|
|
1229
|
+
} ? true : false;
|
|
1230
|
+
/**
|
|
1231
|
+
* Is the steps tuple statically guaranteed to compile to a non-empty
|
|
1232
|
+
* list? True when at least one element is an `IsGuaranteedStep`.
|
|
1233
|
+
* Function and `lazy()` slots may resolve to nothing, and nullish slots
|
|
1234
|
+
* are an explicit drop, so a tuple of only those stays honestly
|
|
1235
|
+
* `| undefined`.
|
|
1236
|
+
*
|
|
1237
|
+
* This is what narrows `currentStep` and `activeForm` in the
|
|
1238
|
+
* common-case wizard while keeping the union wherever a runtime drop is
|
|
1239
|
+
* reachable.
|
|
1240
|
+
*/
|
|
1241
|
+
type StaticallyNonEmpty<S> = S extends readonly [infer First, ...infer Rest] ? IsGuaranteedStep<First> extends true ? true : StaticallyNonEmpty<Rest> : false;
|
|
1242
|
+
/** Active step's key, narrowed to `string` when `S` is statically safe. */
|
|
1243
|
+
type CurrentStepOf<S> = StaticallyNonEmpty<S> extends true ? FormKey : FormKey | undefined;
|
|
1244
|
+
/**
|
|
1245
|
+
* Active step's form handle, schema-erased to
|
|
1246
|
+
* `UseFormReturnType<GenericForm>` and narrowed to non-`undefined` when
|
|
1247
|
+
* `S` is statically safe. Values are loose because the active step's
|
|
1248
|
+
* schema is not statically known; reach for the original form ref or
|
|
1249
|
+
* `ctx.get(ref)` for typed per-step values.
|
|
1250
|
+
*/
|
|
1251
|
+
type ActiveFormOf<S> = StaticallyNonEmpty<S> extends true ? UseFormReturnType<GenericForm> : UseFormReturnType<GenericForm> | undefined;
|
|
1252
|
+
/**
|
|
1253
|
+
* One slot's contribution to {@link FormsRecordOf}. Unwraps `gate()`
|
|
1254
|
+
* and strips nullish off the slot first, so a conditionally-present
|
|
1255
|
+
* form and a `gate(form)` both still map to their key and stay
|
|
1256
|
+
* concretely typed on `wizard.forms`. A purely nullish slot contributes
|
|
1257
|
+
* nothing.
|
|
1258
|
+
*/
|
|
1259
|
+
type SlotRecord<First, U = UnwrapGate<First>, F = Exclude<U, null | undefined>> = [F] extends [
|
|
1260
|
+
never
|
|
1261
|
+
] ? unknown : F extends string ? {
|
|
1262
|
+
readonly [P in F]: AnyForm;
|
|
1263
|
+
} : F extends {
|
|
1264
|
+
readonly key: infer K extends string;
|
|
1265
|
+
} ? {
|
|
1266
|
+
readonly [P in K]: F;
|
|
1267
|
+
} : unknown;
|
|
1268
|
+
/**
|
|
1269
|
+
* Builds the static portion of `wizard.forms` by walking the steps
|
|
1270
|
+
* tuple. Per slot kind:
|
|
1271
|
+
*
|
|
1272
|
+
* - **string** (`'review'`): the literal is the record key, and the
|
|
1273
|
+
* value is `AnyForm`, since the synthesized noop form is opaque at
|
|
1274
|
+
* the type level.
|
|
1275
|
+
* - **form**: the form's own `key` is the record key and the value is
|
|
1276
|
+
* the concrete handle, so `wizard.forms.shipping.values.address`
|
|
1277
|
+
* carries schema-derived field types through. A form behind a
|
|
1278
|
+
* `cond ? form : null` still maps to its key.
|
|
1279
|
+
* - **nullish**: contributes nothing.
|
|
1280
|
+
* - **function / `lazy()`**: contributes nothing statically.
|
|
1281
|
+
* Runtime-resolved forms stay reachable as `AnyForm` through the
|
|
1282
|
+
* catch-all index signature on `WizardForms`.
|
|
1283
|
+
* - **`gate()`**: unwrapped, so it contributes exactly what the slot
|
|
1284
|
+
* it wraps would.
|
|
1285
|
+
*/
|
|
1286
|
+
type FormsRecordOf<S> = S extends readonly [
|
|
1287
|
+
infer First,
|
|
1288
|
+
...infer Rest extends ReadonlyArray<StepSlot>
|
|
1289
|
+
] ? SlotRecord<First> & FormsRecordOf<Rest> : unknown;
|
|
1290
|
+
/**
|
|
1291
|
+
* `wizard.forms` typed view: the static per-step map, intersected with
|
|
1292
|
+
* a catch-all so a statically known key resolves to its concrete form
|
|
1293
|
+
* type and any other string key resolves to `AnyForm`.
|
|
1294
|
+
*/
|
|
1295
|
+
type WizardForms<S> = FormsRecordOf<S> & Readonly<Record<FormKey, AnyForm>>;
|
|
1296
|
+
/**
|
|
1297
|
+
* What `useWizard({ steps, ... })` returns. Every reactive read is a
|
|
1298
|
+
* plain getter, no `.value`, so `wizard.currentStep`, `wizard.progress`
|
|
1299
|
+
* and `wizard.allValues` track inside `computed` and templates
|
|
1300
|
+
* directly.
|
|
1301
|
+
*
|
|
1302
|
+
* Parameterized by the steps tuple `S` so the active-position fields
|
|
1303
|
+
* narrow to non-`undefined` when every slot is positional, and stay
|
|
1304
|
+
* honest unions when a function or `lazy()` slot can drop the position
|
|
1305
|
+
* at runtime. `useWizard`'s `const` type parameter preserves the
|
|
1306
|
+
* literal tuple, so that narrowing happens without a consumer-side
|
|
1307
|
+
* `as const`.
|
|
1308
|
+
*/
|
|
1309
|
+
type UseWizardReturnType<S extends ReadonlyArray<StepSlot> = ReadonlyArray<StepSlot>> = {
|
|
1310
|
+
readonly key: string;
|
|
1311
|
+
/**
|
|
1312
|
+
* Key of the active step. Narrows to `string` when the steps tuple is
|
|
1313
|
+
* statically guaranteed non-empty; otherwise `string | undefined`, so
|
|
1314
|
+
* an empty compiled list surfaces honestly.
|
|
1315
|
+
*/
|
|
1316
|
+
readonly currentStep: CurrentStepOf<S>;
|
|
1317
|
+
/**
|
|
1318
|
+
* A live view of the active step's form, so operating through it
|
|
1319
|
+
* always targets the current step: a handler captured once at setup
|
|
1320
|
+
* (`wizard.activeForm.handleSubmit(() => wizard.next())`) retargets as
|
|
1321
|
+
* the wizard advances. It is a facade, not the raw per-step handle;
|
|
1322
|
+
* reach for `wizard.forms[key]` when you need identity. Same
|
|
1323
|
+
* `undefined` narrowing as `currentStep`.
|
|
1324
|
+
*/
|
|
1325
|
+
readonly activeForm: ActiveFormOf<S>;
|
|
1326
|
+
/** 0-based position of the active step. */
|
|
1327
|
+
readonly activeIndex: number;
|
|
1328
|
+
/** `true` when `currentStep` is the last compiled step. */
|
|
1329
|
+
readonly isFinalStep: boolean;
|
|
1330
|
+
/** The ordered compiled `{ key, form }` slots. */
|
|
1331
|
+
readonly steps: ReadonlyArray<CompiledStep>;
|
|
1332
|
+
/** Every step's form, indexable by step key. */
|
|
1333
|
+
readonly forms: WizardForms<S>;
|
|
1334
|
+
/** `steps.length`. */
|
|
1335
|
+
readonly count: number;
|
|
1336
|
+
/**
|
|
1337
|
+
* Callable readonly proxy over the per-key `FormStatus` record.
|
|
1338
|
+
* Noop-form keys always read as default-valid.
|
|
1339
|
+
*/
|
|
1340
|
+
readonly statuses: WizardStatusesProxy<Record<string, FormStatus>>;
|
|
1341
|
+
/** Each form's values, keyed by step key. */
|
|
1342
|
+
readonly allValues: Readonly<Record<FormKey, unknown>>;
|
|
1343
|
+
/** Each form's validation errors, keyed by step key. Noop forms map to an empty list. */
|
|
1344
|
+
readonly allErrors: Readonly<Record<FormKey, readonly WizardAggregateError[]>>;
|
|
1345
|
+
/**
|
|
1346
|
+
* Normalised step-validity ratio, or the `progress` override's
|
|
1347
|
+
* return. Forward-looking: noop steps count as valid.
|
|
1348
|
+
*/
|
|
1349
|
+
readonly progress: number;
|
|
1350
|
+
/**
|
|
1351
|
+
* `true` when `activeIndex < count - 1`. Purely positional;
|
|
1352
|
+
* navigation never gates on validity.
|
|
1353
|
+
*/
|
|
1354
|
+
readonly canAdvance: boolean;
|
|
1355
|
+
/** `true` when `activeIndex > 0`. */
|
|
1356
|
+
readonly canGoBack: boolean;
|
|
1357
|
+
/**
|
|
1358
|
+
* `isFinalStep` and every step's form valid. Reactive to current
|
|
1359
|
+
* validity, so this is the "enable the Finish button" signal.
|
|
1360
|
+
*/
|
|
1361
|
+
readonly complete: boolean;
|
|
1362
|
+
/**
|
|
1363
|
+
* Monotonic latch: `true` the first time a `handleSubmit` resolves
|
|
1364
|
+
* without throwing AND leaves no errors on any step, and stays `true`
|
|
1365
|
+
* through later edits. A callback that calls `setErrors` and returns
|
|
1366
|
+
* is a failed submit, so it does not flip this. Only `reset()` clears
|
|
1367
|
+
* it. Gates success UI that should reflect submission history rather
|
|
1368
|
+
* than current validity.
|
|
1369
|
+
*/
|
|
1370
|
+
readonly done: boolean;
|
|
1371
|
+
/**
|
|
1372
|
+
* `true` while a `wizard.handleSubmit` call is in flight. Every
|
|
1373
|
+
* navigation method refuses while it is on.
|
|
1374
|
+
*/
|
|
1375
|
+
readonly submitting: boolean;
|
|
1376
|
+
/** `wizard.handleSubmit` invocations, success or failure. */
|
|
1377
|
+
readonly submissionAttempts: number;
|
|
1378
|
+
/**
|
|
1379
|
+
* The error THROWN by the most recent `handleSubmit` callback or its
|
|
1380
|
+
* `onError`, coerced to a real `Error`, and `null` on success. Like
|
|
1381
|
+
* `form.meta.submitError`, this is the unexpected-throw channel: an
|
|
1382
|
+
* expected rejection handled through `setErrors` surfaces on the
|
|
1383
|
+
* error surface and in `onError` instead, leaving this `null`. It is
|
|
1384
|
+
* parked here rather than re-thrown, so the handler always resolves
|
|
1385
|
+
* and never manufactures an unhandled rejection. Cleared at submit
|
|
1386
|
+
* entry and by `reset()`.
|
|
1387
|
+
*/
|
|
1388
|
+
readonly submitError: Error | null;
|
|
1389
|
+
/**
|
|
1390
|
+
* Append-only breadcrumb of navigated step keys. `back()` does not
|
|
1391
|
+
* pop: the trail is an audit log, not a back-stack.
|
|
1392
|
+
*/
|
|
1393
|
+
readonly visited: readonly FormKey[];
|
|
1394
|
+
/**
|
|
1395
|
+
* Advance one step. Refuses while `submitting`. On an UNCLEARED
|
|
1396
|
+
* `gate()` step it behaves as `tryNext()`, so wiring a Next button
|
|
1397
|
+
* straight to it can never skip the gate's confirmation; once that
|
|
1398
|
+
* gate clears it is plain navigation again and does not re-submit.
|
|
1399
|
+
*/
|
|
1400
|
+
readonly next: () => Promise<void>;
|
|
1401
|
+
/** Go back one step. Refuses while `submitting`. */
|
|
1402
|
+
readonly back: () => void;
|
|
1403
|
+
/** Jump to a step by key. Refuses while `submitting`. */
|
|
1404
|
+
readonly goTo: (key: string) => void;
|
|
1405
|
+
/**
|
|
1406
|
+
* Submit the active step, and advance once that submit resolves
|
|
1407
|
+
* clean. Invalid input keeps the pin put under the form's standard
|
|
1408
|
+
* error reveal, with the first error focused and display state
|
|
1409
|
+
* advanced. The advance runs after the submit settles, so a `gate()`
|
|
1410
|
+
* on the active step clears in a single call.
|
|
1411
|
+
*
|
|
1412
|
+
* Inline-bindable (`@click="wizard.tryNext()"`), and resolves to
|
|
1413
|
+
* whether the pin moved. `false` on a degenerate or final-step wizard;
|
|
1414
|
+
* finish through `handleSubmit` instead.
|
|
1415
|
+
*
|
|
1416
|
+
* Called while the active step already has a submit in flight, the
|
|
1417
|
+
* usual case being from inside that form's own callback, it rides
|
|
1418
|
+
* that submit rather than starting a second one, so one action costs
|
|
1419
|
+
* one submission. The pin has not moved by the time it resolves, so
|
|
1420
|
+
* the answer is `false` and the advance lands with the in-flight
|
|
1421
|
+
* submit.
|
|
1422
|
+
*/
|
|
1423
|
+
readonly tryNext: () => Promise<boolean>;
|
|
1424
|
+
/**
|
|
1425
|
+
* Validate the entire step list, from any step, and never advance the
|
|
1426
|
+
* pin. On success `done` latches; on any error the first failing step
|
|
1427
|
+
* is focused and `onError` fires with errors spanning every step.
|
|
1428
|
+
*
|
|
1429
|
+
* To gate advancing a step on its own validity, compose with the
|
|
1430
|
+
* active form's submit instead:
|
|
1431
|
+
* `activeForm.handleSubmit(() => wizard.next())`.
|
|
1432
|
+
*
|
|
1433
|
+
* Returns a handler for `<form @submit>` or for imperative use.
|
|
1434
|
+
*/
|
|
1435
|
+
readonly handleSubmit: (onSubmit: WizardOnSubmit, onError?: WizardOnError) => (event?: Event) => Promise<void>;
|
|
1436
|
+
/**
|
|
1437
|
+
* Zero the wizard lifecycle (`submissionAttempts`, `visited`, `done`),
|
|
1438
|
+
* reset every form, return to `steps[0].key`, and invoke `persist`
|
|
1439
|
+
* with the cleared state. Re-applies the `defaultStatuses` gate seed,
|
|
1440
|
+
* so a reboot returns to the seeded clearance rather than fully
|
|
1441
|
+
* sealed.
|
|
1442
|
+
*/
|
|
1443
|
+
readonly reset: () => void;
|
|
1444
|
+
/**
|
|
1445
|
+
* Re-seal a cleared gate, contingent on `commit`. Awaits `commit`, a
|
|
1446
|
+
* server-side revoke, and re-seals only if it resolves clean, so the
|
|
1447
|
+
* gate reflects server-confirmed truth the same way a clearing submit
|
|
1448
|
+
* does; a thrown `commit` leaves the gate as-is.
|
|
1449
|
+
*
|
|
1450
|
+
* Seal-only, so it can never become the leading-signal foot-gun
|
|
1451
|
+
* `gate()` exists to prevent. `commit` is required: pass `() => {}`
|
|
1452
|
+
* for a deliberate client-only re-seal. Resolves whether the gate
|
|
1453
|
+
* ended up sealed and never rejects; resolves `false` with a dev
|
|
1454
|
+
* warning on a non-gate key.
|
|
1455
|
+
*/
|
|
1456
|
+
readonly relock: (key: FormKey, commit: () => void | Promise<void>) => Promise<boolean>;
|
|
1457
|
+
};
|
|
1458
|
+
|
|
1459
|
+
/**
|
|
1460
|
+
* Portable SSR detection. The plugin captures this value at install time and
|
|
1461
|
+
* exposes it via the registry so every runtime branch reads a single source
|
|
1462
|
+
* of truth instead of sniffing `import.meta.*` (bundler-specific) at each
|
|
1463
|
+
* call site.
|
|
1464
|
+
*
|
|
1465
|
+
* Consumers can override the heuristic explicitly via
|
|
1466
|
+
* `createAttaform({ ssr: true })`; the default handles the common
|
|
1467
|
+
* Node-vs-browser split without relying on any bundler-injected flag.
|
|
1468
|
+
*/
|
|
1469
|
+
interface SSRDetectOptions {
|
|
1470
|
+
/**
|
|
1471
|
+
* Force SSR-vs-client mode, bypassing the `typeof window` heuristic.
|
|
1472
|
+
* `true` activates the SSR code paths (no devtools, no persistence
|
|
1473
|
+
* wiring, payload serialisation enabled); `false` forces client mode.
|
|
1474
|
+
* The Nuxt plugin sets this from `import.meta.server` so SSR detection
|
|
1475
|
+
* never depends on whether `window` is polyfilled. Tests that need to
|
|
1476
|
+
* exercise the SSR code paths under jsdom pass `ssr: true`.
|
|
1477
|
+
*/
|
|
1478
|
+
ssr?: boolean;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* Per-Vue-app container for every form state instance. Each
|
|
1483
|
+
* `app.use(createAttaform())` call gets its own registry, so Attaform runs
|
|
1484
|
+
* under bare Vue 3, SSR through `@vue/server-renderer`, and Nuxt on one
|
|
1485
|
+
* code path.
|
|
1486
|
+
*
|
|
1487
|
+
* Forms are stored as `Map<FormKey, FormStore<GenericForm>>`. The generic
|
|
1488
|
+
* has to relax at storage time because two forms in one app have different
|
|
1489
|
+
* `Form` generics; a caller recovers the specific type through `useForm`'s
|
|
1490
|
+
* overloads.
|
|
1491
|
+
*/
|
|
1492
|
+
|
|
1493
|
+
/**
|
|
1494
|
+
* Serialised snapshot of one form's state, captured by
|
|
1495
|
+
* `renderAttaformState` for SSR and replayed by
|
|
1496
|
+
* `hydrateAttaformState` on the client. Round-trips through
|
|
1497
|
+
* JSON-safe tuples; field references are intentionally omitted
|
|
1498
|
+
* (DOM nodes don't survive serialisation).
|
|
1499
|
+
*/
|
|
1500
|
+
type SerializedFormData = {
|
|
1501
|
+
/** The form's value at snapshot time. */
|
|
1502
|
+
readonly form: unknown;
|
|
1503
|
+
/**
|
|
1504
|
+
* Errors produced by the schema at snapshot time. Replayed into
|
|
1505
|
+
* the client form's error state at hydration; cleared on
|
|
1506
|
+
* successful re-validation client-side.
|
|
1507
|
+
*/
|
|
1508
|
+
readonly schemaErrors: ReadonlyArray<readonly [string, unknown]>;
|
|
1509
|
+
/**
|
|
1510
|
+
* Errors set explicitly via `setErrors` (typically a server
|
|
1511
|
+
* response) at snapshot time. Replayed at hydration; persists across
|
|
1512
|
+
* client-side re-validation.
|
|
1513
|
+
*/
|
|
1514
|
+
readonly userErrors: ReadonlyArray<readonly [string, unknown]>;
|
|
1515
|
+
/** Per-field metadata (timestamps, raw values, connection flags) captured at snapshot time. */
|
|
1516
|
+
readonly fields: ReadonlyArray<readonly [string, unknown]>;
|
|
1517
|
+
/**
|
|
1518
|
+
* Path keys that were in the form's `blankPaths` set at snapshot time,
|
|
1519
|
+
* carrying the "displayed empty" UI state across the SSR boundary.
|
|
1520
|
+
* Without it the client briefly renders `String(slim-default)` (`'0'`,
|
|
1521
|
+
* say) for a field the server rendered blank. Optional in the wire
|
|
1522
|
+
* format, so an older payload shape still deserialises.
|
|
1523
|
+
*/
|
|
1524
|
+
readonly blankPaths?: ReadonlyArray<string>;
|
|
1525
|
+
};
|
|
1526
|
+
type PendingHydration = Map<FormKey, SerializedFormData>;
|
|
1527
|
+
/**
|
|
1528
|
+
* Attaform's per-Vue-app container, one per `app.use(createAttaform())`
|
|
1529
|
+
* call.
|
|
1530
|
+
*
|
|
1531
|
+
* Most consumers never touch it: `useForm` and `injectForm` reach the
|
|
1532
|
+
* registry for you. Access it explicitly only when wiring SSR or a custom
|
|
1533
|
+
* plugin integration.
|
|
1534
|
+
*/
|
|
1535
|
+
type AttaformRegistry = {
|
|
1536
|
+
/**
|
|
1537
|
+
* Live forms keyed by `FormKey`.
|
|
1538
|
+
* @internal
|
|
1539
|
+
*/
|
|
1540
|
+
readonly forms: Map<FormKey, FormStore<GenericForm>>;
|
|
1541
|
+
/**
|
|
1542
|
+
* Live wizards keyed by the consumer-supplied `key` option. Populated by
|
|
1543
|
+
* `useWizard(entryForm, { key })` and read by `injectWizard(key)` to
|
|
1544
|
+
* resolve a cross-component wizard handle. An anonymous wizard does NOT
|
|
1545
|
+
* register here, and is reachable only through ambient provide/inject.
|
|
1546
|
+
* @internal
|
|
1547
|
+
*/
|
|
1548
|
+
readonly wizards: Map<string, UseWizardReturnType>;
|
|
1549
|
+
/**
|
|
1550
|
+
* Snapshots staged by `hydrateAttaformState` waiting to be consumed by the next `useForm` call.
|
|
1551
|
+
* @internal
|
|
1552
|
+
*/
|
|
1553
|
+
readonly pendingHydration: PendingHydration;
|
|
1554
|
+
/** `true` while running on the server during SSR; `false` on the client. */
|
|
1555
|
+
readonly ssr: boolean;
|
|
1556
|
+
/**
|
|
1557
|
+
* Track a consumer of `key`, returning a dispose function to call when
|
|
1558
|
+
* that consumer unmounts. The form is evicted once the last consumer
|
|
1559
|
+
* disposes, so a long-running SPA does not leak detached state across
|
|
1560
|
+
* navigations.
|
|
1561
|
+
* @internal
|
|
1562
|
+
*/
|
|
1563
|
+
readonly trackConsumer: (key: FormKey) => () => void;
|
|
1564
|
+
/**
|
|
1565
|
+
* Track a consumer of wizard `key`, returning a dispose function to call
|
|
1566
|
+
* when that consumer unmounts. The handle leaves `wizards` once the last
|
|
1567
|
+
* consumer disposes, mirroring the form consumer counting. An anonymous
|
|
1568
|
+
* wizard never enters this counter, having no key to count under.
|
|
1569
|
+
* @internal
|
|
1570
|
+
*/
|
|
1571
|
+
readonly trackWizardConsumer: (key: string) => () => void;
|
|
1572
|
+
/**
|
|
1573
|
+
* Mark a form as eligible for SSR prefetch. The form's
|
|
1574
|
+
* `onServerPrefetch` hook reads `shouldPrefetch(key)` and runs the
|
|
1575
|
+
* captured `defaultValues` factory only when this set holds the key and
|
|
1576
|
+
* the skip set does not. Set by `form.activate()` and by `useWizard`'s
|
|
1577
|
+
* current-step auto-mark.
|
|
1578
|
+
* @internal
|
|
1579
|
+
*/
|
|
1580
|
+
readonly enqueuePrefetch: (key: FormKey) => void;
|
|
1581
|
+
/**
|
|
1582
|
+
* Mark a form as ineligible for SSR prefetch, overriding
|
|
1583
|
+
* `enqueuePrefetch`. `useWizard` uses it to keep a non-current step
|
|
1584
|
+
* dormant on the server even when a stray `activate()` enqueued it: the
|
|
1585
|
+
* wizard's "the user is not on this step" signal wins.
|
|
1586
|
+
* @internal
|
|
1587
|
+
*/
|
|
1588
|
+
readonly skipPrefetch: (key: FormKey) => void;
|
|
1589
|
+
/**
|
|
1590
|
+
* Whether `key`'s SSR prefetch should run. Returns `true` iff the key
|
|
1591
|
+
* is enqueued AND not skipped.
|
|
1592
|
+
* @internal
|
|
1593
|
+
*/
|
|
1594
|
+
readonly shouldPrefetch: (key: FormKey) => boolean;
|
|
1595
|
+
};
|
|
1596
|
+
/**
|
|
1597
|
+
* The Vue `InjectionKey` the registry is provided under on the app. Most
|
|
1598
|
+
* consumers never need it: `useForm` and `injectForm` resolve the registry
|
|
1599
|
+
* automatically.
|
|
1600
|
+
*/
|
|
1601
|
+
declare const kAttaformRegistry: InjectionKey<AttaformRegistry>;
|
|
1602
|
+
declare module 'vue' {
|
|
1603
|
+
interface App {
|
|
1604
|
+
/** @internal */
|
|
1605
|
+
_attaform?: AttaformRegistry;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
/** Options for `createRegistry`. */
|
|
1609
|
+
type CreateRegistryOptions = SSRDetectOptions;
|
|
1610
|
+
/**
|
|
1611
|
+
* Create a fresh `AttaformRegistry`. `createAttaform()` calls it
|
|
1612
|
+
* internally, so reach for it directly only when building a custom plugin
|
|
1613
|
+
* that does not want `createAttaform`'s auto-install behaviour: a test
|
|
1614
|
+
* harness, or an embedded app.
|
|
1615
|
+
*/
|
|
1616
|
+
declare function createRegistry(options?: CreateRegistryOptions): AttaformRegistry;
|
|
1617
|
+
/**
|
|
1618
|
+
* Look up the current app's registry from inside a component's `setup()`,
|
|
1619
|
+
* or any synchronous code on the setup call stack.
|
|
1620
|
+
*
|
|
1621
|
+
* Most consumers do not need it, since `useForm` and `injectForm` call it
|
|
1622
|
+
* for you. Reach for it when building a custom integration that needs the
|
|
1623
|
+
* raw registry.
|
|
1624
|
+
*
|
|
1625
|
+
* Throws:
|
|
1626
|
+
* - `OutsideSetupError` when called outside a Vue setup context, from an
|
|
1627
|
+
* event handler or an async callback. Move the call into setup, or
|
|
1628
|
+
* trigger it from a child component.
|
|
1629
|
+
* - `RegistryNotInstalledError` when called inside setup with the plugin
|
|
1630
|
+
* not installed. Add `app.use(createAttaform())` to your app entry.
|
|
1631
|
+
*/
|
|
1632
|
+
declare function useRegistry(): AttaformRegistry;
|
|
1633
|
+
/**
|
|
1634
|
+
* Look up a Vue app's registry by `App` reference, for the SSR helpers
|
|
1635
|
+
* (`renderAttaformState`, `hydrateAttaformState`) that run outside a
|
|
1636
|
+
* component setup context.
|
|
1637
|
+
*
|
|
1638
|
+
* Throws `RegistryNotInstalledError` when the app was never wired with
|
|
1639
|
+
* `createAttaform()`.
|
|
1640
|
+
*/
|
|
1641
|
+
declare function getRegistryFromApp(app: App): AttaformRegistry;
|
|
1642
|
+
|
|
1643
|
+
/**
|
|
1644
|
+
* Multistep-form orchestrator built around an ordered list of step
|
|
1645
|
+
* slots. A slot resolves to a participating form four ways: an existing
|
|
1646
|
+
* `useForm` reference, a bare string key (desugared to a noop form, so
|
|
1647
|
+
* an affordance step participates like any other), an eager function
|
|
1648
|
+
* slot for runtime branching, or a `lazy()`-wrapped function slot that
|
|
1649
|
+
* caches its resolution and re-fires only on its own tracked deps.
|
|
1650
|
+
*
|
|
1651
|
+
* Navigation (`next` / `back` / `goTo`) walks positional indices.
|
|
1652
|
+
* `handleSubmit` validates the whole step list from any step and never
|
|
1653
|
+
* advances; to gate an advance, write
|
|
1654
|
+
* `activeForm.handleSubmit(() => wizard.next())`. URL sync rides on the
|
|
1655
|
+
* `restore` / `persist` callbacks, which default to `?step=<key>`.
|
|
1656
|
+
*/
|
|
1657
|
+
declare function useWizard<const S extends ReadonlyArray<StepSlot>>(options: WizardOptions & {
|
|
1658
|
+
readonly steps: S;
|
|
1659
|
+
}): UseWizardReturnType<S>;
|
|
1660
|
+
|
|
1661
|
+
/** Options accepted by `injectWizard` in place of a bare key string. */
|
|
1662
|
+
type InjectWizardInput = {
|
|
1663
|
+
readonly key?: string | undefined;
|
|
1664
|
+
};
|
|
1665
|
+
/**
|
|
1666
|
+
* Reach an existing wizard from a descendant component without threading
|
|
1667
|
+
* it through props. The counterpart to `useWizard`: `useWizard` creates
|
|
1668
|
+
* and provides, `injectWizard` looks up.
|
|
1669
|
+
*
|
|
1670
|
+
* ```ts
|
|
1671
|
+
* // The nearest ancestor's useWizard call.
|
|
1672
|
+
* const wizard = injectWizard()
|
|
1673
|
+
*
|
|
1674
|
+
* // A specific wizard by key, from anywhere in the app.
|
|
1675
|
+
* const signup = injectWizard('signup-wizard')
|
|
1676
|
+
* ```
|
|
1677
|
+
*
|
|
1678
|
+
* A keyed call is a registry lookup, independent of component-tree
|
|
1679
|
+
* position, and reaches any wizard built with `useWizard({ steps, key
|
|
1680
|
+
* })`. A no-key call takes the closest ambient ancestor; only an
|
|
1681
|
+
* anonymous `useWizard()` fills that slot, so a keyed wizard has to be
|
|
1682
|
+
* addressed by its key, exactly as on the form side.
|
|
1683
|
+
*
|
|
1684
|
+
* Returns `null` when nothing matches, so narrow before use:
|
|
1685
|
+
*
|
|
1686
|
+
* ```ts
|
|
1687
|
+
* const wizard = injectWizard('signup')
|
|
1688
|
+
* if (!wizard) return
|
|
1689
|
+
* wizard.next()
|
|
1690
|
+
* ```
|
|
1691
|
+
*
|
|
1692
|
+
* A keyed miss warns in dev with the registered keys and the call site.
|
|
1693
|
+
* A keyed lookup also pins the handle for this component's lifetime, so
|
|
1694
|
+
* it outlives a parent `useWizard` that unmounts first, and the registry
|
|
1695
|
+
* evicts the entry a microtask after the last consumer disposes. An
|
|
1696
|
+
* ambient lookup does not pin: the parent's scope owns that lifetime.
|
|
1697
|
+
*/
|
|
1698
|
+
declare function injectWizard(input?: string | InjectWizardInput): UseWizardReturnType | null;
|
|
1699
|
+
|
|
1700
|
+
/**
|
|
1701
|
+
* Wrap a function slot in `lazy()` to give that slot its own memoized
|
|
1702
|
+
* cache, distinct from the wizard-wide step compiler.
|
|
1703
|
+
*
|
|
1704
|
+
* Default function slots in `useWizard({ steps })` re-evaluate whenever
|
|
1705
|
+
* the compiled step list re-evaluates, including when an unrelated
|
|
1706
|
+
* slot's reactive deps change. That keeps the rail correct but can
|
|
1707
|
+
* trigger spurious work in a resolver that is expensive (a network
|
|
1708
|
+
* lookup, a heavy schema derivation, an async-derived factory).
|
|
1709
|
+
*
|
|
1710
|
+
* `lazy()` isolates each wrapped slot behind its own cache. The
|
|
1711
|
+
* resolver runs eagerly on the first compile pass; subsequent reads
|
|
1712
|
+
* reuse the cached form. The cache invalidates only when one of the
|
|
1713
|
+
* resolver's own tracked reactive dependencies changes (Vue's
|
|
1714
|
+
* `computed` semantics applied to a slot), or when `wizard.reset()`
|
|
1715
|
+
* triggers a global re-compile:
|
|
1716
|
+
*
|
|
1717
|
+
* const wizard = useWizard({
|
|
1718
|
+
* steps: [
|
|
1719
|
+
* account,
|
|
1720
|
+
* lazy((ctx) => loadShippingForm(ctx.forms.account.values.region)),
|
|
1721
|
+
* confirm,
|
|
1722
|
+
* ],
|
|
1723
|
+
* })
|
|
1724
|
+
*
|
|
1725
|
+
* Resolution semantics:
|
|
1726
|
+
* - Eager at construction so `wizard.steps` and SSR markup are honest
|
|
1727
|
+
* from t=0.
|
|
1728
|
+
* - Memoized: the resolver re-fires only when one of its own tracked
|
|
1729
|
+
* reads changes. An unrelated slot re-evaluating does not re-fire
|
|
1730
|
+
* this one.
|
|
1731
|
+
* - `wizard.reset()` clears every lazy slot's cache so a reboot truly
|
|
1732
|
+
* resolves from scratch.
|
|
1733
|
+
* - Resolving to `null` or `undefined` drops the slot from the compiled
|
|
1734
|
+
* list until the resolver next re-fires.
|
|
1735
|
+
* - The resolver may return a `gate()` wrapper, so a lazy slot can gate
|
|
1736
|
+
* what it resolves to; `lazy((ctx) => gate(s))` and `gate(lazy(s))`
|
|
1737
|
+
* resolve identically.
|
|
1738
|
+
*
|
|
1739
|
+
* The runtime brand returned by `lazy()` is opaque. Use
|
|
1740
|
+
* {@link isLazyMarker} to detect it.
|
|
1741
|
+
*/
|
|
1742
|
+
declare function lazy<Ctx = WizardCtx>(resolve: (ctx: Ctx) => SlotResolution<Ctx>): LazyMarker<Ctx>;
|
|
1743
|
+
|
|
1744
|
+
/**
|
|
1745
|
+
* Wrap a step slot in `gate()` to make it a hard prerequisite: while the
|
|
1746
|
+
* gate is uncleared, every step positioned after it is inaccessible, and
|
|
1747
|
+
* the gate's own form is the only way past.
|
|
1748
|
+
*
|
|
1749
|
+
* A gate is safe by construction. It does not hand the consumer a
|
|
1750
|
+
* predicate to key on; it bakes the correct semantics in:
|
|
1751
|
+
*
|
|
1752
|
+
* - **Confirmation, not intent.** The gate clears only when a member
|
|
1753
|
+
* form submits clean, never when a value merely goes valid. Checking
|
|
1754
|
+
* the box does nothing; submitting the consent step is what opens the
|
|
1755
|
+
* rail. This is the whole reason `gate()` exists: a leading value
|
|
1756
|
+
* signal lets a downstream step collect data before the prerequisite
|
|
1757
|
+
* is confirmed.
|
|
1758
|
+
* - **Linear-forward.** An uncleared gate seals every step after it.
|
|
1759
|
+
* Order places it: put `gate(step)` immediately before what it
|
|
1760
|
+
* guards. Everything downstream is both un-navigable and, through the
|
|
1761
|
+
* `disabled` data freeze, un-fillable, so a deep link, the browser
|
|
1762
|
+
* back button, or a stray `goTo` all redirect to the gate.
|
|
1763
|
+
* - **Freeze-after-clear.** A cleared gate's own form freezes, so
|
|
1764
|
+
* navigating back to it is a read-only review with no withdrawal
|
|
1765
|
+
* path. There is no re-lock dance.
|
|
1766
|
+
*
|
|
1767
|
+
* Wrap any slot kind:
|
|
1768
|
+
*
|
|
1769
|
+
* const wizard = useWizard({ steps: [gate(consent), shipping, payment] })
|
|
1770
|
+
*
|
|
1771
|
+
* A **form** gate (`gate(consentForm)`) clears when its form submits
|
|
1772
|
+
* clean, and is treated as pre-cleared on reload when its member form
|
|
1773
|
+
* rehydrates already valid (a persisted consent), so a seeded-valid gate
|
|
1774
|
+
* SSR-renders open. A bare-string **affordance** gate (`gate('terms')`)
|
|
1775
|
+
* clears on acknowledgment (advancing past it), and re-prompts each
|
|
1776
|
+
* session because that acknowledgment is ephemeral.
|
|
1777
|
+
*
|
|
1778
|
+
* Conditional gates come from composition, not an options bag:
|
|
1779
|
+
*
|
|
1780
|
+
* () => (transfer.values.amount > 10_000 ? gate(kyc) : kyc)
|
|
1781
|
+
*
|
|
1782
|
+
* `gate()` composes with `lazy()` in either order:
|
|
1783
|
+
* `gate(lazy(fn))` and `lazy((ctx) => gate(fn(ctx)))` resolve
|
|
1784
|
+
* identically.
|
|
1785
|
+
*
|
|
1786
|
+
* The runtime brand returned by `gate()` is opaque. Use
|
|
1787
|
+
* {@link isGateMarker} to detect it.
|
|
1788
|
+
*/
|
|
1789
|
+
declare function gate<const Inner extends StepSlot>(step: Inner): GateMarker<Inner>;
|
|
1790
|
+
|
|
1791
|
+
/**
|
|
1792
|
+
* Stable identifiers for library-emitted `ValidationError` codes.
|
|
1793
|
+
*
|
|
1794
|
+
* Convention: `<scope>:<kebab-case-identifier>`. Three scopes are
|
|
1795
|
+
* recognised by the library:
|
|
1796
|
+
*
|
|
1797
|
+
* - `atta:`: emitted by the framework-agnostic core (this map).
|
|
1798
|
+
* - `zod:`: emitted by the Zod adapter, computed inline from
|
|
1799
|
+
* `issue.code` (e.g. `zod:too_small`). No enum here because
|
|
1800
|
+
* Zod's code list evolves.
|
|
1801
|
+
* - consumer-defined: anything the consumer's backend or app stamps
|
|
1802
|
+
* onto a `ValidationError` (a server response handed to `setErrors`,
|
|
1803
|
+
* or a code passed inline). Pick a scope (`api:`, `auth:`, etc.) and
|
|
1804
|
+
* stay consistent.
|
|
1805
|
+
*
|
|
1806
|
+
* Use these constants in tests and error-routing UI:
|
|
1807
|
+
*
|
|
1808
|
+
* ```ts
|
|
1809
|
+
* if (error.code === AttaformErrorCode.NoValueSupplied) {
|
|
1810
|
+
* // user hasn't filled this field
|
|
1811
|
+
* }
|
|
1812
|
+
* ```
|
|
1813
|
+
*/
|
|
1814
|
+
declare const AttaformErrorCode: {
|
|
1815
|
+
/** A required field is in the blank set: no value was supplied. */
|
|
1816
|
+
readonly NoValueSupplied: "atta:no-value-supplied";
|
|
1817
|
+
/** The schema adapter's `validateAtPath` threw synchronously. */
|
|
1818
|
+
readonly AdapterThrew: "atta:adapter-threw";
|
|
1819
|
+
/**
|
|
1820
|
+
* User code inside a `z.preprocess`, `.refine`, or `.transform`
|
|
1821
|
+
* threw (sync or async). The adapter caught the throw and surfaced
|
|
1822
|
+
* it as a `ValidationError` at the field path so the form's normal
|
|
1823
|
+
* error pipeline handles it instead of leaking as an unhandled
|
|
1824
|
+
* rejection or routing through `submitError`.
|
|
1825
|
+
*/
|
|
1826
|
+
readonly ValidatorThrew: "atta:validator-threw";
|
|
1827
|
+
/**
|
|
1828
|
+
* A function-form `defaultValues` factory threw or its promise
|
|
1829
|
+
* rejected. The runtime captures the raw error on `form.hydrateError`
|
|
1830
|
+
* and ALSO surfaces a form-level `ValidationError` (path `[]`) so
|
|
1831
|
+
* the standard error pipeline carries the signal. Critical for the
|
|
1832
|
+
* SSR round-trip: `hydrateError` itself does not ride the wire
|
|
1833
|
+
* payload, but `schemaErrors` does, so the client sees the failure
|
|
1834
|
+
* after rehydration without an extra channel.
|
|
1835
|
+
*/
|
|
1836
|
+
readonly HydrationFailed: "atta:hydration-failed";
|
|
1837
|
+
/** The supplied path didn't resolve to any node in the schema. */
|
|
1838
|
+
readonly PathNotFound: "atta:path-not-found";
|
|
1839
|
+
/**
|
|
1840
|
+
* A walked form's `activate()` (async `defaultValues` factory) threw
|
|
1841
|
+
* during `wizard.handleSubmit`'s path walk. Surfaced as a synthetic
|
|
1842
|
+
* `ValidationError` at the form-level path (`[]`) so the wizard's
|
|
1843
|
+
* aggregate error pipeline can carry the failure alongside ordinary
|
|
1844
|
+
* validation errors. The raw factory error remains on
|
|
1845
|
+
* `form.hydrateError` for retry UX.
|
|
1846
|
+
*/
|
|
1847
|
+
readonly ActivationFailed: "atta:activation-failed";
|
|
1848
|
+
/**
|
|
1849
|
+
* Default code stamped on a manual error set through `form.setErrors`
|
|
1850
|
+
* when the caller omits an explicit `code`. The `setErrors` input is
|
|
1851
|
+
* lenient (`code` optional), so this is the fallback that keeps every
|
|
1852
|
+
* produced `ValidationError` carrying a stable, branchable identifier.
|
|
1853
|
+
* Override it per error by passing your own `code` (`api:…`, `auth:…`).
|
|
1854
|
+
*/
|
|
1855
|
+
readonly UserError: "atta:user-error";
|
|
1856
|
+
/**
|
|
1857
|
+
* Stamped on a `ValidationError` synthesized from a throw or rejection
|
|
1858
|
+
* out of a `handleSubmit` `onSubmit` callback. The raw error also lands
|
|
1859
|
+
* on `form.meta.submitError` (a raw-`Error` inspection channel); this
|
|
1860
|
+
* code marks the copy piped into the user-error layer so the failure
|
|
1861
|
+
* also surfaces on `form.errors` / `meta.ownErrors` / `firstOwnError`
|
|
1862
|
+
* where the UI already reads. A throw carrying its own `path` / `code`
|
|
1863
|
+
* keeps them; a bare `Error` or a non-Error throw defaults here at the
|
|
1864
|
+
* form-level path `[]`.
|
|
1865
|
+
*/
|
|
1866
|
+
readonly SubmitError: "atta:submit-error";
|
|
1867
|
+
/**
|
|
1868
|
+
* Synthesized by `wizard.handleSubmit` when the whole-wizard submit
|
|
1869
|
+
* would otherwise succeed but a `gate()` step is still uncleared. A
|
|
1870
|
+
* gate clears only on its member form's clean submit (confirmation), so
|
|
1871
|
+
* a downstream step that happens to be valid-by-default must not let the
|
|
1872
|
+
* wizard finish past an unconfirmed prerequisite. Emitted at the gate
|
|
1873
|
+
* form's key (path `[]`) so the aggregate error pipeline routes focus to
|
|
1874
|
+
* the gate, and `done` never latches.
|
|
1875
|
+
*/
|
|
1876
|
+
readonly GateNotCleared: "atta:gate-not-cleared";
|
|
1877
|
+
};
|
|
1878
|
+
/** Every `atta:`-prefixed code Attaform itself can put on an error. */
|
|
1879
|
+
type AttaformErrorCode = (typeof AttaformErrorCode)[keyof typeof AttaformErrorCode];
|
|
1880
|
+
|
|
1881
|
+
/**
|
|
1882
|
+
* Options for `createAttaform()`.
|
|
1883
|
+
*/
|
|
1884
|
+
type AttaformPluginOptions = SSRDetectOptions & {
|
|
1885
|
+
/**
|
|
1886
|
+
* Whether to install the Vue DevTools integration. Default `true`.
|
|
1887
|
+
* The integration is dev-only: the `import('./devtools')` sits behind
|
|
1888
|
+
* the `__DEV__` flag, so a consumer's production build folds it out
|
|
1889
|
+
* entirely (no devtools chunk shipped, no fetch attempted). In
|
|
1890
|
+
* development the DevTools peer is loaded lazily and a missing peer
|
|
1891
|
+
* fails silently. Pass `false` to skip the integration even in dev.
|
|
1892
|
+
*/
|
|
1893
|
+
devtools?: boolean;
|
|
1894
|
+
};
|
|
1895
|
+
/**
|
|
1896
|
+
* Create the Vue plugin that installs the form library on a Vue
|
|
1897
|
+
* application. Required only when you want app-wide options
|
|
1898
|
+
* (`devtools: false`, `ssr: true`). For the default setup,
|
|
1899
|
+
* `useForm` / `injectForm` / `useRegister` lazy-install the registry
|
|
1900
|
+
* on first use.
|
|
1901
|
+
*
|
|
1902
|
+
* ```ts
|
|
1903
|
+
* import { createApp } from 'vue'
|
|
1904
|
+
* import { createAttaform } from 'attaform'
|
|
1905
|
+
*
|
|
1906
|
+
* createApp(App)
|
|
1907
|
+
* .use(createAttaform({ devtools: false }))
|
|
1908
|
+
* .mount('#app')
|
|
1909
|
+
* ```
|
|
1910
|
+
*
|
|
1911
|
+
* Under SSR with bare Vue 3, install explicitly with `{ ssr: true }`
|
|
1912
|
+
* from your server entry: the SSR serialization helpers
|
|
1913
|
+
* (`renderAttaformState` / `hydrateAttaformState`) require an
|
|
1914
|
+
* already-attached registry and don't trigger lazy install. Under
|
|
1915
|
+
* Nuxt, install via `attaform/nuxt` instead; the Nuxt module wires
|
|
1916
|
+
* both server and client automatically.
|
|
1917
|
+
*
|
|
1918
|
+
* Installing more than once on the same app is a no-op (the second
|
|
1919
|
+
* call logs a dev-mode warning).
|
|
1920
|
+
*/
|
|
1921
|
+
declare function createAttaform(options?: AttaformPluginOptions): Plugin;
|
|
1922
|
+
|
|
1923
|
+
/**
|
|
1924
|
+
* Serialised snapshot of every form in a Vue app, produced by
|
|
1925
|
+
* `renderAttaformState` and consumed by `hydrateAttaformState`.
|
|
1926
|
+
*
|
|
1927
|
+
* JSON-safe: pass it to `JSON.stringify`, `devalue`, or any other
|
|
1928
|
+
* serialiser before embedding in your SSR payload.
|
|
1929
|
+
*/
|
|
1930
|
+
type SerializedAttaformState = {
|
|
1931
|
+
/** Envelope version; see {@link ATTAFORM_STATE_VERSION}. */
|
|
1932
|
+
readonly v: number;
|
|
1933
|
+
/** Tuples of `[formKey, snapshot]` for every form in the app. */
|
|
1934
|
+
readonly forms: ReadonlyArray<readonly [FormKey, SerializedFormData]>;
|
|
1935
|
+
};
|
|
1936
|
+
/**
|
|
1937
|
+
* Snapshot every form on a Vue app for SSR. Call from your server
|
|
1938
|
+
* entry after rendering the app:
|
|
1939
|
+
*
|
|
1940
|
+
* ```ts
|
|
1941
|
+
* import { renderToString } from '@vue/server-renderer'
|
|
1942
|
+
* import { renderAttaformState, escapeForInlineScript } from 'attaform'
|
|
1943
|
+
*
|
|
1944
|
+
* const html = await renderToString(app)
|
|
1945
|
+
* const state = renderAttaformState(app)
|
|
1946
|
+
* const payload = escapeForInlineScript(JSON.stringify(state))
|
|
1947
|
+
*
|
|
1948
|
+
* return `
|
|
1949
|
+
* ${html}
|
|
1950
|
+
* <script>window.__ATTAFORM_STATE__ = ${payload}</script>
|
|
1951
|
+
* `
|
|
1952
|
+
* ```
|
|
1953
|
+
*
|
|
1954
|
+
* Pair with `hydrateAttaformState` on the client to restore the
|
|
1955
|
+
* forms in their server-rendered state. Nuxt users do not need it:
|
|
1956
|
+
* `attaform/nuxt` wires SSR automatically.
|
|
1957
|
+
*/
|
|
1958
|
+
declare function renderAttaformState(app: App): SerializedAttaformState;
|
|
1959
|
+
/**
|
|
1960
|
+
* Restore forms from a server-rendered snapshot on the client. Call
|
|
1961
|
+
* from your client entry before mounting:
|
|
1962
|
+
*
|
|
1963
|
+
* ```ts
|
|
1964
|
+
* import { createApp } from 'vue'
|
|
1965
|
+
* import { createAttaform, hydrateAttaformState } from 'attaform'
|
|
1966
|
+
*
|
|
1967
|
+
* const app = createApp(App).use(createAttaform())
|
|
1968
|
+
* hydrateAttaformState(app, window.__ATTAFORM_STATE__)
|
|
1969
|
+
* app.mount('#app')
|
|
1970
|
+
* ```
|
|
1971
|
+
*
|
|
1972
|
+
* Accepts `unknown` because the payload crosses the wire untyped
|
|
1973
|
+
* (an inline `<script>` tag, `JSON.parse`): the envelope is
|
|
1974
|
+
* validated at runtime, and anything that isn't a version-matched
|
|
1975
|
+
* `renderAttaformState` snapshot is skipped wholesale, so forms
|
|
1976
|
+
* construct fresh from their schemas instead.
|
|
1977
|
+
*
|
|
1978
|
+
* The next `useForm({ key })` call for each serialised form picks up
|
|
1979
|
+
* the snapshot transparently, with no further action required.
|
|
1980
|
+
*/
|
|
1981
|
+
declare function hydrateAttaformState(app: App, payload: unknown): void;
|
|
1982
|
+
|
|
1983
|
+
/**
|
|
1984
|
+
* Escape a JSON string so it's safe to embed inside an inline
|
|
1985
|
+
* `<script>` tag during SSR. Plain `JSON.stringify` is not safe: a
|
|
1986
|
+
* form value containing the literal substring `</script>` would
|
|
1987
|
+
* break out of the script tag.
|
|
1988
|
+
*
|
|
1989
|
+
* ```ts
|
|
1990
|
+
* const payload = escapeForInlineScript(JSON.stringify(renderAttaformState(app)))
|
|
1991
|
+
* // `<script>window.__ATTAFORM_STATE__ = ${payload}</script>` is safe.
|
|
1992
|
+
* ```
|
|
1993
|
+
*
|
|
1994
|
+
* Output remains valid JSON, so `JSON.parse` round-trips back to the
|
|
1995
|
+
* original value on the client.
|
|
1996
|
+
*/
|
|
1997
|
+
declare function escapeForInlineScript(json: string): string;
|
|
1998
|
+
|
|
1999
|
+
/**
|
|
2000
|
+
* Symbol slot used by custom directive integrations to install an
|
|
2001
|
+
* assigner on the bound element. Read by the v-register directive
|
|
2002
|
+
* when a DOM event fires:
|
|
2003
|
+
*
|
|
2004
|
+
* ```ts
|
|
2005
|
+
* import { assignKey } from 'attaform'
|
|
2006
|
+
* el[assignKey] = (value) => myCustomWriter(value)
|
|
2007
|
+
* ```
|
|
2008
|
+
*
|
|
2009
|
+
* Most consumers never need this: the built-in directives wire default
|
|
2010
|
+
* assigners for text inputs, checkboxes, radios, and selects.
|
|
2011
|
+
*/
|
|
2012
|
+
declare const assignKey: unique symbol;
|
|
2013
|
+
|
|
2014
|
+
/**
|
|
2015
|
+
* Type guard for a `RegisterValue`. Returns `true` when `val` looks
|
|
2016
|
+
* like the object returned from `form.register(path)`.
|
|
2017
|
+
*
|
|
2018
|
+
* ```ts
|
|
2019
|
+
* if (isRegisterValue(slotValue)) {
|
|
2020
|
+
* // slotValue.innerRef is now a Ref<unknown>
|
|
2021
|
+
* }
|
|
2022
|
+
* ```
|
|
2023
|
+
*
|
|
2024
|
+
* Useful when building wrapper components that accept either a
|
|
2025
|
+
* `RegisterValue` or a plain ref via the same prop.
|
|
2026
|
+
*/
|
|
2027
|
+
declare function isRegisterValue<Value = unknown>(val: unknown): val is RegisterValue<Value>;
|
|
2028
|
+
|
|
2029
|
+
/**
|
|
2030
|
+
* Shared building blocks for Attaform's two devtools surfaces: the Vue
|
|
2031
|
+
* DevTools (Chrome-extension) inspector wired up in `./devtools.ts`, and
|
|
2032
|
+
* the Nuxt DevTools (overlay) panel wired up via `../../nuxt.ts` +
|
|
2033
|
+
* `../pages/_attaform_devtools.vue`.
|
|
2034
|
+
*
|
|
2035
|
+
* Houses the window-bridge contract both surfaces consume so a new
|
|
2036
|
+
* bridge field lands in one file. Both surfaces render RAW form values
|
|
2037
|
+
* by design. DevTools is a dev-only surface, and redaction across every
|
|
2038
|
+
* place a value surfaces is impractical security theater rather than a
|
|
2039
|
+
* real safeguard.
|
|
2040
|
+
*/
|
|
2041
|
+
|
|
2042
|
+
/**
|
|
2043
|
+
* Property key on `window` that the Nuxt-side dev plugin attaches the
|
|
2044
|
+
* bridge object to. The iframe-mounted overlay panel reads
|
|
2045
|
+
* `window.parent[DEVTOOLS_WINDOW_KEY]` to reach the host app's registry.
|
|
2046
|
+
*
|
|
2047
|
+
* Underscored + namespaced to make accidental collision with consumer
|
|
2048
|
+
* globals vanishingly unlikely. It must stay stable across versions:
|
|
2049
|
+
* would silently disconnect older library builds from newer overlay
|
|
2050
|
+
* panels in the same browser tab during a library upgrade.
|
|
2051
|
+
*/
|
|
2052
|
+
declare const DEVTOOLS_WINDOW_KEY = "__attaform_devtools__";
|
|
2053
|
+
/**
|
|
2054
|
+
* Shape of the object the host plugin attaches to `window` in dev mode.
|
|
2055
|
+
* The iframe overlay panel reads this to discover the live registry and
|
|
2056
|
+
* render its forms.
|
|
2057
|
+
*
|
|
2058
|
+
* Single-registry assumption: the latest `createAttaform()` install
|
|
2059
|
+
* wins. Multi-app pages (rare; typically only seen in micro-frontend
|
|
2060
|
+
* setups) will only see one app's forms in the panel. Documented but
|
|
2061
|
+
* not actively supported. The alternative (a Set of registries with
|
|
2062
|
+
* union-rendering) is a future call if a real consumer hits it.
|
|
2063
|
+
*/
|
|
2064
|
+
interface AttaformDevtoolsBridge {
|
|
2065
|
+
registry: AttaformRegistry;
|
|
2066
|
+
/**
|
|
2067
|
+
* The library version, surfaced in the panel's footer for support /
|
|
2068
|
+
* bug-report context. Read from `package.json` at host-plugin init.
|
|
2069
|
+
*/
|
|
2070
|
+
version: string;
|
|
2071
|
+
}
|
|
2072
|
+
declare global {
|
|
2073
|
+
interface Window {
|
|
2074
|
+
[DEVTOOLS_WINDOW_KEY]?: AttaformDevtoolsBridge;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
/**
|
|
2079
|
+
* Typed error classes thrown by the form library. Each one signals a
|
|
2080
|
+
* distinct misuse so calling code can branch on `instanceof` instead
|
|
2081
|
+
* of pattern-matching error messages.
|
|
2082
|
+
*
|
|
2083
|
+
* Every class extends `AttaformError`, so consumers can write a single
|
|
2084
|
+
* polymorphic catch (`catch (e) { if (e instanceof AttaformError) ... }`)
|
|
2085
|
+
* instead of OR-chaining checks for each subclass. `AttaformError` itself
|
|
2086
|
+
* extends the standard `Error`, so existing `instanceof Error` usage
|
|
2087
|
+
* keeps working.
|
|
2088
|
+
*/
|
|
2089
|
+
|
|
2090
|
+
/**
|
|
2091
|
+
* Base for every error class thrown by `attaform`. Sets
|
|
2092
|
+
* `this.name` from the constructor's `new.target.name`, so subclasses
|
|
2093
|
+
* don't have to redeclare their own name override.
|
|
2094
|
+
*/
|
|
2095
|
+
declare class AttaformError extends Error {
|
|
2096
|
+
constructor(message: string, options?: ErrorOptions);
|
|
2097
|
+
}
|
|
2098
|
+
/**
|
|
2099
|
+
* Thrown when a path string is malformed, typically a dotted path
|
|
2100
|
+
* with empty segments (e.g. `'a..b'`, leading or trailing dots).
|
|
2101
|
+
* Use array form (`['a', 'b']`) for keys that contain literal dots.
|
|
2102
|
+
*/
|
|
2103
|
+
declare class InvalidPathError extends AttaformError {
|
|
2104
|
+
}
|
|
2105
|
+
/**
|
|
2106
|
+
* Thrown when `useForm` receives an invalid configuration, most often
|
|
2107
|
+
* a schema passed directly as the first argument, or no argument at
|
|
2108
|
+
* all. The configuration is an options bag; the schema is one of
|
|
2109
|
+
* several fields, even when it's the only one in use.
|
|
2110
|
+
*
|
|
2111
|
+
* ```ts
|
|
2112
|
+
* // ✗ Crashes deep inside the validator with an opaque message:
|
|
2113
|
+
* const form = useForm(z.object({ ... }))
|
|
2114
|
+
* // ✗ Same:
|
|
2115
|
+
* const form = useForm()
|
|
2116
|
+
* // ✓ Pass the schema as the `schema` field:
|
|
2117
|
+
* const form = useForm({ schema: z.object({ ... }) })
|
|
2118
|
+
* ```
|
|
2119
|
+
*
|
|
2120
|
+
* The same shape applies to every entry point: `attaform/zod`,
|
|
2121
|
+
* `attaform/zod-v3`, `attaform/zod-v4`, and the schema-agnostic
|
|
2122
|
+
* `attaform` root.
|
|
2123
|
+
*/
|
|
2124
|
+
declare class InvalidUseFormConfigError extends AttaformError {
|
|
2125
|
+
constructor();
|
|
2126
|
+
}
|
|
2127
|
+
/**
|
|
2128
|
+
* Thrown when a `handleSubmit`-supplied `onError` callback itself
|
|
2129
|
+
* throws or rejects. Wraps the inner failure so both the original
|
|
2130
|
+
* cause (via `error.cause`) and the propagation site are visible.
|
|
2131
|
+
*/
|
|
2132
|
+
declare class SubmitErrorHandlerError extends AttaformError {
|
|
2133
|
+
}
|
|
2134
|
+
/**
|
|
2135
|
+
* Thrown when an `attaform` API needs the registry attached to a Vue
|
|
2136
|
+
* app but it isn't there yet. Component-level entry points (`useForm`,
|
|
2137
|
+
* `injectForm`, `useRegister`) lazy-install the registry on first use,
|
|
2138
|
+
* so this error is mostly reached by the SSR helpers `renderAttaformState`
|
|
2139
|
+
* and `hydrateAttaformState`, which run outside a setup context and
|
|
2140
|
+
* have no current instance to install against.
|
|
2141
|
+
*
|
|
2142
|
+
* Fix: add `app.use(createAttaform())` (or `app.use(createAttaform({
|
|
2143
|
+
* ssr: true }))` on the server) to your SSR entry, before
|
|
2144
|
+
* `renderToString` / hydration. Under Nuxt, `attaform/nuxt` already
|
|
2145
|
+
* does this; the error usually points at a non-Nuxt SSR setup that
|
|
2146
|
+
* hasn't installed explicitly.
|
|
2147
|
+
*/
|
|
2148
|
+
declare class RegistryNotInstalledError extends AttaformError {
|
|
2149
|
+
constructor();
|
|
2150
|
+
}
|
|
2151
|
+
/**
|
|
2152
|
+
* Thrown when `useForm` / `injectForm` is called outside of a
|
|
2153
|
+
* Vue `setup()` function, typically from an event handler, watcher,
|
|
2154
|
+
* or async callback that runs after mount.
|
|
2155
|
+
*
|
|
2156
|
+
* Fix: move the call into `setup()`, or trigger it from a child
|
|
2157
|
+
* component whose `setup()` runs the composable.
|
|
2158
|
+
*/
|
|
2159
|
+
declare class OutsideSetupError extends AttaformError {
|
|
2160
|
+
constructor();
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* Thrown when a `useForm({ key })` call uses a key starting with
|
|
2164
|
+
* `__atta:`. That prefix is reserved for keys the library generates
|
|
2165
|
+
* internally (e.g. for anonymous `useForm()` calls without an
|
|
2166
|
+
* explicit key). Pick a different prefix for your form.
|
|
2167
|
+
*/
|
|
2168
|
+
declare class ReservedFormKeyError extends AttaformError {
|
|
2169
|
+
constructor(key: string);
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
export { createRegistry as B, DEVTOOLS_WINDOW_KEY as D, escapeForInlineScript as E, gate as H, getRegistryFromApp as J, hydrateAttaformState as K, injectForm as M, injectWizard as N, OutsideSetupError as O, isRegisterValue as P, kAttaformRegistry as Q, RegistryNotInstalledError as R, lazy as T, renderAttaformState as V, useRegister as X, useRegistry as Y, useWizard as Z, AttaformError as c, AttaformErrorCode as d, InvalidPathError as g, InvalidUseFormConfigError as h, ReservedFormKeyError as i, SubmitErrorHandlerError as l, assignKey as y, createAttaform as z };
|
|
2173
|
+
export type { AttaformRegistry as A, CompiledStep as C, FormStatus as F, GateMarker as G, InjectWizardInput as I, LazyMarker as L, SerializedAttaformState as S, UseRegisterOptions as U, WizardAggregateError as W, AnyForm as a, AttaformDevtoolsBridge as b, AttaformPluginOptions as e, FormStatusSeed as f, SerializedFormData as j, StepSlot as k, UseRegisterReturn as m, 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 };
|