formstand 0.14.0 → 0.15.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/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.0 — 2026-08-04
4
+
5
+ ### Fixed
6
+
7
+ - **`persistForm` no longer applies a draft from a different schema shape.**
8
+ The stored JSON was cast straight to the values type and adopted, so a draft
9
+ written before a schema change rebased the form onto values its own schema
10
+ rejects. Because the default `adopt` mode clears errors and resets the
11
+ baseline, the form then read **clean** while holding them, which is the worst
12
+ version of the failure.
13
+
14
+ A draft is now ignored when its shape conflicts with the form's: a path
15
+ holding a string where the form expects an object, an array of strings where
16
+ it expects rows. Only overlapping paths are compared, and only their kinds,
17
+ so a half-filled draft still restores (a draft is work in progress and would
18
+ fail full validation by design), and so does one that filled an optional the
19
+ initial values left empty.
20
+
21
+ ### Added
22
+
23
+ - **`persistForm`'s `version` option.** Renames and removals are invisible to
24
+ the shape guard above, because JSON drops undefined slots and an absent key
25
+ is indistinguishable from an unfilled optional. `version` covers those: it is
26
+ stored with the draft and a mismatch discards it. Leaving it unset keeps the
27
+ stored format byte-identical, so existing drafts survive the upgrade.
28
+
3
29
  ## 0.14.0 — 2026-07-31
4
30
 
5
31
  ### Added
package/README.md CHANGED
@@ -12,6 +12,7 @@ Zod-schema-first form state for React 19, backed by zustand.
12
12
  - **Field arrays** with stable IDs that survive reorders, inserts, and removes.
13
13
  - **Bound input components** with the accessibility wiring already done.
14
14
  - **A generator**: [`formstand-cli`](https://scrumrot.github.io/formstand/documentation/cli/) writes the whole component from your schema, for six UI kits.
15
+ - **[Devtools](https://scrumrot.github.io/formstand/documentation/devtools)**: `formstand/devtools` is a panel showing every field, both error channels, and the live diff. Renders nothing in production.
15
16
 
16
17
  ## Install
17
18
 
@@ -111,6 +112,7 @@ PATCH-ready payload of exactly what changed.
111
112
  | Components | `TextField`, `NumberField`, `DateField`, `CheckboxField`, `SelectField`, the prop builders, `useNumberInput`, `focusField`/`focusFirstError` | [Components & bindings](https://scrumrot.github.io/formstand/documentation/api/components) |
112
113
  | Validation | five modes plus `reValidateMode`, field-scoped passes, async refines with race handling, per-field `debounceMs` | [Validation](https://scrumrot.github.io/formstand/documentation/validation) |
113
114
  | Utilities | `parsePath`/`getAtPath`/`setAtPath`, `validateSync`/`validateAsync`, `persistForm`, and the full type list | [Utilities & types](https://scrumrot.github.io/formstand/documentation/api/utilities) |
115
+ | Devtools | `<FormstandDevtools form={form} />` from `formstand/devtools` | [Devtools](https://scrumrot.github.io/formstand/documentation/devtools) |
114
116
 
115
117
  Coming from react-hook-form? The
116
118
  [migration guide](https://scrumrot.github.io/formstand/documentation/migrating-from-react-hook-form)
package/dist/index.cjs CHANGED
@@ -1140,6 +1140,25 @@ var shouldValidateOn = (trigger, mode, reValidateMode, submitAttempted, touched
1140
1140
  };
1141
1141
 
1142
1142
  // src/core/persist.ts
1143
+ var kindOf = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
1144
+ var conflictsWith = (draft, reference) => {
1145
+ if (draft === void 0 || draft === null) return false;
1146
+ if (reference === void 0 || reference === null) return false;
1147
+ const draftKind = kindOf(draft);
1148
+ const referenceKind = kindOf(reference);
1149
+ if (draftKind !== referenceKind) return true;
1150
+ if (draftKind === "array") {
1151
+ const [d] = draft;
1152
+ const [r] = reference;
1153
+ return conflictsWith(d, r);
1154
+ }
1155
+ if (draftKind === "object") {
1156
+ const d = draft;
1157
+ const r = reference;
1158
+ return Object.keys(d).some((key) => conflictsWith(d[key], r[key]));
1159
+ }
1160
+ return false;
1161
+ };
1143
1162
  var defaultStorage = () => {
1144
1163
  try {
1145
1164
  return typeof globalThis.localStorage === "undefined" ? null : globalThis.localStorage;
@@ -1152,11 +1171,29 @@ var persistForm = (form, options) => {
1152
1171
  const debounceMs = options.debounceMs ?? 300;
1153
1172
  const apply = options.apply ?? "adopt";
1154
1173
  const applyDraft = (values) => apply === "restore" ? form.setValues(values) : form.adoptValues(values);
1174
+ const referenceShape = () => {
1175
+ try {
1176
+ return JSON.parse(JSON.stringify(form.getState().initialValues));
1177
+ } catch {
1178
+ return void 0;
1179
+ }
1180
+ };
1155
1181
  const restore = () => {
1156
1182
  try {
1157
1183
  const raw = storage === null ? null : storage.getItem(options.key);
1158
1184
  if (raw === null) return false;
1159
- applyDraft(JSON.parse(raw));
1185
+ const parsed = JSON.parse(raw);
1186
+ const versioned = options.version !== void 0 && typeof parsed === "object" && parsed !== null && "__v" in parsed;
1187
+ if (options.version !== void 0 && !versioned) return false;
1188
+ const wrapper = parsed;
1189
+ if (versioned && wrapper.__v !== options.version) return false;
1190
+ if (versioned && wrapper.values === void 0) return false;
1191
+ const values = versioned ? wrapper.values : parsed;
1192
+ const reference = referenceShape();
1193
+ if (reference !== void 0 && conflictsWith(values, reference)) {
1194
+ return false;
1195
+ }
1196
+ applyDraft(values);
1160
1197
  return true;
1161
1198
  } catch {
1162
1199
  return false;
@@ -1164,7 +1201,12 @@ var persistForm = (form, options) => {
1164
1201
  };
1165
1202
  const writeDraft = (values) => {
1166
1203
  try {
1167
- storage?.setItem(options.key, JSON.stringify(values));
1204
+ storage?.setItem(
1205
+ options.key,
1206
+ JSON.stringify(
1207
+ options.version === void 0 ? values : { __v: options.version, values }
1208
+ )
1209
+ );
1168
1210
  } catch {
1169
1211
  }
1170
1212
  };