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.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { getAtPath, slotAtPath, setAtPath, isPlainObject, hasOwnSlot, valuesEqual, useFormSelectorShallow, isFieldDirty, useFormSelector, parsePath } from './chunk-
|
|
2
|
-
export { getAtPath, parsePath, setAtPath, useFormSelector, useFormSelectorShallow } from './chunk-
|
|
3
|
-
import { shouldValidateOn } from './chunk-
|
|
4
|
-
export { shouldValidateOn } from './chunk-
|
|
1
|
+
import { isDevToolsBuild, getAtPath, isDevBuild, slotAtPath, setAtPath, isPlainObject, hasOwnSlot, valuesEqual, useFormSelectorShallow, isFieldDirty, useFormSelector, parsePath } from './chunk-BMXIZEI4.js';
|
|
2
|
+
export { getAtPath, parsePath, setAtPath, useFormSelector, useFormSelectorShallow } from './chunk-BMXIZEI4.js';
|
|
3
|
+
import { flagAt, entryAt, shouldValidateOn } from './chunk-V3PDMTPP.js';
|
|
4
|
+
export { shouldValidateOn } from './chunk-V3PDMTPP.js';
|
|
5
5
|
import { createStore } from 'zustand/vanilla';
|
|
6
6
|
import { devtools } from 'zustand/middleware';
|
|
7
7
|
import { z } from 'zod';
|
|
@@ -114,10 +114,17 @@ var expandIssue = (issue, prefix) => {
|
|
|
114
114
|
}
|
|
115
115
|
return [{ path: issuePathToFormPath(at), message: issue.message }];
|
|
116
116
|
};
|
|
117
|
-
var flattenIssues = (issues) =>
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
var flattenIssues = (issues) => Object.fromEntries(
|
|
118
|
+
issues.flatMap((issue) => expandIssue(issue, [])).reduce((acc, { path, message }) => {
|
|
119
|
+
const existing = acc.get(path);
|
|
120
|
+
if (existing === void 0) {
|
|
121
|
+
acc.set(path, [message]);
|
|
122
|
+
} else if (!existing.includes(message)) {
|
|
123
|
+
existing.push(message);
|
|
124
|
+
}
|
|
125
|
+
return acc;
|
|
126
|
+
}, /* @__PURE__ */ new Map())
|
|
127
|
+
);
|
|
121
128
|
var validateSync = (schema, values) => {
|
|
122
129
|
const result = schema.safeParse(values);
|
|
123
130
|
return result.success ? { kind: "valid", data: result.data } : { kind: "invalid", errors: flattenIssues(result.error.issues) };
|
|
@@ -182,6 +189,34 @@ var hasChecks = (s) => {
|
|
|
182
189
|
const checks = s.def.checks;
|
|
183
190
|
return checks !== void 0 && checks.length > 0;
|
|
184
191
|
};
|
|
192
|
+
var fieldSchemaForIntrospection = (schema, path) => {
|
|
193
|
+
const childOf = (s, segment) => {
|
|
194
|
+
const inner = unwrapForWalk(s);
|
|
195
|
+
if (inner !== null) return childOf(inner, segment);
|
|
196
|
+
if (s instanceof z.ZodObject && typeof segment === "string") {
|
|
197
|
+
const shape = s.shape;
|
|
198
|
+
return Object.hasOwn(shape, segment) ? shape[segment] ?? null : null;
|
|
199
|
+
}
|
|
200
|
+
if (s instanceof z.ZodArray && typeof segment === "number") {
|
|
201
|
+
return s.element;
|
|
202
|
+
}
|
|
203
|
+
if (s instanceof z.ZodTuple && typeof segment === "number") {
|
|
204
|
+
return s.def.items[segment] ?? null;
|
|
205
|
+
}
|
|
206
|
+
if (s instanceof z.ZodDiscriminatedUnion || s instanceof z.ZodUnion) {
|
|
207
|
+
const options = s.def.options;
|
|
208
|
+
return options.reduce(
|
|
209
|
+
(found, option) => found ?? childOf(option, segment),
|
|
210
|
+
null
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
};
|
|
215
|
+
return parsePath(path).reduce(
|
|
216
|
+
(current, segment) => current === null ? null : childOf(current, segment),
|
|
217
|
+
schema
|
|
218
|
+
);
|
|
219
|
+
};
|
|
185
220
|
var fieldSchemaAtPath = (schema, path) => parsePath(path).reduce((current, segment) => {
|
|
186
221
|
if (current === null || hasChecks(current)) return null;
|
|
187
222
|
if (current instanceof z.ZodObject && typeof segment === "string") {
|
|
@@ -200,6 +235,12 @@ var emptyErrors = {};
|
|
|
200
235
|
var emptyBools = {};
|
|
201
236
|
var identityMapper = (n) => n;
|
|
202
237
|
var omitKey = (map, key) => Object.fromEntries(Object.entries(map).filter(([k]) => k !== key));
|
|
238
|
+
var omitKeys = (map, keys) => {
|
|
239
|
+
if (keys.size === 0) return map;
|
|
240
|
+
const entries = Object.entries(map);
|
|
241
|
+
const kept = entries.filter(([k]) => !keys.has(k));
|
|
242
|
+
return kept.length === entries.length ? map : Object.fromEntries(kept);
|
|
243
|
+
};
|
|
203
244
|
var inScopeOfAny = (key, paths) => paths.some((p) => isPathOrChild(key, p));
|
|
204
245
|
var omitScope = (map, paths) => {
|
|
205
246
|
const entries = Object.entries(map);
|
|
@@ -238,13 +279,13 @@ var stringArraysEqual = (a, b) => a === b || a.length === b.length && a.every((v
|
|
|
238
279
|
var reuseErrorRefs = (next, prev) => {
|
|
239
280
|
if (next === prev) return prev;
|
|
240
281
|
const entries = Object.entries(next).map(([k, v]) => {
|
|
241
|
-
const old = prev
|
|
282
|
+
const old = entryAt(prev, k);
|
|
242
283
|
return [
|
|
243
284
|
k,
|
|
244
285
|
old !== void 0 && stringArraysEqual(old, v) ? old : v
|
|
245
286
|
];
|
|
246
287
|
});
|
|
247
|
-
const sameShape = entries.length === Object.keys(prev).length && entries.every(([k, v]) => prev
|
|
288
|
+
const sameShape = entries.length === Object.keys(prev).length && entries.every(([k, v]) => entryAt(prev, k) === v);
|
|
248
289
|
return sameShape ? prev : Object.fromEntries(entries);
|
|
249
290
|
};
|
|
250
291
|
var scopeSettledResult = (result, path, viaSubschema) => result.kind !== "invalid" ? emptyErrors : viaSubschema ? prefixErrorKeys(result.errors, path) : path === "" ? result.errors : pickScope(result.errors, [path]);
|
|
@@ -301,9 +342,10 @@ var createForm = (schema, options) => {
|
|
|
301
342
|
// opting in during development must NOT stream every keystroke to
|
|
302
343
|
// an end user who happens to have the extension installed, where
|
|
303
344
|
// a module-singleton form's unbounded time-travel history is a
|
|
304
|
-
// real memory + serialization cost.
|
|
305
|
-
// without
|
|
306
|
-
|
|
345
|
+
// real memory + serialization cost. Fail-closed gate: a runtime
|
|
346
|
+
// without a readable NODE_ENV (CDN/ESM, Deno, a worker) reads as
|
|
347
|
+
// production here, unlike the console-warning gate.
|
|
348
|
+
enabled: isDevToolsBuild()
|
|
307
349
|
})
|
|
308
350
|
);
|
|
309
351
|
const commitFullPass = (state, next) => errorChannels(state, {
|
|
@@ -419,13 +461,19 @@ var createForm = (schema, options) => {
|
|
|
419
461
|
const slot = slotAtPath(values, path);
|
|
420
462
|
return slot.exists ? { kind: "parse", schema: sub, value: slot.value, viaSubschema: true } : { kind: "skip" };
|
|
421
463
|
};
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
);
|
|
464
|
+
const parseScopeAsync = async (path, scope) => scope.kind === "skip" ? emptyErrors : scopeSettledResult(
|
|
465
|
+
await validateAsync(scope.schema, scope.value),
|
|
466
|
+
path,
|
|
467
|
+
scope.viaSubschema
|
|
468
|
+
);
|
|
469
|
+
const scopeStale = (scope, path, valuesAtStart, live) => {
|
|
470
|
+
if (live === valuesAtStart) return false;
|
|
471
|
+
switch (scope.kind) {
|
|
472
|
+
case "skip":
|
|
473
|
+
return slotAtPath(live, path).exists;
|
|
474
|
+
case "parse":
|
|
475
|
+
return scope.viaSubschema ? !Object.is(getAtPath(live, path), scope.value) : true;
|
|
476
|
+
}
|
|
429
477
|
};
|
|
430
478
|
const fieldResult = (scoped) => {
|
|
431
479
|
const messages = Object.values(scoped).flat();
|
|
@@ -473,10 +521,10 @@ var createForm = (schema, options) => {
|
|
|
473
521
|
const value = getAtPath(state.values, path);
|
|
474
522
|
return {
|
|
475
523
|
value,
|
|
476
|
-
error: state.errors
|
|
477
|
-
touched: state.touched
|
|
524
|
+
error: entryAt(state.errors, path),
|
|
525
|
+
touched: flagAt(state.touched, path),
|
|
478
526
|
dirty: isFieldDirty(value, getAtPath(state.initialValues, path)),
|
|
479
|
-
isValidating: state.isValidating
|
|
527
|
+
isValidating: flagAt(state.isValidating, path)
|
|
480
528
|
};
|
|
481
529
|
};
|
|
482
530
|
const sequences = /* @__PURE__ */ new Map();
|
|
@@ -515,44 +563,66 @@ var createForm = (schema, options) => {
|
|
|
515
563
|
}
|
|
516
564
|
const passes = new Map(paths.map((p) => [p, claimPass(p)]));
|
|
517
565
|
const valuesAtStart = store.getState().values;
|
|
566
|
+
const scopes = paths.map((path) => ({
|
|
567
|
+
path,
|
|
568
|
+
scope: fieldScopeFor(path, valuesAtStart)
|
|
569
|
+
}));
|
|
570
|
+
const allViaSubschema = scopes.every(
|
|
571
|
+
({ scope }) => scope.kind === "skip" || scope.kind === "parse" && scope.viaSubschema
|
|
572
|
+
);
|
|
518
573
|
store.setState((state) => ({
|
|
519
574
|
...state,
|
|
520
|
-
isValidating:
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
575
|
+
isValidating: {
|
|
576
|
+
...state.isValidating,
|
|
577
|
+
...Object.fromEntries(paths.map((p) => [p, true]))
|
|
578
|
+
}
|
|
524
579
|
}));
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
580
|
+
const parsed = allViaSubschema ? {
|
|
581
|
+
kind: "slots",
|
|
582
|
+
byPath: new Map(
|
|
583
|
+
await Promise.all(
|
|
584
|
+
scopes.map(
|
|
585
|
+
async ({ path, scope }) => [path, await parseScopeAsync(path, scope)]
|
|
586
|
+
)
|
|
587
|
+
)
|
|
588
|
+
)
|
|
589
|
+
} : {
|
|
590
|
+
kind: "full",
|
|
591
|
+
errors: await validateAsync(schema, valuesAtStart).then(
|
|
592
|
+
(result) => result.kind === "invalid" ? result.errors : emptyErrors
|
|
593
|
+
)
|
|
594
|
+
};
|
|
595
|
+
const errorsFor = (path) => {
|
|
596
|
+
switch (parsed.kind) {
|
|
597
|
+
case "slots":
|
|
598
|
+
return parsed.byPath.get(path) ?? emptyErrors;
|
|
599
|
+
case "full":
|
|
600
|
+
return pickScope(parsed.errors, [path]);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
const requestedErrors = Object.fromEntries(
|
|
604
|
+
paths.flatMap((path) => Object.entries(errorsFor(path)))
|
|
605
|
+
);
|
|
528
606
|
const requested = settledFields(requestedErrors);
|
|
529
607
|
const live = store.getState();
|
|
530
|
-
const
|
|
531
|
-
(
|
|
608
|
+
const owned = scopes.filter(
|
|
609
|
+
({ path }) => sequences.get(path) === passes.get(path)
|
|
532
610
|
);
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
(acc, p) => omitKey(acc, p),
|
|
539
|
-
state.isValidating
|
|
540
|
-
)
|
|
541
|
-
}));
|
|
542
|
-
}
|
|
543
|
-
return requested;
|
|
544
|
-
}
|
|
611
|
+
const fresh = owned.filter(
|
|
612
|
+
({ path, scope }) => !scopeStale(scope, path, valuesAtStart, live.values)
|
|
613
|
+
).map(({ path }) => path);
|
|
614
|
+
const ownedKeys = new Set(owned.map(({ path }) => path));
|
|
615
|
+
if (owned.length === 0) return requested;
|
|
545
616
|
store.setState((state) => ({
|
|
546
617
|
...state,
|
|
547
|
-
...commitScopedErrors(
|
|
618
|
+
...fresh.length === 0 ? {} : commitScopedErrors(
|
|
548
619
|
state,
|
|
549
|
-
|
|
550
|
-
|
|
620
|
+
Object.fromEntries(
|
|
621
|
+
fresh.flatMap((path) => Object.entries(errorsFor(path)))
|
|
622
|
+
),
|
|
623
|
+
fresh
|
|
551
624
|
),
|
|
552
|
-
isValidating:
|
|
553
|
-
(acc, p) => omitKey(acc, p),
|
|
554
|
-
state.isValidating
|
|
555
|
-
)
|
|
625
|
+
isValidating: omitKeys(state.isValidating, ownedKeys)
|
|
556
626
|
}));
|
|
557
627
|
return requested;
|
|
558
628
|
};
|
|
@@ -566,14 +636,15 @@ var createForm = (schema, options) => {
|
|
|
566
636
|
}
|
|
567
637
|
const pass = claimPass(path);
|
|
568
638
|
const valuesAtStart = store.getState().values;
|
|
639
|
+
const scope = fieldScopeFor(path, valuesAtStart);
|
|
569
640
|
store.setState((state) => ({
|
|
570
641
|
...state,
|
|
571
642
|
isValidating: { ...state.isValidating, [path]: true }
|
|
572
643
|
}));
|
|
573
|
-
const scoped = await
|
|
644
|
+
const scoped = await parseScopeAsync(path, scope);
|
|
574
645
|
const live = store.getState();
|
|
575
646
|
const stillOwns = sequences.get(path) === pass;
|
|
576
|
-
if (!stillOwns || live.values
|
|
647
|
+
if (!stillOwns || scopeStale(scope, path, valuesAtStart, live.values)) {
|
|
577
648
|
if (stillOwns) {
|
|
578
649
|
store.setState((state) => ({
|
|
579
650
|
...state,
|
|
@@ -808,17 +879,24 @@ var createForm = (schema, options) => {
|
|
|
808
879
|
...state,
|
|
809
880
|
...errorChannels(state, { serverErrors: errors })
|
|
810
881
|
})),
|
|
811
|
-
addErrors: (errors) => store.setState((state) =>
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
882
|
+
addErrors: (errors) => store.setState((state) => {
|
|
883
|
+
const entries = Object.entries(errors);
|
|
884
|
+
const removed = new Set(
|
|
885
|
+
entries.filter(([, list]) => list === void 0 || list.length === 0).map(([key]) => key)
|
|
886
|
+
);
|
|
887
|
+
const added = Object.fromEntries(
|
|
888
|
+
entries.filter(([, list]) => list !== void 0 && list.length > 0)
|
|
889
|
+
);
|
|
890
|
+
return {
|
|
891
|
+
...state,
|
|
892
|
+
...errorChannels(state, {
|
|
893
|
+
serverErrors: {
|
|
894
|
+
...omitKeys(state.serverErrors, removed),
|
|
895
|
+
...added
|
|
896
|
+
}
|
|
897
|
+
})
|
|
898
|
+
};
|
|
899
|
+
}),
|
|
822
900
|
clearErrors: (path) => store.setState((state) => {
|
|
823
901
|
const scrub = (map) => path === void 0 ? emptyErrors : omitScope(map, [path]);
|
|
824
902
|
return {
|
|
@@ -830,27 +908,26 @@ var createForm = (schema, options) => {
|
|
|
830
908
|
};
|
|
831
909
|
}),
|
|
832
910
|
updateState: (updater) => {
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
return {
|
|
844
|
-
...next,
|
|
845
|
-
...errorChannels(state, {
|
|
846
|
-
schemaErrors: next.schemaErrors,
|
|
847
|
-
serverErrors: next.serverErrors
|
|
848
|
-
})
|
|
849
|
-
};
|
|
850
|
-
});
|
|
851
|
-
if (store.getState().values !== valuesBefore) {
|
|
911
|
+
const state = store.getState();
|
|
912
|
+
const patch = updater(state);
|
|
913
|
+
if (Object.keys(patch).length === 0) return;
|
|
914
|
+
if (patch.errors !== void 0 && patch.errors !== state.errors) {
|
|
915
|
+
console.warn(
|
|
916
|
+
"[formstand] `errors` is derived from schemaErrors/serverErrors \u2014 patch those channels instead; the direct `errors` patch is ignored."
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
const next = { ...state, ...patch };
|
|
920
|
+
if (next.values !== state.values) {
|
|
852
921
|
clearArrayOpsUnder(store, "");
|
|
853
922
|
}
|
|
923
|
+
const serverErrors = patch.serverErrors !== void 0 ? patch.serverErrors : next.values !== state.values ? releaseChangedSlices(state.serverErrors, state.values, next.values) : state.serverErrors;
|
|
924
|
+
store.setState(() => ({
|
|
925
|
+
...next,
|
|
926
|
+
...errorChannels(state, {
|
|
927
|
+
schemaErrors: next.schemaErrors,
|
|
928
|
+
serverErrors
|
|
929
|
+
})
|
|
930
|
+
}));
|
|
854
931
|
},
|
|
855
932
|
adoptValues: (values) => {
|
|
856
933
|
sequences.clear();
|
|
@@ -959,7 +1036,7 @@ var createForm = (schema, options) => {
|
|
|
959
1036
|
handleSubmit: (onValid, onInvalid, opts) => async (event) => {
|
|
960
1037
|
event?.preventDefault();
|
|
961
1038
|
const result = await submit(onValid, onInvalid, opts);
|
|
962
|
-
if (result.kind === "error" && opts?.onError === void 0 &&
|
|
1039
|
+
if (result.kind === "error" && opts?.onError === void 0 && isDevBuild()) {
|
|
963
1040
|
console.warn(
|
|
964
1041
|
"[formstand] submit handler threw and nothing observed it \u2014 the form shows no feedback. Pass { onError } to handleSubmit (or setError inside your handler):",
|
|
965
1042
|
result.error
|
|
@@ -1010,6 +1087,35 @@ var createForm = (schema, options) => {
|
|
|
1010
1087
|
});
|
|
1011
1088
|
};
|
|
1012
1089
|
|
|
1090
|
+
// src/core/debounce.ts
|
|
1091
|
+
var createDebouncer = (ms) => {
|
|
1092
|
+
const timer = {
|
|
1093
|
+
current: null
|
|
1094
|
+
};
|
|
1095
|
+
const cancel = () => {
|
|
1096
|
+
if (timer.current !== null) {
|
|
1097
|
+
clearTimeout(timer.current);
|
|
1098
|
+
timer.current = null;
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
const schedule = (run) => {
|
|
1102
|
+
cancel();
|
|
1103
|
+
if (ms === 0) {
|
|
1104
|
+
run();
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
timer.current = setTimeout(() => {
|
|
1108
|
+
timer.current = null;
|
|
1109
|
+
run();
|
|
1110
|
+
}, ms);
|
|
1111
|
+
};
|
|
1112
|
+
return Object.freeze({
|
|
1113
|
+
schedule,
|
|
1114
|
+
cancel,
|
|
1115
|
+
pending: () => timer.current !== null
|
|
1116
|
+
});
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1013
1119
|
// src/core/persist.ts
|
|
1014
1120
|
var kindOf = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
|
|
1015
1121
|
var conflictsWith = (draft, reference) => {
|
|
@@ -1041,7 +1147,7 @@ var persistForm = (form, options) => {
|
|
|
1041
1147
|
const storage = options.storage ?? defaultStorage();
|
|
1042
1148
|
const debounceMs = options.debounceMs ?? 300;
|
|
1043
1149
|
const apply = options.apply ?? "adopt";
|
|
1044
|
-
const applyDraft = (values) => apply === "restore" ? form.setValues(values) : form.adoptValues(values);
|
|
1150
|
+
const applyDraft = (values) => apply === "edit" || apply === "restore" ? form.setValues(values) : form.adoptValues(values);
|
|
1045
1151
|
const referenceShape = () => {
|
|
1046
1152
|
try {
|
|
1047
1153
|
return JSON.parse(JSON.stringify(form.getState().initialValues));
|
|
@@ -1063,6 +1169,7 @@ var persistForm = (form, options) => {
|
|
|
1063
1169
|
if (versioned && wrapper.__v !== options.version) return false;
|
|
1064
1170
|
if (versioned && wrapper.values === void 0) return false;
|
|
1065
1171
|
const values = versioned ? wrapper.values : parsed;
|
|
1172
|
+
if (values === null || values === void 0) return false;
|
|
1066
1173
|
const reference = referenceShape();
|
|
1067
1174
|
if (reference !== void 0 && conflictsWith(values, reference)) {
|
|
1068
1175
|
return false;
|
|
@@ -1084,28 +1191,13 @@ var persistForm = (form, options) => {
|
|
|
1084
1191
|
} catch {
|
|
1085
1192
|
}
|
|
1086
1193
|
};
|
|
1087
|
-
const
|
|
1088
|
-
|
|
1089
|
-
};
|
|
1090
|
-
const cancelPending = () => {
|
|
1091
|
-
if (timerRef.current !== null) {
|
|
1092
|
-
clearTimeout(timerRef.current);
|
|
1093
|
-
timerRef.current = null;
|
|
1094
|
-
}
|
|
1095
|
-
};
|
|
1194
|
+
const writes = createDebouncer(debounceMs);
|
|
1195
|
+
const cancelPending = () => writes.cancel();
|
|
1096
1196
|
if (apply !== "manual") {
|
|
1097
1197
|
load();
|
|
1098
1198
|
}
|
|
1099
1199
|
const unsubscribe = form.watchValues((values) => {
|
|
1100
|
-
|
|
1101
|
-
if (debounceMs === 0) {
|
|
1102
|
-
writeDraft(values);
|
|
1103
|
-
} else {
|
|
1104
|
-
timerRef.current = setTimeout(() => {
|
|
1105
|
-
timerRef.current = null;
|
|
1106
|
-
writeDraft(values);
|
|
1107
|
-
}, debounceMs);
|
|
1108
|
-
}
|
|
1200
|
+
writes.schedule(() => writeDraft(values));
|
|
1109
1201
|
});
|
|
1110
1202
|
return Object.freeze({
|
|
1111
1203
|
load,
|
|
@@ -1287,7 +1379,7 @@ var useMaskedInput = (field, options) => {
|
|
|
1287
1379
|
field.setValue(parsed.value);
|
|
1288
1380
|
return;
|
|
1289
1381
|
case "empty": {
|
|
1290
|
-
const empty = field.emptyValue;
|
|
1382
|
+
const empty = field.clearable || typeof field.initialValue !== "string" ? field.emptyValue : "";
|
|
1291
1383
|
setEdit({ raw: text, pushed: empty });
|
|
1292
1384
|
field.setValue(empty);
|
|
1293
1385
|
return;
|
|
@@ -1327,9 +1419,14 @@ var useFormSteps = (form, steps) => {
|
|
|
1327
1419
|
},
|
|
1328
1420
|
[form]
|
|
1329
1421
|
);
|
|
1422
|
+
const currentStep = useRef(step);
|
|
1423
|
+
useEffect(() => {
|
|
1424
|
+
currentStep.current = step;
|
|
1425
|
+
});
|
|
1330
1426
|
const next = useCallback(async () => {
|
|
1331
1427
|
const ok = await validateStep(step);
|
|
1332
1428
|
if (!ok) return false;
|
|
1429
|
+
if (currentStep.current !== step) return false;
|
|
1333
1430
|
const target = Math.min(step + 1, latest.current.length - 1);
|
|
1334
1431
|
setStep(target);
|
|
1335
1432
|
setVisited((prev) => /* @__PURE__ */ new Set([...prev, target]));
|
|
@@ -1351,6 +1448,7 @@ var useFormSteps = (form, steps) => {
|
|
|
1351
1448
|
return await validateStep(from) ? walk(from + 1) : from;
|
|
1352
1449
|
};
|
|
1353
1450
|
const landed = await walk(step);
|
|
1451
|
+
if (currentStep.current !== step) return currentStep.current;
|
|
1354
1452
|
setStep(landed);
|
|
1355
1453
|
setVisited(
|
|
1356
1454
|
(prev) => /* @__PURE__ */ new Set([
|
|
@@ -1396,7 +1494,7 @@ var useFormSteps = (form, steps) => {
|
|
|
1396
1494
|
);
|
|
1397
1495
|
};
|
|
1398
1496
|
var warnUnobserved = (result, options, site) => {
|
|
1399
|
-
if (result.kind === "error" && options?.onError === void 0 &&
|
|
1497
|
+
if (result.kind === "error" && options?.onError === void 0 && isDevBuild()) {
|
|
1400
1498
|
console.warn(
|
|
1401
1499
|
`[formstand] action handler threw and nothing observed it \u2014 the form shows no feedback. Pass { onError } to ${site} (or setError inside your handler):`,
|
|
1402
1500
|
result.error
|
|
@@ -1465,10 +1563,12 @@ function useField(form, pathArg, options) {
|
|
|
1465
1563
|
path: p,
|
|
1466
1564
|
value,
|
|
1467
1565
|
initialValue: initialValue2,
|
|
1468
|
-
|
|
1469
|
-
|
|
1566
|
+
// Own-key reads: a field named "constructor" or "toString" would
|
|
1567
|
+
// otherwise read Object.prototype's function as its error/flag.
|
|
1568
|
+
error: entryAt(state.errors, p),
|
|
1569
|
+
touched: flagAt(state.touched, p),
|
|
1470
1570
|
dirty: isFieldDirty(value, initialValue2),
|
|
1471
|
-
isValidating: state.isValidating
|
|
1571
|
+
isValidating: flagAt(state.isValidating, p)
|
|
1472
1572
|
};
|
|
1473
1573
|
})
|
|
1474
1574
|
);
|
|
@@ -1477,7 +1577,7 @@ function useField(form, pathArg, options) {
|
|
|
1477
1577
|
const initialValue = slice.initialValue;
|
|
1478
1578
|
const { emptyValue, clearable } = useMemo(() => {
|
|
1479
1579
|
if (formSchema !== void 0) {
|
|
1480
|
-
const sub =
|
|
1580
|
+
const sub = fieldSchemaForIntrospection(formSchema, path);
|
|
1481
1581
|
if (sub !== null) {
|
|
1482
1582
|
return {
|
|
1483
1583
|
emptyValue: emptyValueForSchema(sub),
|
|
@@ -1488,28 +1588,26 @@ function useField(form, pathArg, options) {
|
|
|
1488
1588
|
return initialValue === null ? { emptyValue: null, clearable: true } : { emptyValue: void 0, clearable: false };
|
|
1489
1589
|
}, [formSchema, path, initialValue]);
|
|
1490
1590
|
const debounceMs = options?.debounceMs;
|
|
1491
|
-
const
|
|
1591
|
+
const debouncerRef = useRef(null);
|
|
1492
1592
|
useEffect(() => {
|
|
1493
1593
|
return () => {
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
debounceTimerRef.current = null;
|
|
1497
|
-
}
|
|
1594
|
+
debouncerRef.current?.cancel();
|
|
1595
|
+
debouncerRef.current = null;
|
|
1498
1596
|
};
|
|
1499
1597
|
}, [form, path, debounceMs]);
|
|
1500
1598
|
const triggerValidate = useCallback(() => {
|
|
1501
1599
|
if (debounceMs !== void 0 && debounceMs > 0) {
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
const scheduled = getAtPath(
|
|
1506
|
-
|
|
1507
|
-
debounceTimerRef.current = null;
|
|
1600
|
+
const debouncer = debouncerRef.current ?? createDebouncer(debounceMs);
|
|
1601
|
+
debouncerRef.current = debouncer;
|
|
1602
|
+
const scheduledValues = form.store.getState().values;
|
|
1603
|
+
const scheduled = getAtPath(scheduledValues, path);
|
|
1604
|
+
debouncer.schedule(() => {
|
|
1508
1605
|
const state = form.store.getState();
|
|
1509
|
-
|
|
1606
|
+
const rebased = state.values !== scheduledValues && state.values === state.initialValues;
|
|
1607
|
+
if (rebased) return;
|
|
1510
1608
|
if (!Object.is(getAtPath(state.values, path), scheduled)) return;
|
|
1511
1609
|
void form.validateFieldAsync(path);
|
|
1512
|
-
}
|
|
1610
|
+
});
|
|
1513
1611
|
return;
|
|
1514
1612
|
}
|
|
1515
1613
|
form.validateField(path);
|
|
@@ -1525,7 +1623,7 @@ function useField(form, pathArg, options) {
|
|
|
1525
1623
|
state.mode,
|
|
1526
1624
|
state.reValidateMode,
|
|
1527
1625
|
state.submitCount > 0,
|
|
1528
|
-
state.touched
|
|
1626
|
+
flagAt(state.touched, path)
|
|
1529
1627
|
)) {
|
|
1530
1628
|
triggerValidate();
|
|
1531
1629
|
}
|
|
@@ -1606,7 +1704,11 @@ var reconcileIds = (prev, nextItems) => {
|
|
|
1606
1704
|
if (prev.items.length === nextItems.length && prev.items.every((item, i) => Object.is(item, nextItems[i]))) {
|
|
1607
1705
|
return { ...prev, items: nextItems };
|
|
1608
1706
|
}
|
|
1707
|
+
const keptAtIndex = nextItems.map(
|
|
1708
|
+
(item, i) => i < prev.items.length && Object.is(item, prev.items[i])
|
|
1709
|
+
);
|
|
1609
1710
|
const buckets = prev.items.reduce((acc, item, i) => {
|
|
1711
|
+
if (keptAtIndex[i] === true) return acc;
|
|
1610
1712
|
const bucket = acc.get(item);
|
|
1611
1713
|
if (bucket === void 0) {
|
|
1612
1714
|
acc.set(item, [i]);
|
|
@@ -1615,7 +1717,8 @@ var reconcileIds = (prev, nextItems) => {
|
|
|
1615
1717
|
}
|
|
1616
1718
|
return acc;
|
|
1617
1719
|
}, /* @__PURE__ */ new Map());
|
|
1618
|
-
const matchedIds = nextItems.map((item) => {
|
|
1720
|
+
const matchedIds = nextItems.map((item, i) => {
|
|
1721
|
+
if (keptAtIndex[i] === true) return prev.ids[i] ?? null;
|
|
1619
1722
|
const prevIndex = buckets.get(item)?.shift();
|
|
1620
1723
|
return prevIndex === void 0 ? null : prev.ids[prevIndex] ?? null;
|
|
1621
1724
|
});
|
|
@@ -1709,7 +1812,9 @@ function useFieldArray(form, pathArg) {
|
|
|
1709
1812
|
const value = getAtPath(state.values, path);
|
|
1710
1813
|
return {
|
|
1711
1814
|
items: Array.isArray(value) ? value : EMPTY_ITEMS,
|
|
1712
|
-
|
|
1815
|
+
// Own-key read, like useField: an array named "constructor" must
|
|
1816
|
+
// not read Object as its error list.
|
|
1817
|
+
error: entryAt(state.errors, path)
|
|
1713
1818
|
};
|
|
1714
1819
|
})
|
|
1715
1820
|
);
|
|
@@ -1723,7 +1828,7 @@ function useFieldArray(form, pathArg) {
|
|
|
1723
1828
|
state.mode,
|
|
1724
1829
|
state.reValidateMode,
|
|
1725
1830
|
state.submitCount > 0,
|
|
1726
|
-
state.touched
|
|
1831
|
+
flagAt(state.touched, path)
|
|
1727
1832
|
)) {
|
|
1728
1833
|
form.validateField?.(path);
|
|
1729
1834
|
}
|