attaform 0.21.1 → 0.22.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.
Files changed (108) hide show
  1. package/dist/chunks/dev-key-collision-warnings.cjs +1 -1
  2. package/dist/chunks/dev-key-collision-warnings.mjs +1 -1
  3. package/dist/chunks/devtools.cjs +1 -1
  4. package/dist/chunks/devtools.mjs +1 -1
  5. package/dist/chunks/fingerprint2.cjs +1 -1
  6. package/dist/chunks/fingerprint2.mjs +1 -1
  7. package/dist/chunks/indexeddb.cjs +1 -1
  8. package/dist/chunks/indexeddb.mjs +1 -1
  9. package/dist/chunks/local-storage.cjs +1 -1
  10. package/dist/chunks/local-storage.mjs +1 -1
  11. package/dist/chunks/multi-tab-sync.cjs +2 -2
  12. package/dist/chunks/multi-tab-sync.mjs +2 -2
  13. package/dist/chunks/session-storage.cjs +1 -1
  14. package/dist/chunks/session-storage.mjs +1 -1
  15. package/dist/chunks/wire-persistence.cjs +2 -2
  16. package/dist/chunks/wire-persistence.mjs +2 -2
  17. package/dist/index.cjs +37 -24
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +20 -18
  20. package/dist/index.d.mts +20 -18
  21. package/dist/index.d.ts +20 -18
  22. package/dist/index.mjs +38 -25
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/nuxt.d.cts +1 -1
  25. package/dist/nuxt.d.mts +1 -1
  26. package/dist/nuxt.d.ts +1 -1
  27. package/dist/runtime/components/AttaformDevtoolsPanel.vue +396 -216
  28. package/dist/runtime/components/DevtoolsValueTree.vue +176 -114
  29. package/dist/runtime/plugins/attaform.cjs +2 -2
  30. package/dist/runtime/plugins/attaform.mjs +2 -2
  31. package/dist/shared/{attaform.D32WwKk6.cjs → attaform.01iKS_lz.cjs} +260 -356
  32. package/dist/shared/attaform.01iKS_lz.cjs.map +1 -0
  33. package/dist/shared/{attaform.Y1ZGhM4k.mjs → attaform.6xE0Lcfd.mjs} +2 -2
  34. package/dist/shared/{attaform.Y1ZGhM4k.mjs.map → attaform.6xE0Lcfd.mjs.map} +1 -1
  35. package/dist/shared/{attaform.S-pYLSo4.cjs → attaform.AyujQoHp.cjs} +13 -16
  36. package/dist/shared/attaform.AyujQoHp.cjs.map +1 -0
  37. package/dist/shared/{attaform.BupwXkj_.mjs → attaform.BFWb6hDk.mjs} +29 -23
  38. package/dist/shared/attaform.BFWb6hDk.mjs.map +1 -0
  39. package/dist/shared/{attaform.NQ8mybyW.d.mts → attaform.BGwNZ9GV.d.cts} +63 -64
  40. package/dist/shared/{attaform.pmtahXKy.mjs → attaform.BKFwekY2.mjs} +257 -356
  41. package/dist/shared/attaform.BKFwekY2.mjs.map +1 -0
  42. package/dist/shared/{attaform.BSkvn43g.cjs → attaform.C-RtnCJM.cjs} +116 -47
  43. package/dist/shared/attaform.C-RtnCJM.cjs.map +1 -0
  44. package/dist/shared/{attaform.Bv7dRDWK.d.ts → attaform.CCCeEPwa.d.mts} +63 -64
  45. package/dist/shared/{attaform.BM6YD9kZ.cjs → attaform.CR6wGvNu.cjs} +29 -23
  46. package/dist/shared/attaform.CR6wGvNu.cjs.map +1 -0
  47. package/dist/shared/{attaform.Bq5sX7TF.cjs → attaform.CRzpFCjV.cjs} +2 -2
  48. package/dist/shared/{attaform.Bq5sX7TF.cjs.map → attaform.CRzpFCjV.cjs.map} +1 -1
  49. package/dist/shared/{attaform.ClXwitZj.cjs → attaform.CjMcwV7W.cjs} +894 -342
  50. package/dist/shared/attaform.CjMcwV7W.cjs.map +1 -0
  51. package/dist/shared/{attaform.DR6RmxWZ.mjs → attaform.CsB-iKbU.mjs} +888 -337
  52. package/dist/shared/attaform.CsB-iKbU.mjs.map +1 -0
  53. package/dist/shared/{attaform.BWfliRIK.d.cts → attaform.D4XYaasQ.d.ts} +63 -64
  54. package/dist/shared/{attaform.Be8NZG9M.mjs → attaform.DCjgGir_.mjs} +19 -45
  55. package/dist/shared/attaform.DCjgGir_.mjs.map +1 -0
  56. package/dist/shared/{attaform.DMEP_ENr.mjs → attaform.DNuiFCXG.mjs} +14 -17
  57. package/dist/shared/attaform.DNuiFCXG.mjs.map +1 -0
  58. package/dist/shared/{attaform.MtrpT6Ki.d.ts → attaform.DUMWQefY.d.ts} +1 -1
  59. package/dist/shared/{attaform.DozgVlCE.mjs → attaform.DgCfLqay.mjs} +116 -47
  60. package/dist/shared/attaform.DgCfLqay.mjs.map +1 -0
  61. package/dist/shared/{attaform.D0dWZsJt.d.mts → attaform.DvUH4a3o.d.cts} +307 -133
  62. package/dist/shared/{attaform.D0dWZsJt.d.cts → attaform.DvUH4a3o.d.mts} +307 -133
  63. package/dist/shared/{attaform.D0dWZsJt.d.ts → attaform.DvUH4a3o.d.ts} +307 -133
  64. package/dist/shared/{attaform.Duecg2NO.d.mts → attaform.FN0vaQAg.d.mts} +1 -1
  65. package/dist/shared/{attaform.CICFZ1iS.cjs → attaform.Q3eAD2wD.cjs} +19 -45
  66. package/dist/shared/attaform.Q3eAD2wD.cjs.map +1 -0
  67. package/dist/shared/{attaform.FudOcHaa.d.cts → attaform.aekT7mMx.d.cts} +1 -1
  68. package/dist/transforms.cjs +1 -1
  69. package/dist/transforms.mjs +1 -1
  70. package/dist/vite.cjs +1 -1
  71. package/dist/vite.mjs +1 -1
  72. package/dist/zod-v3.cjs +3 -4
  73. package/dist/zod-v3.cjs.map +1 -1
  74. package/dist/zod-v3.d.cts +4 -4
  75. package/dist/zod-v3.d.mts +4 -4
  76. package/dist/zod-v3.d.ts +4 -4
  77. package/dist/zod-v3.mjs +2 -3
  78. package/dist/zod-v3.mjs.map +1 -1
  79. package/dist/zod-v4.cjs +3 -4
  80. package/dist/zod-v4.cjs.map +1 -1
  81. package/dist/zod-v4.d.cts +4 -4
  82. package/dist/zod-v4.d.mts +4 -4
  83. package/dist/zod-v4.d.ts +4 -4
  84. package/dist/zod-v4.mjs +2 -3
  85. package/dist/zod-v4.mjs.map +1 -1
  86. package/dist/zod.cjs +6 -6
  87. package/dist/zod.cjs.map +1 -1
  88. package/dist/zod.d.cts +31 -22
  89. package/dist/zod.d.mts +31 -22
  90. package/dist/zod.d.ts +31 -22
  91. package/dist/zod.mjs +5 -6
  92. package/dist/zod.mjs.map +1 -1
  93. package/package.json +4 -12
  94. package/dist/shared/attaform.BM6YD9kZ.cjs.map +0 -1
  95. package/dist/shared/attaform.BSkvn43g.cjs.map +0 -1
  96. package/dist/shared/attaform.Be8NZG9M.mjs.map +0 -1
  97. package/dist/shared/attaform.BupwXkj_.mjs.map +0 -1
  98. package/dist/shared/attaform.CICFZ1iS.cjs.map +0 -1
  99. package/dist/shared/attaform.ClXwitZj.cjs.map +0 -1
  100. package/dist/shared/attaform.D32WwKk6.cjs.map +0 -1
  101. package/dist/shared/attaform.DMEP_ENr.mjs.map +0 -1
  102. package/dist/shared/attaform.DR6RmxWZ.mjs.map +0 -1
  103. package/dist/shared/attaform.DozgVlCE.mjs.map +0 -1
  104. package/dist/shared/attaform.S-pYLSo4.cjs.map +0 -1
  105. package/dist/shared/attaform.pmtahXKy.mjs.map +0 -1
  106. package/dist/shared/{attaform.DSD85fHb.d.cts → attaform.nf83TIR5.d.cts} +10 -10
  107. package/dist/shared/{attaform.DSD85fHb.d.mts → attaform.nf83TIR5.d.mts} +10 -10
  108. package/dist/shared/{attaform.DSD85fHb.d.ts → attaform.nf83TIR5.d.ts} +10 -10
