formstand 0.20.0 → 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.
- package/CHANGELOG.md +374 -1
- package/README.md +13 -4
- package/dist/{chunk-N7WPXYCU.js → chunk-BMXIZEI4.js} +23 -6
- package/dist/chunk-BMXIZEI4.js.map +1 -0
- package/dist/{chunk-MQ6262S3.js → chunk-V3PDMTPP.js} +7 -3
- package/dist/chunk-V3PDMTPP.js.map +1 -0
- package/dist/{createForm-B73p1TgL.d.cts → createForm-BJum0RBu.d.cts} +10 -7
- package/dist/{createForm-B73p1TgL.d.ts → createForm-BJum0RBu.d.ts} +10 -7
- package/dist/devtools/index.cjs +17 -4
- package/dist/devtools/index.cjs.map +1 -1
- package/dist/devtools/index.d.cts +7 -5
- package/dist/devtools/index.d.ts +7 -5
- package/dist/devtools/index.js +2 -3
- package/dist/devtools/index.js.map +1 -1
- package/dist/index.cjs +251 -125
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -15
- package/dist/index.d.ts +18 -15
- package/dist/index.js +231 -126
- package/dist/index.js.map +1 -1
- package/dist/testing/index.cjs +4 -1
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +2 -2
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/testing/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-MQ6262S3.js.map +0 -1
- package/dist/chunk-N7WPXYCU.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -112,8 +112,10 @@ var isPlainObject = (v) => {
|
|
|
112
112
|
};
|
|
113
113
|
var valuesEqual = (a, b) => {
|
|
114
114
|
if (Object.is(a, b)) return true;
|
|
115
|
+
if (typeof a === "number" && typeof b === "number") return a === b;
|
|
115
116
|
if (a instanceof Date && b instanceof Date) {
|
|
116
|
-
|
|
117
|
+
const [ta, tb] = [a.getTime(), b.getTime()];
|
|
118
|
+
return ta === tb || Number.isNaN(ta) && Number.isNaN(tb);
|
|
117
119
|
}
|
|
118
120
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
119
121
|
return a.length === b.length && a.every((v, i) => valuesEqual(v, b[i]));
|
|
@@ -143,6 +145,18 @@ var isFieldDirty = (value, initialValue) => {
|
|
|
143
145
|
return dirty;
|
|
144
146
|
};
|
|
145
147
|
|
|
148
|
+
// src/core/env.ts
|
|
149
|
+
var isDevBuild = () => typeof process === "undefined" || process.env?.["NODE_ENV"] !== "production";
|
|
150
|
+
var isDevToolsBuild = () => {
|
|
151
|
+
if (typeof process === "undefined") return false;
|
|
152
|
+
const env = process.env?.["NODE_ENV"];
|
|
153
|
+
return typeof env === "string" && env !== "production";
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// src/core/maps.ts
|
|
157
|
+
var entryAt = (map, key) => Object.hasOwn(map, key) ? map[key] : void 0;
|
|
158
|
+
var flagAt = (map, key) => Object.hasOwn(map, key) && map[key] === true;
|
|
159
|
+
|
|
146
160
|
// src/core/path.ts
|
|
147
161
|
var MAX_ARRAY_INDEX = 1e5;
|
|
148
162
|
var isIndexSegment = (s) => {
|
|
@@ -153,11 +167,18 @@ var PARSE_CACHE_MAX = 4096;
|
|
|
153
167
|
var parseCache = /* @__PURE__ */ new Map();
|
|
154
168
|
var parsePath = (path) => {
|
|
155
169
|
const cached = parseCache.get(path);
|
|
156
|
-
if (cached !== void 0)
|
|
170
|
+
if (cached !== void 0) {
|
|
171
|
+
parseCache.delete(path);
|
|
172
|
+
parseCache.set(path, cached);
|
|
173
|
+
return cached;
|
|
174
|
+
}
|
|
157
175
|
const segments = Object.freeze(
|
|
158
176
|
path === "" ? [] : path.split(".").map((s) => isIndexSegment(s) ? Number(s) : s)
|
|
159
177
|
);
|
|
160
|
-
if (parseCache.size >= PARSE_CACHE_MAX)
|
|
178
|
+
if (parseCache.size >= PARSE_CACHE_MAX) {
|
|
179
|
+
const oldest = parseCache.keys().next().value;
|
|
180
|
+
if (oldest !== void 0) parseCache.delete(oldest);
|
|
181
|
+
}
|
|
161
182
|
parseCache.set(path, segments);
|
|
162
183
|
return segments;
|
|
163
184
|
};
|
|
@@ -246,10 +267,17 @@ var expandIssue = (issue, prefix) => {
|
|
|
246
267
|
}
|
|
247
268
|
return [{ path: issuePathToFormPath(at), message: issue.message }];
|
|
248
269
|
};
|
|
249
|
-
var flattenIssues = (issues) =>
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
270
|
+
var flattenIssues = (issues) => Object.fromEntries(
|
|
271
|
+
issues.flatMap((issue) => expandIssue(issue, [])).reduce((acc, { path, message }) => {
|
|
272
|
+
const existing = acc.get(path);
|
|
273
|
+
if (existing === void 0) {
|
|
274
|
+
acc.set(path, [message]);
|
|
275
|
+
} else if (!existing.includes(message)) {
|
|
276
|
+
existing.push(message);
|
|
277
|
+
}
|
|
278
|
+
return acc;
|
|
279
|
+
}, /* @__PURE__ */ new Map())
|
|
280
|
+
);
|
|
253
281
|
var validateSync = (schema, values) => {
|
|
254
282
|
const result = schema.safeParse(values);
|
|
255
283
|
return result.success ? { kind: "valid", data: result.data } : { kind: "invalid", errors: flattenIssues(result.error.issues) };
|
|
@@ -314,6 +342,34 @@ var hasChecks = (s) => {
|
|
|
314
342
|
const checks = s.def.checks;
|
|
315
343
|
return checks !== void 0 && checks.length > 0;
|
|
316
344
|
};
|
|
345
|
+
var fieldSchemaForIntrospection = (schema, path) => {
|
|
346
|
+
const childOf = (s, segment) => {
|
|
347
|
+
const inner = unwrapForWalk(s);
|
|
348
|
+
if (inner !== null) return childOf(inner, segment);
|
|
349
|
+
if (s instanceof zod.z.ZodObject && typeof segment === "string") {
|
|
350
|
+
const shape = s.shape;
|
|
351
|
+
return Object.hasOwn(shape, segment) ? shape[segment] ?? null : null;
|
|
352
|
+
}
|
|
353
|
+
if (s instanceof zod.z.ZodArray && typeof segment === "number") {
|
|
354
|
+
return s.element;
|
|
355
|
+
}
|
|
356
|
+
if (s instanceof zod.z.ZodTuple && typeof segment === "number") {
|
|
357
|
+
return s.def.items[segment] ?? null;
|
|
358
|
+
}
|
|
359
|
+
if (s instanceof zod.z.ZodDiscriminatedUnion || s instanceof zod.z.ZodUnion) {
|
|
360
|
+
const options = s.def.options;
|
|
361
|
+
return options.reduce(
|
|
362
|
+
(found, option) => found ?? childOf(option, segment),
|
|
363
|
+
null
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
};
|
|
368
|
+
return parsePath(path).reduce(
|
|
369
|
+
(current, segment) => current === null ? null : childOf(current, segment),
|
|
370
|
+
schema
|
|
371
|
+
);
|
|
372
|
+
};
|
|
317
373
|
var fieldSchemaAtPath = (schema, path) => parsePath(path).reduce((current, segment) => {
|
|
318
374
|
if (current === null || hasChecks(current)) return null;
|
|
319
375
|
if (current instanceof zod.z.ZodObject && typeof segment === "string") {
|
|
@@ -332,6 +388,12 @@ var emptyErrors = {};
|
|
|
332
388
|
var emptyBools = {};
|
|
333
389
|
var identityMapper = (n) => n;
|
|
334
390
|
var omitKey = (map, key) => Object.fromEntries(Object.entries(map).filter(([k]) => k !== key));
|
|
391
|
+
var omitKeys = (map, keys) => {
|
|
392
|
+
if (keys.size === 0) return map;
|
|
393
|
+
const entries = Object.entries(map);
|
|
394
|
+
const kept = entries.filter(([k]) => !keys.has(k));
|
|
395
|
+
return kept.length === entries.length ? map : Object.fromEntries(kept);
|
|
396
|
+
};
|
|
335
397
|
var inScopeOfAny = (key, paths) => paths.some((p) => isPathOrChild(key, p));
|
|
336
398
|
var omitScope = (map, paths) => {
|
|
337
399
|
const entries = Object.entries(map);
|
|
@@ -370,13 +432,13 @@ var stringArraysEqual = (a, b) => a === b || a.length === b.length && a.every((v
|
|
|
370
432
|
var reuseErrorRefs = (next, prev) => {
|
|
371
433
|
if (next === prev) return prev;
|
|
372
434
|
const entries = Object.entries(next).map(([k, v]) => {
|
|
373
|
-
const old = prev
|
|
435
|
+
const old = entryAt(prev, k);
|
|
374
436
|
return [
|
|
375
437
|
k,
|
|
376
438
|
old !== void 0 && stringArraysEqual(old, v) ? old : v
|
|
377
439
|
];
|
|
378
440
|
});
|
|
379
|
-
const sameShape = entries.length === Object.keys(prev).length && entries.every(([k, v]) => prev
|
|
441
|
+
const sameShape = entries.length === Object.keys(prev).length && entries.every(([k, v]) => entryAt(prev, k) === v);
|
|
380
442
|
return sameShape ? prev : Object.fromEntries(entries);
|
|
381
443
|
};
|
|
382
444
|
var scopeSettledResult = (result, path, viaSubschema) => result.kind !== "invalid" ? emptyErrors : viaSubschema ? prefixErrorKeys(result.errors, path) : path === "" ? result.errors : pickScope(result.errors, [path]);
|
|
@@ -433,9 +495,10 @@ var createForm = (schema, options) => {
|
|
|
433
495
|
// opting in during development must NOT stream every keystroke to
|
|
434
496
|
// an end user who happens to have the extension installed, where
|
|
435
497
|
// a module-singleton form's unbounded time-travel history is a
|
|
436
|
-
// real memory + serialization cost.
|
|
437
|
-
// without
|
|
438
|
-
|
|
498
|
+
// real memory + serialization cost. Fail-closed gate: a runtime
|
|
499
|
+
// without a readable NODE_ENV (CDN/ESM, Deno, a worker) reads as
|
|
500
|
+
// production here, unlike the console-warning gate.
|
|
501
|
+
enabled: isDevToolsBuild()
|
|
439
502
|
})
|
|
440
503
|
);
|
|
441
504
|
const commitFullPass = (state, next) => errorChannels(state, {
|
|
@@ -551,13 +614,19 @@ var createForm = (schema, options) => {
|
|
|
551
614
|
const slot = slotAtPath(values, path);
|
|
552
615
|
return slot.exists ? { kind: "parse", schema: sub, value: slot.value, viaSubschema: true } : { kind: "skip" };
|
|
553
616
|
};
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
);
|
|
617
|
+
const parseScopeAsync = async (path, scope) => scope.kind === "skip" ? emptyErrors : scopeSettledResult(
|
|
618
|
+
await validateAsync(scope.schema, scope.value),
|
|
619
|
+
path,
|
|
620
|
+
scope.viaSubschema
|
|
621
|
+
);
|
|
622
|
+
const scopeStale = (scope, path, valuesAtStart, live) => {
|
|
623
|
+
if (live === valuesAtStart) return false;
|
|
624
|
+
switch (scope.kind) {
|
|
625
|
+
case "skip":
|
|
626
|
+
return slotAtPath(live, path).exists;
|
|
627
|
+
case "parse":
|
|
628
|
+
return scope.viaSubschema ? !Object.is(getAtPath(live, path), scope.value) : true;
|
|
629
|
+
}
|
|
561
630
|
};
|
|
562
631
|
const fieldResult = (scoped) => {
|
|
563
632
|
const messages = Object.values(scoped).flat();
|
|
@@ -605,10 +674,10 @@ var createForm = (schema, options) => {
|
|
|
605
674
|
const value = getAtPath(state.values, path);
|
|
606
675
|
return {
|
|
607
676
|
value,
|
|
608
|
-
error: state.errors
|
|
609
|
-
touched: state.touched
|
|
677
|
+
error: entryAt(state.errors, path),
|
|
678
|
+
touched: flagAt(state.touched, path),
|
|
610
679
|
dirty: isFieldDirty(value, getAtPath(state.initialValues, path)),
|
|
611
|
-
isValidating: state.isValidating
|
|
680
|
+
isValidating: flagAt(state.isValidating, path)
|
|
612
681
|
};
|
|
613
682
|
};
|
|
614
683
|
const sequences = /* @__PURE__ */ new Map();
|
|
@@ -647,44 +716,66 @@ var createForm = (schema, options) => {
|
|
|
647
716
|
}
|
|
648
717
|
const passes = new Map(paths.map((p) => [p, claimPass(p)]));
|
|
649
718
|
const valuesAtStart = store.getState().values;
|
|
719
|
+
const scopes = paths.map((path) => ({
|
|
720
|
+
path,
|
|
721
|
+
scope: fieldScopeFor(path, valuesAtStart)
|
|
722
|
+
}));
|
|
723
|
+
const allViaSubschema = scopes.every(
|
|
724
|
+
({ scope }) => scope.kind === "skip" || scope.kind === "parse" && scope.viaSubschema
|
|
725
|
+
);
|
|
650
726
|
store.setState((state) => ({
|
|
651
727
|
...state,
|
|
652
|
-
isValidating:
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
728
|
+
isValidating: {
|
|
729
|
+
...state.isValidating,
|
|
730
|
+
...Object.fromEntries(paths.map((p) => [p, true]))
|
|
731
|
+
}
|
|
656
732
|
}));
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
|
|
733
|
+
const parsed = allViaSubschema ? {
|
|
734
|
+
kind: "slots",
|
|
735
|
+
byPath: new Map(
|
|
736
|
+
await Promise.all(
|
|
737
|
+
scopes.map(
|
|
738
|
+
async ({ path, scope }) => [path, await parseScopeAsync(path, scope)]
|
|
739
|
+
)
|
|
740
|
+
)
|
|
741
|
+
)
|
|
742
|
+
} : {
|
|
743
|
+
kind: "full",
|
|
744
|
+
errors: await validateAsync(schema, valuesAtStart).then(
|
|
745
|
+
(result) => result.kind === "invalid" ? result.errors : emptyErrors
|
|
746
|
+
)
|
|
747
|
+
};
|
|
748
|
+
const errorsFor = (path) => {
|
|
749
|
+
switch (parsed.kind) {
|
|
750
|
+
case "slots":
|
|
751
|
+
return parsed.byPath.get(path) ?? emptyErrors;
|
|
752
|
+
case "full":
|
|
753
|
+
return pickScope(parsed.errors, [path]);
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
const requestedErrors = Object.fromEntries(
|
|
757
|
+
paths.flatMap((path) => Object.entries(errorsFor(path)))
|
|
758
|
+
);
|
|
660
759
|
const requested = settledFields(requestedErrors);
|
|
661
760
|
const live = store.getState();
|
|
662
|
-
const
|
|
663
|
-
(
|
|
761
|
+
const owned = scopes.filter(
|
|
762
|
+
({ path }) => sequences.get(path) === passes.get(path)
|
|
664
763
|
);
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
(acc, p) => omitKey(acc, p),
|
|
671
|
-
state.isValidating
|
|
672
|
-
)
|
|
673
|
-
}));
|
|
674
|
-
}
|
|
675
|
-
return requested;
|
|
676
|
-
}
|
|
764
|
+
const fresh = owned.filter(
|
|
765
|
+
({ path, scope }) => !scopeStale(scope, path, valuesAtStart, live.values)
|
|
766
|
+
).map(({ path }) => path);
|
|
767
|
+
const ownedKeys = new Set(owned.map(({ path }) => path));
|
|
768
|
+
if (owned.length === 0) return requested;
|
|
677
769
|
store.setState((state) => ({
|
|
678
770
|
...state,
|
|
679
|
-
...commitScopedErrors(
|
|
771
|
+
...fresh.length === 0 ? {} : commitScopedErrors(
|
|
680
772
|
state,
|
|
681
|
-
|
|
682
|
-
|
|
773
|
+
Object.fromEntries(
|
|
774
|
+
fresh.flatMap((path) => Object.entries(errorsFor(path)))
|
|
775
|
+
),
|
|
776
|
+
fresh
|
|
683
777
|
),
|
|
684
|
-
isValidating:
|
|
685
|
-
(acc, p) => omitKey(acc, p),
|
|
686
|
-
state.isValidating
|
|
687
|
-
)
|
|
778
|
+
isValidating: omitKeys(state.isValidating, ownedKeys)
|
|
688
779
|
}));
|
|
689
780
|
return requested;
|
|
690
781
|
};
|
|
@@ -698,14 +789,15 @@ var createForm = (schema, options) => {
|
|
|
698
789
|
}
|
|
699
790
|
const pass = claimPass(path);
|
|
700
791
|
const valuesAtStart = store.getState().values;
|
|
792
|
+
const scope = fieldScopeFor(path, valuesAtStart);
|
|
701
793
|
store.setState((state) => ({
|
|
702
794
|
...state,
|
|
703
795
|
isValidating: { ...state.isValidating, [path]: true }
|
|
704
796
|
}));
|
|
705
|
-
const scoped = await
|
|
797
|
+
const scoped = await parseScopeAsync(path, scope);
|
|
706
798
|
const live = store.getState();
|
|
707
799
|
const stillOwns = sequences.get(path) === pass;
|
|
708
|
-
if (!stillOwns || live.values
|
|
800
|
+
if (!stillOwns || scopeStale(scope, path, valuesAtStart, live.values)) {
|
|
709
801
|
if (stillOwns) {
|
|
710
802
|
store.setState((state) => ({
|
|
711
803
|
...state,
|
|
@@ -940,17 +1032,24 @@ var createForm = (schema, options) => {
|
|
|
940
1032
|
...state,
|
|
941
1033
|
...errorChannels(state, { serverErrors: errors })
|
|
942
1034
|
})),
|
|
943
|
-
addErrors: (errors) => store.setState((state) =>
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1035
|
+
addErrors: (errors) => store.setState((state) => {
|
|
1036
|
+
const entries = Object.entries(errors);
|
|
1037
|
+
const removed = new Set(
|
|
1038
|
+
entries.filter(([, list]) => list === void 0 || list.length === 0).map(([key]) => key)
|
|
1039
|
+
);
|
|
1040
|
+
const added = Object.fromEntries(
|
|
1041
|
+
entries.filter(([, list]) => list !== void 0 && list.length > 0)
|
|
1042
|
+
);
|
|
1043
|
+
return {
|
|
1044
|
+
...state,
|
|
1045
|
+
...errorChannels(state, {
|
|
1046
|
+
serverErrors: {
|
|
1047
|
+
...omitKeys(state.serverErrors, removed),
|
|
1048
|
+
...added
|
|
1049
|
+
}
|
|
1050
|
+
})
|
|
1051
|
+
};
|
|
1052
|
+
}),
|
|
954
1053
|
clearErrors: (path) => store.setState((state) => {
|
|
955
1054
|
const scrub = (map) => path === void 0 ? emptyErrors : omitScope(map, [path]);
|
|
956
1055
|
return {
|
|
@@ -962,27 +1061,26 @@ var createForm = (schema, options) => {
|
|
|
962
1061
|
};
|
|
963
1062
|
}),
|
|
964
1063
|
updateState: (updater) => {
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
return {
|
|
976
|
-
...next,
|
|
977
|
-
...errorChannels(state, {
|
|
978
|
-
schemaErrors: next.schemaErrors,
|
|
979
|
-
serverErrors: next.serverErrors
|
|
980
|
-
})
|
|
981
|
-
};
|
|
982
|
-
});
|
|
983
|
-
if (store.getState().values !== valuesBefore) {
|
|
1064
|
+
const state = store.getState();
|
|
1065
|
+
const patch = updater(state);
|
|
1066
|
+
if (Object.keys(patch).length === 0) return;
|
|
1067
|
+
if (patch.errors !== void 0 && patch.errors !== state.errors) {
|
|
1068
|
+
console.warn(
|
|
1069
|
+
"[formstand] `errors` is derived from schemaErrors/serverErrors \u2014 patch those channels instead; the direct `errors` patch is ignored."
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
const next = { ...state, ...patch };
|
|
1073
|
+
if (next.values !== state.values) {
|
|
984
1074
|
clearArrayOpsUnder(store, "");
|
|
985
1075
|
}
|
|
1076
|
+
const serverErrors = patch.serverErrors !== void 0 ? patch.serverErrors : next.values !== state.values ? releaseChangedSlices(state.serverErrors, state.values, next.values) : state.serverErrors;
|
|
1077
|
+
store.setState(() => ({
|
|
1078
|
+
...next,
|
|
1079
|
+
...errorChannels(state, {
|
|
1080
|
+
schemaErrors: next.schemaErrors,
|
|
1081
|
+
serverErrors
|
|
1082
|
+
})
|
|
1083
|
+
}));
|
|
986
1084
|
},
|
|
987
1085
|
adoptValues: (values) => {
|
|
988
1086
|
sequences.clear();
|
|
@@ -1091,7 +1189,7 @@ var createForm = (schema, options) => {
|
|
|
1091
1189
|
handleSubmit: (onValid, onInvalid, opts) => async (event) => {
|
|
1092
1190
|
event?.preventDefault();
|
|
1093
1191
|
const result = await submit(onValid, onInvalid, opts);
|
|
1094
|
-
if (result.kind === "error" && opts?.onError === void 0 &&
|
|
1192
|
+
if (result.kind === "error" && opts?.onError === void 0 && isDevBuild()) {
|
|
1095
1193
|
console.warn(
|
|
1096
1194
|
"[formstand] submit handler threw and nothing observed it \u2014 the form shows no feedback. Pass { onError } to handleSubmit (or setError inside your handler):",
|
|
1097
1195
|
result.error
|
|
@@ -1159,6 +1257,35 @@ var shouldValidateOn = (trigger, mode, reValidateMode, submitAttempted, touched
|
|
|
1159
1257
|
}
|
|
1160
1258
|
};
|
|
1161
1259
|
|
|
1260
|
+
// src/core/debounce.ts
|
|
1261
|
+
var createDebouncer = (ms) => {
|
|
1262
|
+
const timer = {
|
|
1263
|
+
current: null
|
|
1264
|
+
};
|
|
1265
|
+
const cancel = () => {
|
|
1266
|
+
if (timer.current !== null) {
|
|
1267
|
+
clearTimeout(timer.current);
|
|
1268
|
+
timer.current = null;
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
const schedule = (run) => {
|
|
1272
|
+
cancel();
|
|
1273
|
+
if (ms === 0) {
|
|
1274
|
+
run();
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
timer.current = setTimeout(() => {
|
|
1278
|
+
timer.current = null;
|
|
1279
|
+
run();
|
|
1280
|
+
}, ms);
|
|
1281
|
+
};
|
|
1282
|
+
return Object.freeze({
|
|
1283
|
+
schedule,
|
|
1284
|
+
cancel,
|
|
1285
|
+
pending: () => timer.current !== null
|
|
1286
|
+
});
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1162
1289
|
// src/core/persist.ts
|
|
1163
1290
|
var kindOf = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
|
|
1164
1291
|
var conflictsWith = (draft, reference) => {
|
|
@@ -1190,7 +1317,7 @@ var persistForm = (form, options) => {
|
|
|
1190
1317
|
const storage = options.storage ?? defaultStorage();
|
|
1191
1318
|
const debounceMs = options.debounceMs ?? 300;
|
|
1192
1319
|
const apply = options.apply ?? "adopt";
|
|
1193
|
-
const applyDraft = (values) => apply === "restore" ? form.setValues(values) : form.adoptValues(values);
|
|
1320
|
+
const applyDraft = (values) => apply === "edit" || apply === "restore" ? form.setValues(values) : form.adoptValues(values);
|
|
1194
1321
|
const referenceShape = () => {
|
|
1195
1322
|
try {
|
|
1196
1323
|
return JSON.parse(JSON.stringify(form.getState().initialValues));
|
|
@@ -1212,6 +1339,7 @@ var persistForm = (form, options) => {
|
|
|
1212
1339
|
if (versioned && wrapper.__v !== options.version) return false;
|
|
1213
1340
|
if (versioned && wrapper.values === void 0) return false;
|
|
1214
1341
|
const values = versioned ? wrapper.values : parsed;
|
|
1342
|
+
if (values === null || values === void 0) return false;
|
|
1215
1343
|
const reference = referenceShape();
|
|
1216
1344
|
if (reference !== void 0 && conflictsWith(values, reference)) {
|
|
1217
1345
|
return false;
|
|
@@ -1233,28 +1361,13 @@ var persistForm = (form, options) => {
|
|
|
1233
1361
|
} catch {
|
|
1234
1362
|
}
|
|
1235
1363
|
};
|
|
1236
|
-
const
|
|
1237
|
-
|
|
1238
|
-
};
|
|
1239
|
-
const cancelPending = () => {
|
|
1240
|
-
if (timerRef.current !== null) {
|
|
1241
|
-
clearTimeout(timerRef.current);
|
|
1242
|
-
timerRef.current = null;
|
|
1243
|
-
}
|
|
1244
|
-
};
|
|
1364
|
+
const writes = createDebouncer(debounceMs);
|
|
1365
|
+
const cancelPending = () => writes.cancel();
|
|
1245
1366
|
if (apply !== "manual") {
|
|
1246
1367
|
load();
|
|
1247
1368
|
}
|
|
1248
1369
|
const unsubscribe = form.watchValues((values) => {
|
|
1249
|
-
|
|
1250
|
-
if (debounceMs === 0) {
|
|
1251
|
-
writeDraft(values);
|
|
1252
|
-
} else {
|
|
1253
|
-
timerRef.current = setTimeout(() => {
|
|
1254
|
-
timerRef.current = null;
|
|
1255
|
-
writeDraft(values);
|
|
1256
|
-
}, debounceMs);
|
|
1257
|
-
}
|
|
1370
|
+
writes.schedule(() => writeDraft(values));
|
|
1258
1371
|
});
|
|
1259
1372
|
return Object.freeze({
|
|
1260
1373
|
load,
|
|
@@ -1436,7 +1549,7 @@ var useMaskedInput = (field, options) => {
|
|
|
1436
1549
|
field.setValue(parsed.value);
|
|
1437
1550
|
return;
|
|
1438
1551
|
case "empty": {
|
|
1439
|
-
const empty = field.emptyValue;
|
|
1552
|
+
const empty = field.clearable || typeof field.initialValue !== "string" ? field.emptyValue : "";
|
|
1440
1553
|
setEdit({ raw: text, pushed: empty });
|
|
1441
1554
|
field.setValue(empty);
|
|
1442
1555
|
return;
|
|
@@ -1484,9 +1597,14 @@ var useFormSteps = (form, steps) => {
|
|
|
1484
1597
|
},
|
|
1485
1598
|
[form]
|
|
1486
1599
|
);
|
|
1600
|
+
const currentStep = react.useRef(step);
|
|
1601
|
+
react.useEffect(() => {
|
|
1602
|
+
currentStep.current = step;
|
|
1603
|
+
});
|
|
1487
1604
|
const next = react.useCallback(async () => {
|
|
1488
1605
|
const ok = await validateStep(step);
|
|
1489
1606
|
if (!ok) return false;
|
|
1607
|
+
if (currentStep.current !== step) return false;
|
|
1490
1608
|
const target = Math.min(step + 1, latest.current.length - 1);
|
|
1491
1609
|
setStep(target);
|
|
1492
1610
|
setVisited((prev) => /* @__PURE__ */ new Set([...prev, target]));
|
|
@@ -1508,6 +1626,7 @@ var useFormSteps = (form, steps) => {
|
|
|
1508
1626
|
return await validateStep(from) ? walk(from + 1) : from;
|
|
1509
1627
|
};
|
|
1510
1628
|
const landed = await walk(step);
|
|
1629
|
+
if (currentStep.current !== step) return currentStep.current;
|
|
1511
1630
|
setStep(landed);
|
|
1512
1631
|
setVisited(
|
|
1513
1632
|
(prev) => /* @__PURE__ */ new Set([
|
|
@@ -1553,7 +1672,7 @@ var useFormSteps = (form, steps) => {
|
|
|
1553
1672
|
);
|
|
1554
1673
|
};
|
|
1555
1674
|
var warnUnobserved = (result, options, site) => {
|
|
1556
|
-
if (result.kind === "error" && options?.onError === void 0 &&
|
|
1675
|
+
if (result.kind === "error" && options?.onError === void 0 && isDevBuild()) {
|
|
1557
1676
|
console.warn(
|
|
1558
1677
|
`[formstand] action handler threw and nothing observed it \u2014 the form shows no feedback. Pass { onError } to ${site} (or setError inside your handler):`,
|
|
1559
1678
|
result.error
|
|
@@ -1622,10 +1741,12 @@ function useField(form, pathArg, options) {
|
|
|
1622
1741
|
path: p,
|
|
1623
1742
|
value,
|
|
1624
1743
|
initialValue: initialValue2,
|
|
1625
|
-
|
|
1626
|
-
|
|
1744
|
+
// Own-key reads: a field named "constructor" or "toString" would
|
|
1745
|
+
// otherwise read Object.prototype's function as its error/flag.
|
|
1746
|
+
error: entryAt(state.errors, p),
|
|
1747
|
+
touched: flagAt(state.touched, p),
|
|
1627
1748
|
dirty: isFieldDirty(value, initialValue2),
|
|
1628
|
-
isValidating: state.isValidating
|
|
1749
|
+
isValidating: flagAt(state.isValidating, p)
|
|
1629
1750
|
};
|
|
1630
1751
|
})
|
|
1631
1752
|
);
|
|
@@ -1634,7 +1755,7 @@ function useField(form, pathArg, options) {
|
|
|
1634
1755
|
const initialValue = slice.initialValue;
|
|
1635
1756
|
const { emptyValue, clearable } = react.useMemo(() => {
|
|
1636
1757
|
if (formSchema !== void 0) {
|
|
1637
|
-
const sub =
|
|
1758
|
+
const sub = fieldSchemaForIntrospection(formSchema, path);
|
|
1638
1759
|
if (sub !== null) {
|
|
1639
1760
|
return {
|
|
1640
1761
|
emptyValue: emptyValueForSchema(sub),
|
|
@@ -1645,28 +1766,26 @@ function useField(form, pathArg, options) {
|
|
|
1645
1766
|
return initialValue === null ? { emptyValue: null, clearable: true } : { emptyValue: void 0, clearable: false };
|
|
1646
1767
|
}, [formSchema, path, initialValue]);
|
|
1647
1768
|
const debounceMs = options?.debounceMs;
|
|
1648
|
-
const
|
|
1769
|
+
const debouncerRef = react.useRef(null);
|
|
1649
1770
|
react.useEffect(() => {
|
|
1650
1771
|
return () => {
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
debounceTimerRef.current = null;
|
|
1654
|
-
}
|
|
1772
|
+
debouncerRef.current?.cancel();
|
|
1773
|
+
debouncerRef.current = null;
|
|
1655
1774
|
};
|
|
1656
1775
|
}, [form, path, debounceMs]);
|
|
1657
1776
|
const triggerValidate = react.useCallback(() => {
|
|
1658
1777
|
if (debounceMs !== void 0 && debounceMs > 0) {
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
const scheduled = getAtPath(
|
|
1663
|
-
|
|
1664
|
-
debounceTimerRef.current = null;
|
|
1778
|
+
const debouncer = debouncerRef.current ?? createDebouncer(debounceMs);
|
|
1779
|
+
debouncerRef.current = debouncer;
|
|
1780
|
+
const scheduledValues = form.store.getState().values;
|
|
1781
|
+
const scheduled = getAtPath(scheduledValues, path);
|
|
1782
|
+
debouncer.schedule(() => {
|
|
1665
1783
|
const state = form.store.getState();
|
|
1666
|
-
|
|
1784
|
+
const rebased = state.values !== scheduledValues && state.values === state.initialValues;
|
|
1785
|
+
if (rebased) return;
|
|
1667
1786
|
if (!Object.is(getAtPath(state.values, path), scheduled)) return;
|
|
1668
1787
|
void form.validateFieldAsync(path);
|
|
1669
|
-
}
|
|
1788
|
+
});
|
|
1670
1789
|
return;
|
|
1671
1790
|
}
|
|
1672
1791
|
form.validateField(path);
|
|
@@ -1682,7 +1801,7 @@ function useField(form, pathArg, options) {
|
|
|
1682
1801
|
state.mode,
|
|
1683
1802
|
state.reValidateMode,
|
|
1684
1803
|
state.submitCount > 0,
|
|
1685
|
-
state.touched
|
|
1804
|
+
flagAt(state.touched, path)
|
|
1686
1805
|
)) {
|
|
1687
1806
|
triggerValidate();
|
|
1688
1807
|
}
|
|
@@ -1763,7 +1882,11 @@ var reconcileIds = (prev, nextItems) => {
|
|
|
1763
1882
|
if (prev.items.length === nextItems.length && prev.items.every((item, i) => Object.is(item, nextItems[i]))) {
|
|
1764
1883
|
return { ...prev, items: nextItems };
|
|
1765
1884
|
}
|
|
1885
|
+
const keptAtIndex = nextItems.map(
|
|
1886
|
+
(item, i) => i < prev.items.length && Object.is(item, prev.items[i])
|
|
1887
|
+
);
|
|
1766
1888
|
const buckets = prev.items.reduce((acc, item, i) => {
|
|
1889
|
+
if (keptAtIndex[i] === true) return acc;
|
|
1767
1890
|
const bucket = acc.get(item);
|
|
1768
1891
|
if (bucket === void 0) {
|
|
1769
1892
|
acc.set(item, [i]);
|
|
@@ -1772,7 +1895,8 @@ var reconcileIds = (prev, nextItems) => {
|
|
|
1772
1895
|
}
|
|
1773
1896
|
return acc;
|
|
1774
1897
|
}, /* @__PURE__ */ new Map());
|
|
1775
|
-
const matchedIds = nextItems.map((item) => {
|
|
1898
|
+
const matchedIds = nextItems.map((item, i) => {
|
|
1899
|
+
if (keptAtIndex[i] === true) return prev.ids[i] ?? null;
|
|
1776
1900
|
const prevIndex = buckets.get(item)?.shift();
|
|
1777
1901
|
return prevIndex === void 0 ? null : prev.ids[prevIndex] ?? null;
|
|
1778
1902
|
});
|
|
@@ -1866,7 +1990,9 @@ function useFieldArray(form, pathArg) {
|
|
|
1866
1990
|
const value = getAtPath(state.values, path);
|
|
1867
1991
|
return {
|
|
1868
1992
|
items: Array.isArray(value) ? value : EMPTY_ITEMS,
|
|
1869
|
-
|
|
1993
|
+
// Own-key read, like useField: an array named "constructor" must
|
|
1994
|
+
// not read Object as its error list.
|
|
1995
|
+
error: entryAt(state.errors, path)
|
|
1870
1996
|
};
|
|
1871
1997
|
})
|
|
1872
1998
|
);
|
|
@@ -1880,7 +2006,7 @@ function useFieldArray(form, pathArg) {
|
|
|
1880
2006
|
state.mode,
|
|
1881
2007
|
state.reValidateMode,
|
|
1882
2008
|
state.submitCount > 0,
|
|
1883
|
-
state.touched
|
|
2009
|
+
flagAt(state.touched, path)
|
|
1884
2010
|
)) {
|
|
1885
2011
|
form.validateField?.(path);
|
|
1886
2012
|
}
|