@@ -1,4 +1,4 @@
1
- import { ObjectDirective, Ref, ComputedRef } from 'vue';
1
+ import { ObjectDirective, Ref, MaybeRefOrGetter, ComputedRef } from 'vue';
2
2
 
3
3
  /**
4
4
  * Schema-attached field metadata — the shared types used by both Zod
@@ -73,6 +73,123 @@ type ResolvedFieldMeta = {
73
73
  readonly meta: Readonly<FieldMetaPayload>;
74
74
  };
75
75
 
76
+ /**
77
+ * Path primitives for advanced integrations. The form library accepts
78
+ * paths in dotted-string form (`'user.email'`) at every public API.
79
+ * These primitives are exposed for adapter authors who need to
80
+ * canonicalise user-provided paths.
81
+ */
82
+ declare const pathKeyBrand: unique symbol;
83
+ /**
84
+ * Branded string identifier for a canonicalised path. Useful as a
85
+ * `Map` key — two paths that resolve to the same canonical form
86
+ * produce the same `PathKey`. Treat as opaque; don't try to parse.
87
+ */
88
+ type PathKey = string & {
89
+ readonly [pathKeyBrand]: 'PathKey';
90
+ };
91
+ /** A single path segment — a property name or array index. */
92
+ type Segment = string | number;
93
+ /** A structured path as a read-only sequence of segments. */
94
+ type Path = readonly Segment[];
95
+ /**
96
+ * Parse a dotted-string path into structured segments.
97
+ *
98
+ * ```ts
99
+ * parseDottedPath('user.address.line1') // ['user', 'address', 'line1']
100
+ * parseDottedPath('items.0.name') // ['items', 0, 'name']
101
+ * parseDottedPath('') // [''] (the empty-string key)
102
+ * ```
103
+ *
104
+ * The empty-string input `''` is the **literal empty-key path**, not
105
+ * the root. Use the array form `[]` for root. Form-level errors
106
+ * (root `.refine()`) live at the empty-string path bucket so
107
+ * `errors('')` returns them without sweeping every field error too.
108
+ *
109
+ * Throws `InvalidPathError` for paths with empty INTERNAL segments
110
+ * (`'a..b'`, leading or trailing dots). For keys containing literal
111
+ * dots, pass an array form (`['user.name']`) instead.
112
+ */
113
+ declare function parseDottedPath(path: string): Segment[];
114
+ /**
115
+ * Canonicalise a path into structured segments plus a stable string
116
+ * key. Accepts either dotted-string or array form; integer-looking
117
+ * segments normalise to numbers.
118
+ *
119
+ * ```ts
120
+ * canonicalizePath('items.0.name')
121
+ * // { segments: ['items', 0, 'name'], key: '["items",0,"name"]' as PathKey }
122
+ *
123
+ * canonicalizePath(['items', 0, 'name'])
124
+ * // → same result
125
+ * ```
126
+ *
127
+ * The returned `key` is suitable as a `Map`/`Set` key — equal paths
128
+ * produce equal keys regardless of input form.
129
+ */
130
+ declare function canonicalizePath(input: string | Path): {
131
+ segments: readonly Segment[];
132
+ key: PathKey;
133
+ };
134
+ /**
135
+ * The root path — an empty segment tuple. Pass to APIs that accept
136
+ * a `Path` to address the form value as a whole.
137
+ */
138
+ declare const ROOT_PATH: Path;
139
+ /** Stable string key for the root path. */
140
+ declare const ROOT_PATH_KEY: PathKey;
141
+ /**
142
+ * `true` when `path` starts with every segment of `prefix` (in order).
143
+ * The empty `prefix` matches every path — ROOT prefix is universal.
144
+ *
145
+ * Walks segments rather than `PathKey` strings because the data this
146
+ * helper operates on (e.g. `meta.errors[].path`) carries segment
147
+ * arrays directly.
148
+ *
149
+ * ```ts
150
+ * isPathPrefix(['cargo'], ['cargo', 'items', 0, 'sku']) // true
151
+ * isPathPrefix(['cargo', 'items'], ['cargo']) // false (path shorter)
152
+ * isPathPrefix([], ['anything']) // true (root prefix)
153
+ * ```
154
+ */
155
+ declare function isPathPrefix(prefix: readonly Segment[], path: readonly Segment[]): boolean;
156
+
157
+ /**
158
+ * Per-FormStore registry tracking which DOM elements have opted into
159
+ * persistence for which paths. Lives on the FormStore so that two SFCs
160
+ * sharing a key share the registry — opt-ins are per-element, not
161
+ * per-component.
162
+ *
163
+ * The directive's input handler computes `meta.persist` for each write
164
+ * by calling `hasOptIn(elementId, path)` — only THIS element's writes
165
+ * persist if THIS element opted in. Other call sites that aren't tied
166
+ * to a single element (history undo/redo, field-array helpers, devtools
167
+ * edits) use `hasAnyOptInForPath(path)` — persist if any element has
168
+ * opted into that path.
169
+ *
170
+ * Internal data structure: `Map<PathKey, Set<elementId>>`. Small forms
171
+ * have ~10-50 paths; iteration is cheap. All operations are O(1) given
172
+ * (id, path).
173
+ */
174
+ type PersistOptInRegistry = {
175
+ /** Add an opt-in entry; idempotent. */
176
+ add(elementId: string, path: PathKey): void;
177
+ /** Remove a single (element, path) entry. */
178
+ remove(elementId: string, path: PathKey): void;
179
+ /** Remove every opt-in for `elementId`. Called from directive's beforeUnmount. */
180
+ removeAllFor(elementId: string): void;
181
+ /** Check whether THIS element has opted into THIS path. */
182
+ hasOptIn(elementId: string, path: PathKey): boolean;
183
+ /** Check whether ANY element has opted into this path. */
184
+ hasAnyOptInForPath(path: PathKey): boolean;
185
+ /** Iterate every path that currently has at least one opt-in. */
186
+ optedInPaths(): IterableIterator<PathKey>;
187
+ /** True iff no element has opted into any path. */
188
+ isEmpty(): boolean;
189
+ /** Drop every entry. Called from FormStore.dispose. */
190
+ clear(): void;
191
+ };
192
+
76
193
  /** Internal brand for the `Unset` type. Never exposed at runtime. */
77
194
  declare const _unsetBrand: unique symbol;
78
195
  /**
@@ -545,6 +662,15 @@ type DefaultValuesInput<T> = T extends string ? string | Unset : T extends numbe
545
662
  [K in keyof T]?: DefaultValuesInput<T[K]>;
546
663
  } | Unset : T;
547
664
 
665
+ /**
666
+ * Identifier for a form. A `FormKey` is the string passed via
667
+ * `useForm({ key })`, used to look up a form by name from a distant
668
+ * component, namespace persisted drafts, and label errors and
669
+ * DevTools entries. Anonymous `useForm` calls allocate one
670
+ * automatically; you only need to pick one when the form needs
671
+ * stable identity.
672
+ */
673
+ type FormKey = string;
548
674
  /**
549
675
  * Per-form options threaded from `useForm` into the adapter factory.
550
676
  * Today carries the resolved `maxRecursionDepth` so adapter walks can
@@ -555,133 +681,6 @@ interface SchemaFactoryOptions {
555
681
  /** Resolved recursion ceiling (per-form > app-default > library default). */
556
682
  maxRecursionDepth: number;
557
683
  }
558
-
559
- /**
560
- * Path primitives for advanced integrations. The form library accepts
561
- * paths in dotted-string form (`'user.email'`) at every public API.
562
- * These primitives are exposed for adapter authors who need to
563
- * canonicalise user-provided paths.
564
- */
565
- declare const pathKeyBrand: unique symbol;
566
- /**
567
- * Branded string identifier for a canonicalised path. Useful as a
568
- * `Map` key — two paths that resolve to the same canonical form
569
- * produce the same `PathKey`. Treat as opaque; don't try to parse.
570
- */
571
- type PathKey = string & {
572
- readonly [pathKeyBrand]: 'PathKey';
573
- };
574
- /** A single path segment — a property name or array index. */
575
- type Segment = string | number;
576
- /** A structured path as a read-only sequence of segments. */
577
- type Path = readonly Segment[];
578
- /**
579
- * Parse a dotted-string path into structured segments.
580
- *
581
- * ```ts
582
- * parseDottedPath('user.address.line1') // ['user', 'address', 'line1']
583
- * parseDottedPath('items.0.name') // ['items', 0, 'name']
584
- * parseDottedPath('') // [''] (the empty-string key)
585
- * ```
586
- *
587
- * The empty-string input `''` is the **literal empty-key path**, not
588
- * the root. Use the array form `[]` for root. Form-level errors
589
- * (root `.refine()`) live at the empty-string path bucket so
590
- * `errors('')` returns them without sweeping every field error too.
591
- *
592
- * Throws `InvalidPathError` for paths with empty INTERNAL segments
593
- * (`'a..b'`, leading or trailing dots). For keys containing literal
594
- * dots, pass an array form (`['user.name']`) instead.
595
- */
596
- declare function parseDottedPath(path: string): Segment[];
597
- /**
598
- * Canonicalise a path into structured segments plus a stable string
599
- * key. Accepts either dotted-string or array form; integer-looking
600
- * segments normalise to numbers.
601
- *
602
- * ```ts
603
- * canonicalizePath('items.0.name')
604
- * // { segments: ['items', 0, 'name'], key: '["items",0,"name"]' as PathKey }
605
- *
606
- * canonicalizePath(['items', 0, 'name'])
607
- * // → same result
608
- * ```
609
- *
610
- * The returned `key` is suitable as a `Map`/`Set` key — equal paths
611
- * produce equal keys regardless of input form.
612
- */
613
- declare function canonicalizePath(input: string | Path): {
614
- segments: readonly Segment[];
615
- key: PathKey;
616
- };
617
- /**
618
- * The root path — an empty segment tuple. Pass to APIs that accept
619
- * a `Path` to address the form value as a whole.
620
- */
621
- declare const ROOT_PATH: Path;
622
- /** Stable string key for the root path. */
623
- declare const ROOT_PATH_KEY: PathKey;
624
- /**
625
- * `true` when `path` starts with every segment of `prefix` (in order).
626
- * The empty `prefix` matches every path — ROOT prefix is universal.
627
- *
628
- * Walks segments rather than `PathKey` strings because the data this
629
- * helper operates on (e.g. `meta.errors[].path`) carries segment
630
- * arrays directly.
631
- *
632
- * ```ts
633
- * isPathPrefix(['cargo'], ['cargo', 'items', 0, 'sku']) // true
634
- * isPathPrefix(['cargo', 'items'], ['cargo']) // false (path shorter)
635
- * isPathPrefix([], ['anything']) // true (root prefix)
636
- * ```
637
- */
638
- declare function isPathPrefix(prefix: readonly Segment[], path: readonly Segment[]): boolean;
639
-
640
- /**
641
- * Per-FormStore registry tracking which DOM elements have opted into
642
- * persistence for which paths. Lives on the FormStore so that two SFCs
643
- * sharing a key share the registry — opt-ins are per-element, not
644
- * per-component.
645
- *
646
- * The directive's input handler computes `meta.persist` for each write
647
- * by calling `hasOptIn(elementId, path)` — only THIS element's writes
648
- * persist if THIS element opted in. Other call sites that aren't tied
649
- * to a single element (history undo/redo, field-array helpers, devtools
650
- * edits) use `hasAnyOptInForPath(path)` — persist if any element has
651
- * opted into that path.
652
- *
653
- * Internal data structure: `Map<PathKey, Set<elementId>>`. Small forms
654
- * have ~10-50 paths; iteration is cheap. All operations are O(1) given
655
- * (id, path).
656
- */
657
- type PersistOptInRegistry = {
658
- /** Add an opt-in entry; idempotent. */
659
- add(elementId: string, path: PathKey): void;
660
- /** Remove a single (element, path) entry. */
661
- remove(elementId: string, path: PathKey): void;
662
- /** Remove every opt-in for `elementId`. Called from directive's beforeUnmount. */
663
- removeAllFor(elementId: string): void;
664
- /** Check whether THIS element has opted into THIS path. */
665
- hasOptIn(elementId: string, path: PathKey): boolean;
666
- /** Check whether ANY element has opted into this path. */
667
- hasAnyOptInForPath(path: PathKey): boolean;
668
- /** Iterate every path that currently has at least one opt-in. */
669
- optedInPaths(): IterableIterator<PathKey>;
670
- /** True iff no element has opted into any path. */
671
- isEmpty(): boolean;
672
- /** Drop every entry. Called from FormStore.dispose. */
673
- clear(): void;
674
- };
675
-
676
- /**
677
- * Identifier for a form. A `FormKey` is the string passed via
678
- * `useForm({ key })`, used to look up a form by name from a distant
679
- * component, namespace persisted drafts, and label errors and
680
- * DevTools entries. Anonymous `useForm` calls allocate one
681
- * automatically; you only need to pick one when the form needs
682
- * stable identity.
683
- */
684
- type FormKey = string;
685
684
  /**
686
685
  * One validation failure. `path` points at the offending field as a
687
686
  * structured array — `['user', 'address', 0, 'line1']` for a nested
@@ -1541,6 +1540,132 @@ type WriteMeta = {
1541
1540
  * consumer code.
1542
1541
  */
1543
1542
  readonly crossTab?: boolean;
1543
+ /**
1544
+ * When `true`, this write lands normally (storage, validation,
1545
+ * persistence, history) but does NOT notify `form.onChange` handlers.
1546
+ * The side-channel reacts to user edits, not programmatic rebaselines:
1547
+ * `reset()` tags its replacement with this flag, and the public
1548
+ * `setValue(path, value, { silent: true })` option forwards it so a
1549
+ * consumer hydrating the form (loading a saved record into the fields)
1550
+ * can land values without echoing each one back through an autosave
1551
+ * loop. The store's internal taggers and that single consumer-facing
1552
+ * option are the only writers.
1553
+ */
1554
+ readonly silent?: boolean;
1555
+ };
1556
+ /** Options for a `setValue` write. */
1557
+ type SetValueOptions = {
1558
+ /**
1559
+ * When `true`, the write lands normally (storage, validation, persistence,
1560
+ * history) but does NOT notify `form.onChange` handlers. Use it to hydrate
1561
+ * the form (load a saved record into the fields) without echoing every
1562
+ * field back through an autosave loop.
1563
+ */
1564
+ readonly silent?: boolean;
1565
+ };
1566
+ /**
1567
+ * A source address for `form.onChange` — the subtree(s) a handler reacts to.
1568
+ *
1569
+ * - a dotted path string (`'user.email'`) — one leaf or subtree;
1570
+ * - a list of path strings (`['shipping', 'billing']`) — react to any of
1571
+ * several paths; the handler fires once per matched path, `ctx.path`
1572
+ * distinguishing which;
1573
+ * - a getter or ref / computed resolving to either — re-read on each write,
1574
+ * so the aim can follow a moving target (the active list row). Re-aiming
1575
+ * is never itself a trigger; only a real write dispatches.
1576
+ *
1577
+ * Omit the source entirely (`form.onChange(handler)`) to react to the whole
1578
+ * form. An empty list (`form.onChange([], handler)`) lists zero paths, so it
1579
+ * never fires — that is a deliberate no-op, NOT a shorthand for the root.
1580
+ */
1581
+ type OnChangeSource = MaybeRefOrGetter<string | readonly string[]>;
1582
+ /**
1583
+ * Context handed to an `onChange` handler alongside the changed value.
1584
+ *
1585
+ * `onChange` is a pure side-channel: it reacts to value changes and runs
1586
+ * side effects, but never touches the form's own lifecycle. Nothing a
1587
+ * handler does here marks the form dirty, pending, or validating — autosave
1588
+ * status lives in the consumer's own state, validation in `.refine` and
1589
+ * `field.show*`.
1590
+ */
1591
+ type OnChangeContext<FormApi = unknown> = {
1592
+ /**
1593
+ * The source path this fire is for, in dotted form (`'user.email'`).
1594
+ * The empty string `''` is the whole form (a root handler). For a
1595
+ * multi-path source, the handler fires once per matched path and `path`
1596
+ * names which one.
1597
+ */
1598
+ readonly path: string;
1599
+ /**
1600
+ * The value at the source path BEFORE this change, seeded at registration.
1601
+ * Accurate for leaf sources. For a container or the whole form, an
1602
+ * in-place leaf edit preserves the container's reference, so `previous`
1603
+ * can be reference-equal to the current value (Vue's deep-watch gotcha) —
1604
+ * snapshot inside the handler if a true container diff is needed.
1605
+ */
1606
+ readonly previous: unknown;
1607
+ /**
1608
+ * Aborted when a newer write to the same source supersedes this run. A
1609
+ * debounced or awaiting handler should bail on `signal.aborted` (or pass
1610
+ * `signal` straight to `fetch`) so superseded work cancels itself.
1611
+ */
1612
+ readonly signal: AbortSignal;
1613
+ /** Retry counter — `0` on the first run, incremented by `onError`'s `retry()`. */
1614
+ readonly attempt: number;
1615
+ /**
1616
+ * The form handle, so a portable `useForm({ onChange })` handler can reach
1617
+ * back into the form (e.g. `ctx.form.validateAsync(ctx.path)` to gate an
1618
+ * autosave on validity). Typed precisely by the `form.onChange` overload.
1619
+ */
1620
+ readonly form: FormApi;
1621
+ /**
1622
+ * The leaf path(s), in dotted form, that actually changed in this
1623
+ * dispatch. For a leaf source this is just `[path]`; for a container or
1624
+ * the whole form it is every changed descendant.
1625
+ */
1626
+ readonly changed: readonly string[];
1627
+ };
1628
+ /**
1629
+ * Context handed to an `onChange` handler's `onError` callback when the
1630
+ * handler throws or rejects.
1631
+ */
1632
+ type OnChangeErrorContext<FormApi = unknown> = {
1633
+ /** The source path this run was for, in dotted form (`''` for the whole form). */
1634
+ readonly path: string;
1635
+ /** The value passed to the handler that failed. */
1636
+ readonly value: unknown;
1637
+ /** The attempt number that failed (`0` on the first run). */
1638
+ readonly attempt: number;
1639
+ /**
1640
+ * Re-run the handler with the same value and `attempt + 1`. A no-op once a
1641
+ * newer write has superseded this run — stale work is never resurrected.
1642
+ * Backoff and a retry cap are the consumer's to impose.
1643
+ */
1644
+ readonly retry: () => void;
1645
+ /** The form handle. Typed precisely by the `form.onChange` overload. */
1646
+ readonly form: FormApi;
1647
+ };
1648
+ /** A reaction to form value changes. Its return is ignored; throws route to `onError`. */
1649
+ type OnChangeHandler<Value = unknown, FormApi = unknown> = (value: Value, ctx: OnChangeContext<FormApi>) => void | Promise<void>;
1650
+ /** Handles a throw / rejection from an `onChange` handler. Must not throw. */
1651
+ type OnChangeErrorHandler<FormApi = unknown> = (error: unknown, ctx: OnChangeErrorContext<FormApi>) => void;
1652
+ /** Options for `form.onChange`. */
1653
+ type OnChangeOptions<FormApi = unknown> = {
1654
+ /**
1655
+ * Called when the handler throws or its promise rejects. Without it, a
1656
+ * failure is swallowed (logged in dev). `onChange` never throws into the
1657
+ * write that triggered it.
1658
+ */
1659
+ readonly onError?: OnChangeErrorHandler<FormApi>;
1660
+ };
1661
+ /**
1662
+ * The `useForm({ onChange })` option — a whole-form handler registered at
1663
+ * construction, bound to the form's lifetime. Either a bare handler or a
1664
+ * `{ handler, onError }` pair.
1665
+ */
1666
+ type OnChangeConfig<Value = unknown, FormApi = unknown> = OnChangeHandler<Value, FormApi> | {
1667
+ readonly handler: OnChangeHandler<Value, FormApi>;
1668
+ readonly onError?: OnChangeErrorHandler<FormApi>;
1544
1669
  };
1545
1670
  /**
1546
1671
  * Undo/redo configuration passed via `useForm({ history })`.
@@ -1826,6 +1951,17 @@ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema ext
1826
1951
  * per blur for `<input v-register.lazy>`).
1827
1952
  */
1828
1953
  debounceMs?: number;
1954
+ /**
1955
+ * A whole-form `onChange` handler, registered at construction and bound to
1956
+ * the form's lifetime. The same side-channel as `form.onChange(handler)`,
1957
+ * but declared in the options bag so it travels with the form (handy for a
1958
+ * `useAutosave`-style composable). Pass a handler, or `{ handler, onError }`.
1959
+ *
1960
+ * For path-scoped reactions, call `form.onChange('path', handler)` on the
1961
+ * returned form instead. `onChange` never touches the form's own
1962
+ * lifecycle — keep validation in `.refine` and `field.show*`.
1963
+ */
1964
+ onChange?: OnChangeConfig<Form, UseFormReturnType<Form>>;
1829
1965
  /**
1830
1966
  * Opt-in persistence of the form's draft state. Off by default —
1831
1967
  * with no config, no reads, no writes, no storage code is loaded.
@@ -4286,8 +4422,11 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
4286
4422
  * type at a leaf). Refinement-level mismatches (out-of-enum
4287
4423
  * values, failing format checks, etc.) succeed and surface as
4288
4424
  * field errors instead.
4425
+ *
4426
+ * Pass `{ silent: true }` to land the write without notifying
4427
+ * `form.onChange` handlers (e.g. hydrating a saved record).
4289
4428
  */
4290
- <Value extends SetValuePayload<DefaultValuesShape<Form>, WriteShape<Form>>>(value: Value): boolean;
4429
+ <Value extends SetValuePayload<DefaultValuesShape<Form>, WriteShape<Form>>>(value: Value, options?: SetValueOptions): boolean;
4291
4430
  /**
4292
4431
  * Write at a specific path. Pass a value or a callback receiving
4293
4432
  * the previous value at that path.
@@ -4305,14 +4444,49 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
4305
4444
  * it blank (storage holds the slim default; UI displays
4306
4445
  * empty; submit raises "No value supplied" for required schemas).
4307
4446
  */
4308
- <Path extends FlatPath<Form>, Value extends PathSetValuePayload<NestedType<Form, Path>>>(path: Path, value: Value): boolean;
4447
+ <Path extends FlatPath<Form>, Value extends PathSetValuePayload<NestedType<Form, Path>>>(path: Path, value: Value, options?: SetValueOptions): boolean;
4309
4448
  /**
4310
4449
  * Tuple-segment form. Equivalent to the dotted-string overload —
4311
4450
  * useful when paths are built from variables or arrays:
4312
4451
  * `form.setValue([prefix, 'line1'], 'value')`. The resolved leaf
4313
4452
  * type is exact, matching the dotted-string form.
4314
4453
  */
4315
- <const S extends ReadonlyArray<string | number>, Value extends PathSetValuePayload<NestedType<Form, JoinSegments<S>>>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : never), value: Value): boolean;
4454
+ <const S extends ReadonlyArray<string | number>, Value extends PathSetValuePayload<NestedType<Form, JoinSegments<S>>>>(segments: S & ([JoinSegments<S>] extends [FlatPath<Form>] ? unknown : never), value: Value, options?: SetValueOptions): boolean;
4455
+ };
4456
+ /**
4457
+ * Subscribe to form value changes — the side-channel autosave is built on.
4458
+ * Three call forms:
4459
+ *
4460
+ * - `form.onChange(handler, options?)` — react to the whole form.
4461
+ * - `form.onChange('user.email', handler, options?)` — react to one path;
4462
+ * `value` is that path's value.
4463
+ * - `form.onChange(source, handler, options?)` — react to a list of paths,
4464
+ * or a getter / ref resolving to a path or list (re-read on each write,
4465
+ * so the aim can follow a moving target like the active list row).
4466
+ *
4467
+ * The handler runs AFTER the value lands. Its return is ignored, and a
4468
+ * throw or rejection routes to `options.onError`, never into the write that
4469
+ * triggered it. Returns an idempotent `stop()`; called inside a component's
4470
+ * setup it also stops automatically on unmount.
4471
+ *
4472
+ * `onChange` is a pure side-channel: nothing it does marks the form dirty,
4473
+ * pending, or validating. Keep validation feedback in `.refine` and
4474
+ * `field.show*`, and track autosave status in your own state.
4475
+ *
4476
+ * ```ts
4477
+ * form.onChange('user.email', async (email, ctx) => {
4478
+ * const verdict = await ctx.form.validateAsync(ctx.path)
4479
+ * if (verdict.success) await api.save({ email }, { signal: ctx.signal })
4480
+ * }, { onError: (error, ctx) => ctx.retry() })
4481
+ * ```
4482
+ */
4483
+ onChange: {
4484
+ /** React to the whole form. `value` is the current form. */
4485
+ (handler: OnChangeHandler<ReadForm, UseFormReturnType<Form, GetValueFormType, ReadForm, K>>, options?: OnChangeOptions<UseFormReturnType<Form, GetValueFormType, ReadForm, K>>): () => void;
4486
+ /** React to one path. `value` is that path's value. */
4487
+ <P extends FlatPath<Form>>(source: P, handler: OnChangeHandler<NestedType<Form, P>, UseFormReturnType<Form, GetValueFormType, ReadForm, K>>, options?: OnChangeOptions<UseFormReturnType<Form, GetValueFormType, ReadForm, K>>): () => void;
4488
+ /** React to a list of paths, or a getter / ref / computed. `value` is unknown. */
4489
+ (source: OnChangeSource, handler: OnChangeHandler<unknown, UseFormReturnType<Form, GetValueFormType, ReadForm, K>>, options?: OnChangeOptions<UseFormReturnType<Form, GetValueFormType, ReadForm, K>>): () => void;
4316
4490
  };
4317
4491
  /**
4318
4492
  * Reactive validation status. Re-runs whenever the form (or the
@@ -4916,5 +5090,5 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
4916
5090
  blankPaths: ComputedRef<BlankPathsView>;
4917
5091
  };
4918
5092
 
4919
- export { ROOT_PATH as a0, ROOT_PATH_KEY as a1, canonicalizePath as av, isPathPrefix as aw, isUnset as ax, parseDottedPath as ay, unset as az };
4920
- export type { Primitive as $, AbstractSchema as A, HistoryConfig as B, CoercionEntry as C, DeepPartial as D, ErrorsProxyShape as E, FieldMetaPayload as F, GenericForm as G, HandleSubmit as H, IsTuple as I, IsUnion as J, JoinSegments as K, KeyofUnion as L, LiftedValueShape as M, MetaTrackerValue as N, NestedReadType as O, NestedType as P, OnError as Q, OnInvalidSubmitPolicy as R, OnSubmit as S, PartialFlatPath as T, Path as U, PathKey as V, PendingValidationStatus as W, PersistConfig as X, PersistConfigOptions as Y, PersistIncludeMode as Z, PersistOptInRegistry as _, ApiErrorDetails as a, ReactiveValidationStatus as a2, RegisterDirective as a3, RegisterFlatPath as a4, RegisterModelDynamicCustomDirective as a5, RegisterOptions as a6, RegisterSelectModifier as a7, RegisterTextModifier as a8, RegisterTransform as a9, RegisterValue as aa, SchemaFactoryOptions as ab, Segment as ac, SetValueCallback as ad, SetValuePayload as ae, SettledValidationStatus as af, SlimPrimitiveKind as ag, SlimRuntimeOf as ah, SubmitHandler as ai, TransformAbortHolder as aj, Unset as ak, UseFormConfiguration as al, UseFormReturnType as am, ValidateOn as an, ValidateOnConfig as ao, ValidationError as ap, ValidationResponse as aq, ValidationResponseWithoutValue as ar, ValueOfUnion as as, WriteMeta as at, WriteShape as au, ApiErrorEntry as b, ApiErrorEnvelope as c, ArrayItem as d, ArrayPath as e, AttaformDefaults as f, CoercionRegistry as g, CoercionResult as h, CustomDirectiveRegisterAssignerFn as i, DefaultValuesInput as j, DefaultValuesResponse as k, DefaultValuesShape as l, DisplayCtx as m, DisplayMachine as n, DisplayState as o, FieldState as p, FieldStateMap as q, FieldStateMapEntry as r, FlatPath as s, FormErrorRecord as t, FormErrorsSurface as u, FormKey as v, FormMeta as w, FormStorage as x, FormStorageKind as y, GetDisplayState as z };
5093
+ export { canonicalizePath as aA, isPathPrefix as aB, isUnset as aC, parseDottedPath as aD, unset as aE, ROOT_PATH as ab, ROOT_PATH_KEY as ac };
5094
+ export type { OnChangeSource as $, AttaformDefaults as A, FormStorage as B, CoercionEntry as C, DefaultValuesInput as D, ErrorsProxyShape as E, FormKey as F, GenericForm as G, FormStorageKind as H, HandleSubmit as I, HistoryConfig as J, IsTuple as K, IsUnion as L, JoinSegments as M, KeyofUnion as N, LiftedValueShape as O, MetaTrackerValue as P, NestedReadType as Q, RegisterModelDynamicCustomDirective as R, NestedType as S, OnChangeConfig as T, UseFormConfiguration as U, ValidationError as V, OnChangeContext as W, OnChangeErrorContext as X, OnChangeErrorHandler as Y, OnChangeHandler as Z, OnChangeOptions as _, AbstractSchema as a, OnError as a0, OnInvalidSubmitPolicy as a1, OnSubmit as a2, PartialFlatPath as a3, Path as a4, PathKey as a5, PendingValidationStatus as a6, PersistConfig as a7, PersistConfigOptions as a8, PersistIncludeMode as a9, SchemaFactoryOptions as aF, TransformAbortHolder as aG, PersistOptInRegistry as aH, Primitive as aa, ReactiveValidationStatus as ad, RegisterDirective as ae, RegisterFlatPath as af, RegisterOptions as ag, RegisterSelectModifier as ah, RegisterTextModifier as ai, RegisterTransform as aj, Segment as ak, SetValueCallback as al, SetValueOptions as am, SetValuePayload as an, SettledValidationStatus as ao, SlimPrimitiveKind as ap, SlimRuntimeOf as aq, SubmitHandler as ar, Unset as as, ValidateOn as at, ValidateOnConfig as au, ValidationResponse as av, ValidationResponseWithoutValue as aw, ValueOfUnion as ax, WriteMeta as ay, WriteShape as az, UseFormReturnType as b, RegisterValue as c, GetDisplayState as d, ApiErrorEnvelope as e, ApiErrorDetails as f, ApiErrorEntry as g, ArrayItem as h, ArrayPath as i, CoercionRegistry as j, CoercionResult as k, CustomDirectiveRegisterAssignerFn as l, DeepPartial as m, DefaultValuesResponse as n, DefaultValuesShape as o, DisplayCtx as p, DisplayMachine as q, DisplayState as r, FieldMetaPayload as s, FieldState as t, FieldStateMap as u, FieldStateMapEntry as v, FlatPath as w, FormErrorRecord as x, FormErrorsSurface as y, FormMeta as z };