formstand 0.25.0 → 0.26.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 +112 -0
- package/dist/chunk-XJ6W5CKB.js +22 -0
- package/dist/chunk-XJ6W5CKB.js.map +1 -0
- package/dist/{createForm-CMVALnc3.d.cts → createForm-RE6Bjnfv.d.cts} +17 -6
- package/dist/{createForm-CMVALnc3.d.ts → createForm-RE6Bjnfv.d.ts} +17 -6
- package/dist/devtools/index.cjs +30 -54
- package/dist/devtools/index.cjs.map +1 -1
- package/dist/devtools/index.js +31 -21
- package/dist/devtools/index.js.map +1 -1
- package/dist/index.cjs +315 -94
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -3
- package/dist/index.d.ts +13 -3
- package/dist/index.js +470 -97
- package/dist/index.js.map +1 -1
- package/dist/testing/index.cjs +6 -10
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +4 -4
- package/dist/testing/index.d.ts +4 -4
- package/dist/testing/index.js +6 -10
- package/dist/testing/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-EQIS5IVH.js +0 -172
- package/dist/chunk-EQIS5IVH.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { isDevToolsBuild,
|
|
2
|
-
export {
|
|
1
|
+
import { isDevToolsBuild, isDevBuild, useFormSelectorShallow, useFormSelector } from './chunk-XJ6W5CKB.js';
|
|
2
|
+
export { useFormSelector, useFormSelectorShallow } from './chunk-XJ6W5CKB.js';
|
|
3
3
|
import { flagAt, entryAt, shouldValidateOn } from './chunk-V3PDMTPP.js';
|
|
4
4
|
export { shouldValidateOn } from './chunk-V3PDMTPP.js';
|
|
5
5
|
import { createStore } from 'zustand/vanilla';
|
|
@@ -103,6 +103,158 @@ var clearArrayOpsUnder = (store, path, keepSelf = false) => {
|
|
|
103
103
|
);
|
|
104
104
|
related.forEach((p) => byPath.delete(p));
|
|
105
105
|
};
|
|
106
|
+
|
|
107
|
+
// src/core/equality.ts
|
|
108
|
+
var isPlainObject = (v) => {
|
|
109
|
+
if (typeof v !== "object" || v === null) return false;
|
|
110
|
+
const proto = Object.getPrototypeOf(v);
|
|
111
|
+
return proto === Object.prototype || proto === null;
|
|
112
|
+
};
|
|
113
|
+
var valuesEqual = (a, b) => {
|
|
114
|
+
if (Object.is(a, b)) return true;
|
|
115
|
+
if (typeof a === "number" && typeof b === "number") return a === b;
|
|
116
|
+
if (a instanceof Date && b instanceof Date) {
|
|
117
|
+
const [ta, tb] = [a.getTime(), b.getTime()];
|
|
118
|
+
return ta === tb || Number.isNaN(ta) && Number.isNaN(tb);
|
|
119
|
+
}
|
|
120
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
121
|
+
return a.length === b.length && a.every((v, i) => valuesEqual(v, b[i]));
|
|
122
|
+
}
|
|
123
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
124
|
+
const aKeys = Object.keys(a);
|
|
125
|
+
const bKeys = Object.keys(b);
|
|
126
|
+
return aKeys.length === bKeys.length && aKeys.every((k) => Object.hasOwn(b, k) && valuesEqual(a[k], b[k]));
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
};
|
|
130
|
+
var dirtyCache = /* @__PURE__ */ new WeakMap();
|
|
131
|
+
var MAX_VERDICTS = 4;
|
|
132
|
+
var isFieldDirty = (value, initialValue) => {
|
|
133
|
+
if (Object.is(value, initialValue)) return false;
|
|
134
|
+
if (typeof value !== "object" || value === null) {
|
|
135
|
+
return !valuesEqual(value, initialValue);
|
|
136
|
+
}
|
|
137
|
+
const cached = dirtyCache.get(value) ?? [];
|
|
138
|
+
const hit = cached.find((v) => Object.is(v.initial, initialValue));
|
|
139
|
+
if (hit !== void 0) return hit.dirty;
|
|
140
|
+
const dirty = !valuesEqual(value, initialValue);
|
|
141
|
+
dirtyCache.set(
|
|
142
|
+
value,
|
|
143
|
+
[{ initial: initialValue, dirty }, ...cached].slice(0, MAX_VERDICTS)
|
|
144
|
+
);
|
|
145
|
+
return dirty;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// src/core/path.ts
|
|
149
|
+
var MAX_ARRAY_INDEX = 1e5;
|
|
150
|
+
var isIndexSegment = (s) => {
|
|
151
|
+
const n = Number(s);
|
|
152
|
+
return Number.isInteger(n) && n >= 0 && String(n) === s;
|
|
153
|
+
};
|
|
154
|
+
var PARSE_CACHE_MAX = 4096;
|
|
155
|
+
var parseCache = /* @__PURE__ */ new Map();
|
|
156
|
+
var parsePath = (path) => {
|
|
157
|
+
const cached = parseCache.get(path);
|
|
158
|
+
if (cached !== void 0) {
|
|
159
|
+
parseCache.delete(path);
|
|
160
|
+
parseCache.set(path, cached);
|
|
161
|
+
return cached;
|
|
162
|
+
}
|
|
163
|
+
const segments = Object.freeze(
|
|
164
|
+
path === "" ? [] : path.split(".").map((s) => isIndexSegment(s) ? Number(s) : s)
|
|
165
|
+
);
|
|
166
|
+
if (parseCache.size >= PARSE_CACHE_MAX) {
|
|
167
|
+
const oldest = parseCache.keys().next().value;
|
|
168
|
+
if (oldest !== void 0) parseCache.delete(oldest);
|
|
169
|
+
}
|
|
170
|
+
parseCache.set(path, segments);
|
|
171
|
+
return segments;
|
|
172
|
+
};
|
|
173
|
+
var isPlainRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
174
|
+
var readSegment = (obj, segment) => {
|
|
175
|
+
if (obj == null) return void 0;
|
|
176
|
+
if (Array.isArray(obj)) {
|
|
177
|
+
return typeof segment === "number" ? obj[segment] : void 0;
|
|
178
|
+
}
|
|
179
|
+
return isPlainRecord(obj) && Object.hasOwn(obj, String(segment)) ? obj[String(segment)] : void 0;
|
|
180
|
+
};
|
|
181
|
+
var getAtPath = (obj, path) => parsePath(path).reduce(readSegment, obj);
|
|
182
|
+
var slotAtPath = (obj, path) => {
|
|
183
|
+
const walked = parsePath(path).reduce(
|
|
184
|
+
(acc, segment) => {
|
|
185
|
+
const container = acc.current;
|
|
186
|
+
const addressable = typeof segment === "number" ? Array.isArray(container) ? segment < container.length : isPlainRecord(container) : isPlainRecord(container);
|
|
187
|
+
return !acc.ok || !addressable ? { ok: false, current: void 0 } : { ok: true, current: readSegment(container, segment) };
|
|
188
|
+
},
|
|
189
|
+
{ ok: true, current: obj }
|
|
190
|
+
);
|
|
191
|
+
return walked.ok ? { exists: true, value: walked.current } : { exists: false };
|
|
192
|
+
};
|
|
193
|
+
var hasOwnSlot = (obj, path) => parsePath(path).reduce(
|
|
194
|
+
(acc, segment) => {
|
|
195
|
+
const container = acc.current;
|
|
196
|
+
const owns = Array.isArray(container) ? typeof segment === "number" && segment < container.length : isPlainRecord(container) && Object.hasOwn(container, String(segment));
|
|
197
|
+
return !acc.ok || !owns ? { ok: false, current: void 0 } : { ok: true, current: readSegment(container, segment) };
|
|
198
|
+
},
|
|
199
|
+
{ ok: true, current: obj }
|
|
200
|
+
).ok;
|
|
201
|
+
var writeIntoArray = (arr, index, rest, value, kindAt, prefix) => {
|
|
202
|
+
const length = Math.max(arr.length, index + 1);
|
|
203
|
+
return Array.from(
|
|
204
|
+
{ length },
|
|
205
|
+
(_, i) => i === index ? writeSegments(arr[i], rest, value, kindAt, [...prefix, index]) : arr[i]
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
var refusesHugeIndex = (index) => {
|
|
209
|
+
if (index > MAX_ARRAY_INDEX) {
|
|
210
|
+
console.warn(
|
|
211
|
+
`[formstand] refusing to write array index ${index} (max ${MAX_ARRAY_INDEX}); value left unchanged.`
|
|
212
|
+
);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
};
|
|
217
|
+
var writeSegments = (obj, segments, value, kindAt, prefix) => {
|
|
218
|
+
const [head, ...rest] = segments;
|
|
219
|
+
if (head === void 0) return value;
|
|
220
|
+
if (Array.isArray(obj)) {
|
|
221
|
+
if (typeof head !== "number") {
|
|
222
|
+
console.warn(
|
|
223
|
+
`[formstand] cannot write string key "${head}" into an array; value left unchanged.`
|
|
224
|
+
);
|
|
225
|
+
return obj;
|
|
226
|
+
}
|
|
227
|
+
if (refusesHugeIndex(head)) return obj;
|
|
228
|
+
return writeIntoArray(obj, head, rest, value, kindAt, prefix);
|
|
229
|
+
}
|
|
230
|
+
if (isPlainRecord(obj)) {
|
|
231
|
+
if (!isPlainObject(obj)) {
|
|
232
|
+
console.warn(
|
|
233
|
+
`[formstand] cannot write key "${String(head)}" through a non-plain object (Date/Map/Set/class instance); value left unchanged.`
|
|
234
|
+
);
|
|
235
|
+
return obj;
|
|
236
|
+
}
|
|
237
|
+
const key2 = String(head);
|
|
238
|
+
return {
|
|
239
|
+
...obj,
|
|
240
|
+
[key2]: writeSegments(obj[key2], rest, value, kindAt, [...prefix, key2])
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const kind = kindAt?.(prefix.map(String).join(".")) ?? (typeof head === "number" ? "array" : "record");
|
|
244
|
+
if (kind === "array") {
|
|
245
|
+
if (typeof head !== "number") {
|
|
246
|
+
console.warn(
|
|
247
|
+
`[formstand] cannot write string key "${head}" into an array; value left unchanged.`
|
|
248
|
+
);
|
|
249
|
+
return obj;
|
|
250
|
+
}
|
|
251
|
+
if (refusesHugeIndex(head)) return obj;
|
|
252
|
+
return writeIntoArray([], head, rest, value, kindAt, prefix);
|
|
253
|
+
}
|
|
254
|
+
const key = String(head);
|
|
255
|
+
return { [key]: writeSegments(void 0, rest, value, kindAt, [...prefix, key]) };
|
|
256
|
+
};
|
|
257
|
+
var setAtPath = (obj, path, value, containerKindAt2) => writeSegments(obj, parsePath(path), value, containerKindAt2, []);
|
|
106
258
|
var issuePathToFormPath = (path) => path.filter((segment) => typeof segment !== "symbol").join(".");
|
|
107
259
|
var expandIssue = (issue, prefix) => {
|
|
108
260
|
const at = [...prefix, ...issue.path];
|
|
@@ -134,6 +286,12 @@ var validateAsync = async (schema, values) => {
|
|
|
134
286
|
return result.success ? { kind: "valid", data: result.data } : { kind: "invalid", errors: flattenIssues(result.error.issues) };
|
|
135
287
|
};
|
|
136
288
|
var isAsyncRequiredError = (e) => e instanceof z.core.$ZodAsyncError || e instanceof Error && /Encountered Promise during synchronous parse/i.test(e.message);
|
|
289
|
+
var reportBackgroundFailure = (error) => {
|
|
290
|
+
console.error(
|
|
291
|
+
"[formstand] a background validation pass threw (an async refinement rejected). Its in-flight flags were released. The error:",
|
|
292
|
+
error
|
|
293
|
+
);
|
|
294
|
+
};
|
|
137
295
|
var isPathOrChild = (key, path) => key === path || key.startsWith(`${path}.`);
|
|
138
296
|
var prefixErrorKeys = (errors, path) => path === "" ? errors : Object.fromEntries(
|
|
139
297
|
Object.entries(errors).map(([k, v]) => [
|
|
@@ -185,6 +343,42 @@ var schemaHasPath = (schema, path) => {
|
|
|
185
343
|
};
|
|
186
344
|
return walk(schema, parsePath(path), SCHEMA_WALK_BUDGET);
|
|
187
345
|
};
|
|
346
|
+
var schemaAllowsKindChangeAt = (schema, path) => {
|
|
347
|
+
const flexible = (s) => s instanceof z.ZodUnion || s instanceof z.ZodDiscriminatedUnion || s instanceof z.ZodPipe || s instanceof z.ZodAny || s instanceof z.ZodUnknown || s instanceof z.ZodLazy || s instanceof z.ZodCatch || s.def.coerce === true;
|
|
348
|
+
const strip = (s) => {
|
|
349
|
+
if (s instanceof z.ZodOptional || s instanceof z.ZodNullable) {
|
|
350
|
+
return strip(s.unwrap());
|
|
351
|
+
}
|
|
352
|
+
if (s instanceof z.ZodDefault || s instanceof z.ZodPrefault || s instanceof z.ZodReadonly || s instanceof z.ZodNonOptional) {
|
|
353
|
+
return strip(s.def.innerType);
|
|
354
|
+
}
|
|
355
|
+
return s;
|
|
356
|
+
};
|
|
357
|
+
const walk = (s, segments, budget) => {
|
|
358
|
+
if (budget <= 0) return true;
|
|
359
|
+
const node = strip(s);
|
|
360
|
+
if (flexible(node)) return true;
|
|
361
|
+
const [head, ...rest] = segments;
|
|
362
|
+
if (head === void 0) return false;
|
|
363
|
+
if (node instanceof z.ZodObject) {
|
|
364
|
+
const shape = node.shape;
|
|
365
|
+
const child = Object.hasOwn(shape, String(head)) ? shape[String(head)] : void 0;
|
|
366
|
+
return child === void 0 ? true : walk(child, rest, budget - 1);
|
|
367
|
+
}
|
|
368
|
+
if (node instanceof z.ZodArray) {
|
|
369
|
+
return typeof head === "number" ? walk(node.element, rest, budget - 1) : true;
|
|
370
|
+
}
|
|
371
|
+
if (node instanceof z.ZodTuple && typeof head === "number") {
|
|
372
|
+
const item = node.def.items[head];
|
|
373
|
+
return item === void 0 ? true : walk(item, rest, budget - 1);
|
|
374
|
+
}
|
|
375
|
+
if (node instanceof z.ZodRecord) {
|
|
376
|
+
return walk(node.valueType, rest, budget - 1);
|
|
377
|
+
}
|
|
378
|
+
return true;
|
|
379
|
+
};
|
|
380
|
+
return walk(schema, parsePath(path), SCHEMA_WALK_BUDGET);
|
|
381
|
+
};
|
|
188
382
|
var hasChecks = (s) => {
|
|
189
383
|
const checks = s.def.checks;
|
|
190
384
|
return checks !== void 0 && checks.length > 0;
|
|
@@ -269,7 +463,7 @@ var fieldSchemaAtPath = (schema, path) => parsePath(path).reduce((current, segme
|
|
|
269
463
|
}, schema);
|
|
270
464
|
|
|
271
465
|
// src/core/createForm.ts
|
|
272
|
-
var shallowFieldEqual = (a, b) => Object.is(a.value, b.value) && a.error === b.error && a.touched === b.touched && a.dirty === b.dirty && a.isValidating === b.isValidating;
|
|
466
|
+
var shallowFieldEqual = (a, b) => Object.is(a.value, b.value) && Object.is(a.initialValue, b.initialValue) && a.error === b.error && a.touched === b.touched && a.dirty === b.dirty && a.isValidating === b.isValidating;
|
|
273
467
|
var emptyErrors = {};
|
|
274
468
|
var emptyBools = {};
|
|
275
469
|
var identityMapper = (n) => n;
|
|
@@ -327,7 +521,26 @@ var reuseErrorRefs = (next, prev) => {
|
|
|
327
521
|
const sameShape = entries.length === Object.keys(prev).length && entries.every(([k, v]) => entryAt(prev, k) === v);
|
|
328
522
|
return sameShape ? prev : Object.fromEntries(entries);
|
|
329
523
|
};
|
|
330
|
-
var
|
|
524
|
+
var noScopeErrors = { scoped: emptyErrors, full: null };
|
|
525
|
+
var scopeParse = (result, path, viaSubschema) => {
|
|
526
|
+
const errors = result.kind === "invalid" ? result.errors : emptyErrors;
|
|
527
|
+
return viaSubschema ? { scoped: prefixErrorKeys(errors, path), full: null } : {
|
|
528
|
+
scoped: path === "" ? errors : pickScope(errors, [path]),
|
|
529
|
+
full: errors
|
|
530
|
+
};
|
|
531
|
+
};
|
|
532
|
+
var pruneUnreported = (prev, full) => {
|
|
533
|
+
const entries = Object.entries(prev);
|
|
534
|
+
const kept = entries.filter(([k]) => Object.hasOwn(full, k));
|
|
535
|
+
return kept.length === entries.length ? prev : Object.fromEntries(kept);
|
|
536
|
+
};
|
|
537
|
+
var dropEmptyLists = (map) => {
|
|
538
|
+
const entries = Object.entries(map);
|
|
539
|
+
const kept = entries.filter(
|
|
540
|
+
([, list]) => list !== void 0 && list.length > 0
|
|
541
|
+
);
|
|
542
|
+
return kept.length === entries.length ? map : Object.fromEntries(kept);
|
|
543
|
+
};
|
|
331
544
|
var mergeErrorChannels = (schema, server) => {
|
|
332
545
|
const extra = Object.entries(server).filter(
|
|
333
546
|
([k]) => !Object.hasOwn(schema, k)
|
|
@@ -437,17 +650,17 @@ var createForm = (schema, options) => {
|
|
|
437
650
|
const merged = scopes.reduce(
|
|
438
651
|
(acc, { path, scope }) => scope.kind === "skip" ? acc : {
|
|
439
652
|
...acc,
|
|
440
|
-
...
|
|
653
|
+
...scopeParse(
|
|
441
654
|
validateSyncLatched(path, scope.schema, scope.value),
|
|
442
655
|
path,
|
|
443
656
|
true
|
|
444
|
-
)
|
|
657
|
+
).scoped
|
|
445
658
|
},
|
|
446
659
|
{}
|
|
447
660
|
);
|
|
448
661
|
store.setState((state) => ({
|
|
449
662
|
...state,
|
|
450
|
-
...commitScopedErrors(state, merged, paths)
|
|
663
|
+
...commitScopedErrors(state, merged, paths, null)
|
|
451
664
|
}));
|
|
452
665
|
return settledFields(merged);
|
|
453
666
|
}
|
|
@@ -456,7 +669,7 @@ var createForm = (schema, options) => {
|
|
|
456
669
|
const requestedErrors = pickScope(fullErrors, paths);
|
|
457
670
|
store.setState((state) => ({
|
|
458
671
|
...state,
|
|
459
|
-
...commitScopedErrors(state, requestedErrors, paths)
|
|
672
|
+
...commitScopedErrors(state, requestedErrors, paths, fullErrors)
|
|
460
673
|
}));
|
|
461
674
|
return settledFields(requestedErrors);
|
|
462
675
|
} catch (e) {
|
|
@@ -501,7 +714,7 @@ var createForm = (schema, options) => {
|
|
|
501
714
|
const slot = slotAtPath(values, path);
|
|
502
715
|
return slot.exists ? { kind: "parse", schema: sub, value: slot.value, viaSubschema: true } : { kind: "skip" };
|
|
503
716
|
};
|
|
504
|
-
const parseScopeAsync = async (path, scope) => scope.kind === "skip" ?
|
|
717
|
+
const parseScopeAsync = async (path, scope) => scope.kind === "skip" ? noScopeErrors : scopeParse(
|
|
505
718
|
await validateAsync(scope.schema, scope.value),
|
|
506
719
|
path,
|
|
507
720
|
scope.viaSubschema
|
|
@@ -521,14 +734,18 @@ var createForm = (schema, options) => {
|
|
|
521
734
|
};
|
|
522
735
|
const settledFields = (scoped) => Object.keys(scoped).length === 0 ? { kind: "valid" } : { kind: "invalid", errors: scoped };
|
|
523
736
|
const settledFormResult = (result) => settledFields(result.kind === "invalid" ? result.errors : emptyErrors);
|
|
524
|
-
const commitScopedErrors = (state, scoped, paths) => errorChannels(state, {
|
|
737
|
+
const commitScopedErrors = (state, scoped, paths, full) => errorChannels(state, {
|
|
525
738
|
schemaErrors: reuseErrorRefs(
|
|
526
|
-
mergeScopedErrors(
|
|
739
|
+
mergeScopedErrors(
|
|
740
|
+
full === null ? state.schemaErrors : pruneUnreported(state.schemaErrors, full),
|
|
741
|
+
scoped,
|
|
742
|
+
paths
|
|
743
|
+
),
|
|
527
744
|
state.schemaErrors
|
|
528
745
|
),
|
|
529
746
|
serverErrors: omitScope(state.serverErrors, paths)
|
|
530
747
|
});
|
|
531
|
-
const commitFieldErrors = (state,
|
|
748
|
+
const commitFieldErrors = (state, parse, path) => path === "" ? commitFullPass(state, parse.scoped) : commitScopedErrors(state, parse.scoped, [path], parse.full);
|
|
532
749
|
const validateField = (path) => {
|
|
533
750
|
warnMissingSchemaPath("validateField", path);
|
|
534
751
|
const scope = fieldScopeFor(path, store.getState().values);
|
|
@@ -536,7 +753,7 @@ var createForm = (schema, options) => {
|
|
|
536
753
|
return { kind: "pending", promise: validateFieldAsync(path) };
|
|
537
754
|
}
|
|
538
755
|
try {
|
|
539
|
-
const
|
|
756
|
+
const parse = scope.kind === "skip" ? noScopeErrors : scopeParse(
|
|
540
757
|
validateSyncLatched(
|
|
541
758
|
scope.viaSubschema ? path : "",
|
|
542
759
|
scope.schema,
|
|
@@ -547,9 +764,9 @@ var createForm = (schema, options) => {
|
|
|
547
764
|
);
|
|
548
765
|
store.setState((state) => ({
|
|
549
766
|
...state,
|
|
550
|
-
...commitFieldErrors(state,
|
|
767
|
+
...commitFieldErrors(state, parse, path)
|
|
551
768
|
}));
|
|
552
|
-
return fieldResult(scoped);
|
|
769
|
+
return fieldResult(parse.scoped);
|
|
553
770
|
} catch (e) {
|
|
554
771
|
if (isAsyncRequiredError(e)) {
|
|
555
772
|
return { kind: "pending", promise: validateFieldAsync(path) };
|
|
@@ -559,11 +776,15 @@ var createForm = (schema, options) => {
|
|
|
559
776
|
};
|
|
560
777
|
const snapshotField = (state, path) => {
|
|
561
778
|
const value = getAtPath(state.values, path);
|
|
779
|
+
const initialValue = getAtPath(state.initialValues, path);
|
|
780
|
+
const error = entryAt(state.errors, path);
|
|
562
781
|
return {
|
|
563
782
|
value,
|
|
564
|
-
|
|
783
|
+
initialValue,
|
|
784
|
+
error,
|
|
785
|
+
firstError: error?.[0],
|
|
565
786
|
touched: flagAt(state.touched, path),
|
|
566
|
-
dirty: isFieldDirty(value,
|
|
787
|
+
dirty: isFieldDirty(value, initialValue),
|
|
567
788
|
isValidating: flagAt(state.isValidating, path)
|
|
568
789
|
};
|
|
569
790
|
};
|
|
@@ -577,7 +798,14 @@ var createForm = (schema, options) => {
|
|
|
577
798
|
const pass = claimPass(FORM_SEQ_KEY);
|
|
578
799
|
const valuesAtStart = store.getState().values;
|
|
579
800
|
store.setState((state) => ({ ...state, isValidatingForm: true }));
|
|
580
|
-
const result = await validateAsync(schema, valuesAtStart)
|
|
801
|
+
const result = await validateAsync(schema, valuesAtStart).catch(
|
|
802
|
+
(error) => {
|
|
803
|
+
if (sequences.get(FORM_SEQ_KEY) === pass) {
|
|
804
|
+
store.setState((state) => ({ ...state, isValidatingForm: false }));
|
|
805
|
+
}
|
|
806
|
+
throw error;
|
|
807
|
+
}
|
|
808
|
+
);
|
|
581
809
|
const live = store.getState();
|
|
582
810
|
const stillOwns = sequences.get(FORM_SEQ_KEY) === pass;
|
|
583
811
|
if (!stillOwns || live.values !== valuesAtStart) {
|
|
@@ -617,12 +845,15 @@ var createForm = (schema, options) => {
|
|
|
617
845
|
...Object.fromEntries(paths.map((p) => [p, true]))
|
|
618
846
|
}
|
|
619
847
|
}));
|
|
620
|
-
const
|
|
848
|
+
const parseBatch = async () => allViaSubschema ? {
|
|
621
849
|
kind: "slots",
|
|
622
850
|
byPath: new Map(
|
|
623
851
|
await Promise.all(
|
|
624
852
|
scopes.map(
|
|
625
|
-
async ({ path, scope }) => [
|
|
853
|
+
async ({ path, scope }) => [
|
|
854
|
+
path,
|
|
855
|
+
(await parseScopeAsync(path, scope)).scoped
|
|
856
|
+
]
|
|
626
857
|
)
|
|
627
858
|
)
|
|
628
859
|
)
|
|
@@ -632,6 +863,18 @@ var createForm = (schema, options) => {
|
|
|
632
863
|
(result) => result.kind === "invalid" ? result.errors : emptyErrors
|
|
633
864
|
)
|
|
634
865
|
};
|
|
866
|
+
const parsed = await parseBatch().catch((error) => {
|
|
867
|
+
const stillOwned = new Set(
|
|
868
|
+
paths.filter((p) => sequences.get(p) === passes.get(p))
|
|
869
|
+
);
|
|
870
|
+
if (stillOwned.size > 0) {
|
|
871
|
+
store.setState((state) => ({
|
|
872
|
+
...state,
|
|
873
|
+
isValidating: omitKeys(state.isValidating, stillOwned)
|
|
874
|
+
}));
|
|
875
|
+
}
|
|
876
|
+
throw error;
|
|
877
|
+
});
|
|
635
878
|
const errorsFor = (path) => {
|
|
636
879
|
switch (parsed.kind) {
|
|
637
880
|
case "slots":
|
|
@@ -660,7 +903,8 @@ var createForm = (schema, options) => {
|
|
|
660
903
|
Object.fromEntries(
|
|
661
904
|
fresh.flatMap((path) => Object.entries(errorsFor(path)))
|
|
662
905
|
),
|
|
663
|
-
fresh
|
|
906
|
+
fresh,
|
|
907
|
+
parsed.kind === "full" ? parsed.errors : null
|
|
664
908
|
),
|
|
665
909
|
isValidating: omitKeys(state.isValidating, ownedKeys)
|
|
666
910
|
}));
|
|
@@ -681,24 +925,32 @@ var createForm = (schema, options) => {
|
|
|
681
925
|
...state,
|
|
682
926
|
isValidating: { ...state.isValidating, [path]: true }
|
|
683
927
|
}));
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
const stillOwns = sequences.get(path) === pass;
|
|
687
|
-
if (!stillOwns || scopeStale(scope, path, valuesAtStart, live.values)) {
|
|
688
|
-
if (stillOwns) {
|
|
928
|
+
const releaseOwnFlag = () => {
|
|
929
|
+
if (sequences.get(path) === pass) {
|
|
689
930
|
store.setState((state) => ({
|
|
690
931
|
...state,
|
|
691
932
|
isValidating: omitKey(state.isValidating, path)
|
|
692
933
|
}));
|
|
693
934
|
}
|
|
694
|
-
|
|
935
|
+
};
|
|
936
|
+
const parse = await parseScopeAsync(path, scope).catch(
|
|
937
|
+
(error) => {
|
|
938
|
+
releaseOwnFlag();
|
|
939
|
+
throw error;
|
|
940
|
+
}
|
|
941
|
+
);
|
|
942
|
+
const live = store.getState();
|
|
943
|
+
const stillOwns = sequences.get(path) === pass;
|
|
944
|
+
if (!stillOwns || scopeStale(scope, path, valuesAtStart, live.values)) {
|
|
945
|
+
releaseOwnFlag();
|
|
946
|
+
return fieldResult(parse.scoped);
|
|
695
947
|
}
|
|
696
948
|
store.setState((state) => ({
|
|
697
949
|
...state,
|
|
698
|
-
...commitFieldErrors(state,
|
|
950
|
+
...commitFieldErrors(state, parse, path),
|
|
699
951
|
isValidating: omitKey(state.isValidating, path)
|
|
700
952
|
}));
|
|
701
|
-
return fieldResult(scoped);
|
|
953
|
+
return fieldResult(parse.scoped);
|
|
702
954
|
};
|
|
703
955
|
const applyArrayOp = (path, nextArray, mapper, indexInBounds) => {
|
|
704
956
|
const current = getAtPath(store.getState().values, path);
|
|
@@ -757,7 +1009,9 @@ var createForm = (schema, options) => {
|
|
|
757
1009
|
count: 0,
|
|
758
1010
|
baseline: false
|
|
759
1011
|
};
|
|
760
|
-
const submit = async (onValid,
|
|
1012
|
+
const submit = async (onValid, onInvalidOrOptions, positionalOptions) => {
|
|
1013
|
+
const submitOptions = typeof onInvalidOrOptions === "object" ? onInvalidOrOptions : positionalOptions;
|
|
1014
|
+
const onInvalid = typeof onInvalidOrOptions === "function" ? onInvalidOrOptions : submitOptions?.onInvalid;
|
|
761
1015
|
if (inFlight.count > 0 && submitOptions?.force !== true) {
|
|
762
1016
|
return { kind: "skipped" };
|
|
763
1017
|
}
|
|
@@ -798,6 +1052,7 @@ var createForm = (schema, options) => {
|
|
|
798
1052
|
try {
|
|
799
1053
|
onInvalid?.(merged);
|
|
800
1054
|
} catch (error) {
|
|
1055
|
+
submitOptions?.onError?.(error);
|
|
801
1056
|
return { kind: "error", error };
|
|
802
1057
|
}
|
|
803
1058
|
return { kind: "invalid", errors: merged };
|
|
@@ -824,7 +1079,10 @@ var createForm = (schema, options) => {
|
|
|
824
1079
|
}
|
|
825
1080
|
};
|
|
826
1081
|
if (options.validateOnMount === true) {
|
|
827
|
-
validate();
|
|
1082
|
+
const result = validate();
|
|
1083
|
+
if (result.kind === "pending") {
|
|
1084
|
+
void result.promise.catch(reportBackgroundFailure);
|
|
1085
|
+
}
|
|
828
1086
|
}
|
|
829
1087
|
return Object.freeze({
|
|
830
1088
|
schema,
|
|
@@ -917,7 +1175,7 @@ var createForm = (schema, options) => {
|
|
|
917
1175
|
},
|
|
918
1176
|
setErrors: (errors) => store.setState((state) => ({
|
|
919
1177
|
...state,
|
|
920
|
-
...errorChannels(state, { serverErrors: errors })
|
|
1178
|
+
...errorChannels(state, { serverErrors: dropEmptyLists(errors) })
|
|
921
1179
|
})),
|
|
922
1180
|
addErrors: (errors) => store.setState((state) => {
|
|
923
1181
|
const entries = Object.entries(errors);
|
|
@@ -960,11 +1218,11 @@ var createForm = (schema, options) => {
|
|
|
960
1218
|
if (next.values !== state.values) {
|
|
961
1219
|
clearArrayOpsUnder(store, "");
|
|
962
1220
|
}
|
|
963
|
-
const serverErrors = patch.serverErrors !== void 0 ? patch.serverErrors : next.values !== state.values ? releaseChangedSlices(state.serverErrors, state.values, next.values) : state.serverErrors;
|
|
1221
|
+
const serverErrors = patch.serverErrors !== void 0 ? dropEmptyLists(patch.serverErrors) : next.values !== state.values ? releaseChangedSlices(state.serverErrors, state.values, next.values) : state.serverErrors;
|
|
964
1222
|
store.setState(() => ({
|
|
965
1223
|
...next,
|
|
966
1224
|
...errorChannels(state, {
|
|
967
|
-
schemaErrors:
|
|
1225
|
+
schemaErrors: patch.schemaErrors === void 0 ? state.schemaErrors : dropEmptyLists(patch.schemaErrors),
|
|
968
1226
|
serverErrors
|
|
969
1227
|
})
|
|
970
1228
|
}));
|
|
@@ -1073,9 +1331,14 @@ var createForm = (schema, options) => {
|
|
|
1073
1331
|
swapIndices(a, b),
|
|
1074
1332
|
(len) => Number.isInteger(a) && Number.isInteger(b) && a >= 0 && a < len && b >= 0 && b < len
|
|
1075
1333
|
),
|
|
1076
|
-
handleSubmit: (onValid,
|
|
1334
|
+
handleSubmit: (onValid, onInvalidOrOptions, positionalOptions) => async (event) => {
|
|
1077
1335
|
event?.preventDefault();
|
|
1078
|
-
const
|
|
1336
|
+
const opts = typeof onInvalidOrOptions === "object" ? onInvalidOrOptions : positionalOptions;
|
|
1337
|
+
const result = await submit(
|
|
1338
|
+
onValid,
|
|
1339
|
+
onInvalidOrOptions,
|
|
1340
|
+
positionalOptions
|
|
1341
|
+
);
|
|
1079
1342
|
if (result.kind === "error" && opts?.onError === void 0 && isDevBuild()) {
|
|
1080
1343
|
console.warn(
|
|
1081
1344
|
"[formstand] submit handler threw and nothing observed it \u2014 the form shows no feedback. Pass { onError } to handleSubmit (or setError inside your handler):",
|
|
@@ -1101,8 +1364,8 @@ var createForm = (schema, options) => {
|
|
|
1101
1364
|
restore: (snap) => {
|
|
1102
1365
|
clearArrayOpsUnder(store, "");
|
|
1103
1366
|
store.setState(() => {
|
|
1104
|
-
const schemaErrors = snap.schemaErrors ?? emptyErrors;
|
|
1105
|
-
const serverErrors = snap.serverErrors ?? emptyErrors;
|
|
1367
|
+
const schemaErrors = dropEmptyLists(snap.schemaErrors ?? emptyErrors);
|
|
1368
|
+
const serverErrors = dropEmptyLists(snap.serverErrors ?? emptyErrors);
|
|
1106
1369
|
return {
|
|
1107
1370
|
...snap,
|
|
1108
1371
|
schemaErrors,
|
|
@@ -1132,11 +1395,13 @@ var createDebouncer = (ms) => {
|
|
|
1132
1395
|
const timer = {
|
|
1133
1396
|
current: null
|
|
1134
1397
|
};
|
|
1398
|
+
const scheduled = { current: null };
|
|
1135
1399
|
const cancel = () => {
|
|
1136
1400
|
if (timer.current !== null) {
|
|
1137
1401
|
clearTimeout(timer.current);
|
|
1138
1402
|
timer.current = null;
|
|
1139
1403
|
}
|
|
1404
|
+
scheduled.current = null;
|
|
1140
1405
|
};
|
|
1141
1406
|
const schedule = (run) => {
|
|
1142
1407
|
cancel();
|
|
@@ -1144,37 +1409,50 @@ var createDebouncer = (ms) => {
|
|
|
1144
1409
|
run();
|
|
1145
1410
|
return;
|
|
1146
1411
|
}
|
|
1412
|
+
scheduled.current = run;
|
|
1147
1413
|
timer.current = setTimeout(() => {
|
|
1148
1414
|
timer.current = null;
|
|
1415
|
+
scheduled.current = null;
|
|
1149
1416
|
run();
|
|
1150
1417
|
}, ms);
|
|
1151
1418
|
};
|
|
1419
|
+
const flush = () => {
|
|
1420
|
+
const run = scheduled.current;
|
|
1421
|
+
if (run === null) return;
|
|
1422
|
+
cancel();
|
|
1423
|
+
run();
|
|
1424
|
+
};
|
|
1152
1425
|
return Object.freeze({
|
|
1153
1426
|
schedule,
|
|
1154
1427
|
cancel,
|
|
1428
|
+
flush,
|
|
1155
1429
|
pending: () => timer.current !== null
|
|
1156
1430
|
});
|
|
1157
1431
|
};
|
|
1158
1432
|
|
|
1159
1433
|
// src/core/persist.ts
|
|
1160
1434
|
var kindOf = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
|
|
1161
|
-
var
|
|
1162
|
-
|
|
1163
|
-
if (
|
|
1435
|
+
var joinPath = (base, key) => base === "" ? String(key) : `${base}.${String(key)}`;
|
|
1436
|
+
var conflictPath = (draft, reference, path, flexibleAt) => {
|
|
1437
|
+
if (draft === void 0 || draft === null) return null;
|
|
1438
|
+
if (reference === void 0 || reference === null) return null;
|
|
1164
1439
|
const draftKind = kindOf(draft);
|
|
1165
1440
|
const referenceKind = kindOf(reference);
|
|
1166
|
-
if (draftKind !== referenceKind) return
|
|
1441
|
+
if (draftKind !== referenceKind) return flexibleAt(path) ? null : path;
|
|
1167
1442
|
if (draftKind === "array") {
|
|
1168
1443
|
const [d] = draft;
|
|
1169
1444
|
const [r] = reference;
|
|
1170
|
-
return
|
|
1445
|
+
return conflictPath(d, r, joinPath(path, 0), flexibleAt);
|
|
1171
1446
|
}
|
|
1172
1447
|
if (draftKind === "object") {
|
|
1173
1448
|
const d = draft;
|
|
1174
1449
|
const r = reference;
|
|
1175
|
-
return Object.keys(d).
|
|
1450
|
+
return Object.keys(d).reduce(
|
|
1451
|
+
(found, key) => found ?? conflictPath(d[key], r[key], joinPath(path, key), flexibleAt),
|
|
1452
|
+
null
|
|
1453
|
+
);
|
|
1176
1454
|
}
|
|
1177
|
-
return
|
|
1455
|
+
return null;
|
|
1178
1456
|
};
|
|
1179
1457
|
var defaultStorage = () => {
|
|
1180
1458
|
try {
|
|
@@ -1187,7 +1465,35 @@ var persistForm = (form, options) => {
|
|
|
1187
1465
|
const storage = options.storage ?? defaultStorage();
|
|
1188
1466
|
const debounceMs = options.debounceMs ?? 300;
|
|
1189
1467
|
const apply = options.apply ?? "adopt";
|
|
1190
|
-
const
|
|
1468
|
+
const schema = form.schema;
|
|
1469
|
+
const fillRequiredGaps = (draft, initial, path) => {
|
|
1470
|
+
if (!isPlainObject(draft) || !isPlainObject(initial)) return draft;
|
|
1471
|
+
return Object.keys(initial).reduce(
|
|
1472
|
+
(acc, key) => {
|
|
1473
|
+
const childPath = joinPath(path, key);
|
|
1474
|
+
if (Object.hasOwn(draft, key)) {
|
|
1475
|
+
const next = fillRequiredGaps(draft[key], initial[key], childPath);
|
|
1476
|
+
return Object.is(next, draft[key]) ? acc : { ...acc, [key]: next };
|
|
1477
|
+
}
|
|
1478
|
+
if (initial[key] === void 0) return acc;
|
|
1479
|
+
const sub = fieldSchemaForIntrospection(schema, childPath);
|
|
1480
|
+
return sub !== null && isClearableSchema(sub) ? acc : { ...acc, [key]: initial[key] };
|
|
1481
|
+
},
|
|
1482
|
+
draft
|
|
1483
|
+
);
|
|
1484
|
+
};
|
|
1485
|
+
const applyDraft = (values) => {
|
|
1486
|
+
const filled = fillRequiredGaps(
|
|
1487
|
+
values,
|
|
1488
|
+
form.getState().initialValues,
|
|
1489
|
+
""
|
|
1490
|
+
);
|
|
1491
|
+
if (apply === "edit" || apply === "restore") {
|
|
1492
|
+
form.setValues(filled);
|
|
1493
|
+
} else {
|
|
1494
|
+
form.adoptValues(filled);
|
|
1495
|
+
}
|
|
1496
|
+
};
|
|
1191
1497
|
const referenceShape = () => {
|
|
1192
1498
|
try {
|
|
1193
1499
|
return JSON.parse(JSON.stringify(form.getState().initialValues));
|
|
@@ -1195,31 +1501,68 @@ var persistForm = (form, options) => {
|
|
|
1195
1501
|
return void 0;
|
|
1196
1502
|
}
|
|
1197
1503
|
};
|
|
1198
|
-
const
|
|
1504
|
+
const flexibleAt = (path) => schemaAllowsKindChangeAt(schema, path);
|
|
1505
|
+
const inspect = () => {
|
|
1506
|
+
const raw = (() => {
|
|
1507
|
+
try {
|
|
1508
|
+
return storage === null ? null : storage.getItem(options.key);
|
|
1509
|
+
} catch {
|
|
1510
|
+
return null;
|
|
1511
|
+
}
|
|
1512
|
+
})();
|
|
1513
|
+
if (raw === null) return { kind: "absent" };
|
|
1199
1514
|
try {
|
|
1200
|
-
const raw = storage === null ? null : storage.getItem(options.key);
|
|
1201
|
-
if (raw === null) return false;
|
|
1202
1515
|
const parsed = JSON.parse(raw);
|
|
1203
1516
|
const versioned = options.version !== void 0 && typeof parsed === "object" && parsed !== null && "__v" in parsed;
|
|
1204
|
-
if (options.version !== void 0 && !versioned)
|
|
1517
|
+
if (options.version !== void 0 && !versioned) {
|
|
1518
|
+
return {
|
|
1519
|
+
kind: "ignored",
|
|
1520
|
+
reason: `it carries no version and this configuration expects version ${String(options.version)}`
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1205
1523
|
if (options.version === void 0 && typeof parsed === "object" && parsed !== null && Object.hasOwn(parsed, "__v") && Object.hasOwn(parsed, "values")) {
|
|
1206
|
-
return
|
|
1524
|
+
return {
|
|
1525
|
+
kind: "ignored",
|
|
1526
|
+
reason: "it was written with a version and this configuration has none"
|
|
1527
|
+
};
|
|
1207
1528
|
}
|
|
1208
1529
|
const wrapper = parsed;
|
|
1209
|
-
if (versioned && wrapper.__v !== options.version)
|
|
1210
|
-
|
|
1530
|
+
if (versioned && wrapper.__v !== options.version) {
|
|
1531
|
+
return {
|
|
1532
|
+
kind: "ignored",
|
|
1533
|
+
reason: `its version (${String(wrapper.__v)}) is not the configured version (${String(options.version)})`
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
if (versioned && wrapper.values === void 0) {
|
|
1537
|
+
return { kind: "ignored", reason: "its version wrapper holds no values" };
|
|
1538
|
+
}
|
|
1211
1539
|
const values = versioned ? wrapper.values : parsed;
|
|
1212
|
-
if (values === null || values === void 0)
|
|
1540
|
+
if (values === null || values === void 0) {
|
|
1541
|
+
return { kind: "ignored", reason: "it holds no values" };
|
|
1542
|
+
}
|
|
1213
1543
|
const reference = referenceShape();
|
|
1214
|
-
|
|
1215
|
-
|
|
1544
|
+
const conflict = reference === void 0 ? null : conflictPath(values, reference, "", flexibleAt);
|
|
1545
|
+
if (conflict !== null) {
|
|
1546
|
+
return {
|
|
1547
|
+
kind: "ignored",
|
|
1548
|
+
reason: `its shape conflicts with the form's initial values at "${conflict}" (set the version option when the schema changes)`
|
|
1549
|
+
};
|
|
1216
1550
|
}
|
|
1217
1551
|
applyDraft(values);
|
|
1218
|
-
return
|
|
1552
|
+
return { kind: "applied" };
|
|
1219
1553
|
} catch {
|
|
1220
|
-
return
|
|
1554
|
+
return { kind: "ignored", reason: "it is not valid JSON" };
|
|
1221
1555
|
}
|
|
1222
1556
|
};
|
|
1557
|
+
const load = () => {
|
|
1558
|
+
const outcome = inspect();
|
|
1559
|
+
if (outcome.kind === "ignored" && isDevBuild()) {
|
|
1560
|
+
console.warn(
|
|
1561
|
+
`[formstand] persistForm: ignored the draft under "${options.key}": ${outcome.reason}. The next edit overwrites it.`
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
return outcome.kind === "applied";
|
|
1565
|
+
};
|
|
1223
1566
|
const writeDraft = (values) => {
|
|
1224
1567
|
try {
|
|
1225
1568
|
storage?.setItem(
|
|
@@ -1232,24 +1575,28 @@ var persistForm = (form, options) => {
|
|
|
1232
1575
|
}
|
|
1233
1576
|
};
|
|
1234
1577
|
const writes = createDebouncer(debounceMs);
|
|
1235
|
-
const cancelPending = () => writes.cancel();
|
|
1236
1578
|
if (apply !== "manual") {
|
|
1237
1579
|
load();
|
|
1238
1580
|
}
|
|
1581
|
+
const cleared = { current: false };
|
|
1239
1582
|
const unsubscribe = form.watchValues((values) => {
|
|
1583
|
+
if (cleared.current && values === form.getState().initialValues) return;
|
|
1584
|
+
cleared.current = false;
|
|
1240
1585
|
writes.schedule(() => writeDraft(values));
|
|
1241
1586
|
});
|
|
1242
1587
|
return Object.freeze({
|
|
1243
1588
|
load,
|
|
1589
|
+
flush: () => writes.flush(),
|
|
1244
1590
|
clear: () => {
|
|
1245
|
-
|
|
1591
|
+
writes.cancel();
|
|
1592
|
+
cleared.current = true;
|
|
1246
1593
|
try {
|
|
1247
1594
|
storage?.removeItem(options.key);
|
|
1248
1595
|
} catch {
|
|
1249
1596
|
}
|
|
1250
1597
|
},
|
|
1251
1598
|
dispose: () => {
|
|
1252
|
-
|
|
1599
|
+
writes.flush();
|
|
1253
1600
|
unsubscribe();
|
|
1254
1601
|
}
|
|
1255
1602
|
});
|
|
@@ -1261,6 +1608,7 @@ var usePersistForm = (form, options) => {
|
|
|
1261
1608
|
latestOptions.current = options;
|
|
1262
1609
|
});
|
|
1263
1610
|
const key = options.key;
|
|
1611
|
+
const storage = options.storage;
|
|
1264
1612
|
useEffect(() => {
|
|
1265
1613
|
const handle = persistForm(form, latestOptions.current);
|
|
1266
1614
|
inner.current = handle;
|
|
@@ -1268,10 +1616,11 @@ var usePersistForm = (form, options) => {
|
|
|
1268
1616
|
inner.current = null;
|
|
1269
1617
|
handle.dispose();
|
|
1270
1618
|
};
|
|
1271
|
-
}, [form, key]);
|
|
1619
|
+
}, [form, key, storage]);
|
|
1272
1620
|
return useMemo(
|
|
1273
1621
|
() => ({
|
|
1274
1622
|
load: () => inner.current?.load() ?? false,
|
|
1623
|
+
flush: () => inner.current?.flush(),
|
|
1275
1624
|
clear: () => inner.current?.clear(),
|
|
1276
1625
|
// dispose stops THIS component's watching early; the effect cleanup
|
|
1277
1626
|
// still runs its own dispose harmlessly (persistForm's dispose is
|
|
@@ -1305,9 +1654,12 @@ var useForm = (schema, options) => {
|
|
|
1305
1654
|
var hasFieldError = (error) => error !== void 0 && error.length > 0;
|
|
1306
1655
|
var ariaInvalid = (field) => hasFieldError(field.error) ? true : void 0;
|
|
1307
1656
|
var numberToInputText = (value) => value === void 0 || value === null || Number.isNaN(value) ? "" : String(value);
|
|
1657
|
+
var DECIMAL_TEXT = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
|
|
1308
1658
|
var parseNumberText = (text) => {
|
|
1309
|
-
|
|
1310
|
-
|
|
1659
|
+
const trimmed = text.trim();
|
|
1660
|
+
if (trimmed === "") return { kind: "empty" };
|
|
1661
|
+
if (!DECIMAL_TEXT.test(trimmed)) return { kind: "invalid" };
|
|
1662
|
+
const parsed = Number(trimmed);
|
|
1311
1663
|
return Number.isFinite(parsed) ? { kind: "number", value: parsed } : { kind: "invalid" };
|
|
1312
1664
|
};
|
|
1313
1665
|
var textInputProps = (field) => ({
|
|
@@ -1335,11 +1687,11 @@ var numberInputProps = (field) => ({
|
|
|
1335
1687
|
onBlur: field.onBlur
|
|
1336
1688
|
});
|
|
1337
1689
|
var dateToInputText = (value) => {
|
|
1338
|
-
if (value === void 0 || value === null
|
|
1339
|
-
|
|
1340
|
-
|
|
1690
|
+
if (value === void 0 || value === null) return "";
|
|
1691
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
1692
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
1341
1693
|
const pad = (part) => String(part).padStart(2, "0");
|
|
1342
|
-
return `${String(
|
|
1694
|
+
return `${String(date.getFullYear()).padStart(4, "0")}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
1343
1695
|
};
|
|
1344
1696
|
var parseDateText = (text) => {
|
|
1345
1697
|
if (text.trim() === "") return { kind: "empty" };
|
|
@@ -1419,7 +1771,8 @@ var useMaskedInput = (field, options) => {
|
|
|
1419
1771
|
field.setValue(parsed.value);
|
|
1420
1772
|
return;
|
|
1421
1773
|
case "empty": {
|
|
1422
|
-
const
|
|
1774
|
+
const isStringField = typeof field.initialValue === "string" || typeof field.value === "string";
|
|
1775
|
+
const empty = field.clearable || !isStringField ? field.emptyValue : "";
|
|
1423
1776
|
setEdit({ raw: text, pushed: empty });
|
|
1424
1777
|
field.setValue(empty);
|
|
1425
1778
|
return;
|
|
@@ -1478,7 +1831,8 @@ var useFormSteps = (form, steps) => {
|
|
|
1478
1831
|
const goTo = useCallback(
|
|
1479
1832
|
async (target) => {
|
|
1480
1833
|
const count = latest.current.length;
|
|
1481
|
-
|
|
1834
|
+
if (!Number.isFinite(target)) return currentStep.current;
|
|
1835
|
+
const clamped = Math.max(0, Math.min(Math.trunc(target), count - 1));
|
|
1482
1836
|
if (clamped <= step) {
|
|
1483
1837
|
setStep(clamped);
|
|
1484
1838
|
return clamped;
|
|
@@ -1551,8 +1905,7 @@ var useFormAction = (form, onValid, options) => {
|
|
|
1551
1905
|
const { onValid: handler, options: opts } = latest.current;
|
|
1552
1906
|
const result = await form.submit(
|
|
1553
1907
|
(data) => handler(data, formData),
|
|
1554
|
-
opts
|
|
1555
|
-
opts?.onError === void 0 ? void 0 : { onError: opts.onError }
|
|
1908
|
+
opts ?? {}
|
|
1556
1909
|
);
|
|
1557
1910
|
warnUnobserved(result, opts, "useFormAction");
|
|
1558
1911
|
},
|
|
@@ -1568,8 +1921,7 @@ var useFormActionState = (form, action, initialState, options) => {
|
|
|
1568
1921
|
async (data) => {
|
|
1569
1922
|
nextRef.current = await action(prev, data, formData);
|
|
1570
1923
|
},
|
|
1571
|
-
options
|
|
1572
|
-
options?.onError === void 0 ? void 0 : { onError: options.onError }
|
|
1924
|
+
options ?? {}
|
|
1573
1925
|
);
|
|
1574
1926
|
warnUnobserved(result, options, "useFormActionState");
|
|
1575
1927
|
return nextRef.current;
|
|
@@ -1635,6 +1987,12 @@ function useField(form, pathArg, options) {
|
|
|
1635
1987
|
debouncerRef.current = null;
|
|
1636
1988
|
};
|
|
1637
1989
|
}, [form, path, debounceMs]);
|
|
1990
|
+
const validateNow = useCallback(() => {
|
|
1991
|
+
const result = form.validateField(path);
|
|
1992
|
+
if (result.kind === "pending") {
|
|
1993
|
+
void result.promise.catch(reportBackgroundFailure);
|
|
1994
|
+
}
|
|
1995
|
+
}, [form, path]);
|
|
1638
1996
|
const triggerValidate = useCallback(() => {
|
|
1639
1997
|
if (debounceMs !== void 0 && debounceMs > 0) {
|
|
1640
1998
|
const debouncer = debouncerRef.current ?? createDebouncer(debounceMs);
|
|
@@ -1646,12 +2004,12 @@ function useField(form, pathArg, options) {
|
|
|
1646
2004
|
const rebased = state.values !== scheduledValues && state.values === state.initialValues;
|
|
1647
2005
|
if (rebased) return;
|
|
1648
2006
|
if (!Object.is(getAtPath(state.values, path), scheduled)) return;
|
|
1649
|
-
void form.validateFieldAsync(path);
|
|
2007
|
+
void form.validateFieldAsync(path).catch(reportBackgroundFailure);
|
|
1650
2008
|
});
|
|
1651
2009
|
return;
|
|
1652
2010
|
}
|
|
1653
|
-
|
|
1654
|
-
}, [form, path, debounceMs]);
|
|
2011
|
+
validateNow();
|
|
2012
|
+
}, [form, path, debounceMs, validateNow]);
|
|
1655
2013
|
const setValue = useCallback(
|
|
1656
2014
|
(value) => {
|
|
1657
2015
|
const before = form.store.getState();
|
|
@@ -1700,9 +2058,10 @@ function useField(form, pathArg, options) {
|
|
|
1700
2058
|
state.submitCount > 0,
|
|
1701
2059
|
true
|
|
1702
2060
|
)) {
|
|
1703
|
-
|
|
2061
|
+
debouncerRef.current?.cancel();
|
|
2062
|
+
validateNow();
|
|
1704
2063
|
}
|
|
1705
|
-
}, [form, path,
|
|
2064
|
+
}, [form, path, validateNow]);
|
|
1706
2065
|
return useMemo(
|
|
1707
2066
|
() => ({
|
|
1708
2067
|
path: slice.path,
|
|
@@ -2069,29 +2428,37 @@ var useFormError = (form) => useStore(
|
|
|
2069
2428
|
function useFormValues(form) {
|
|
2070
2429
|
return useStore(form.store, (state) => state.values);
|
|
2071
2430
|
}
|
|
2072
|
-
|
|
2073
|
-
// src/react/useFields.ts
|
|
2074
2431
|
function useFields(form, paths) {
|
|
2432
|
+
const [arity] = useState(paths.length);
|
|
2433
|
+
if (paths.length !== arity) {
|
|
2434
|
+
throw new Error(
|
|
2435
|
+
`[formstand] useFields: the paths tuple changed length (${String(arity)} to ${String(paths.length)}); its length and order must be stable across renders, so pass a literal or a module-level constant.`
|
|
2436
|
+
);
|
|
2437
|
+
}
|
|
2075
2438
|
const fields = paths.map(
|
|
2076
2439
|
(path) => (
|
|
2077
2440
|
// eslint-disable-next-line react-hooks/rules-of-hooks -- a fixed hook count by the stable-arity contract above; each element must be a REAL UseFieldReturn so it spreads into the prop builders
|
|
2078
2441
|
useField(form, path)
|
|
2079
2442
|
)
|
|
2080
2443
|
);
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
fields.
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2444
|
+
return useMemo(() => {
|
|
2445
|
+
const merged = [
|
|
2446
|
+
...new Set(fields.flatMap((field) => field.error ?? []))
|
|
2447
|
+
];
|
|
2448
|
+
return {
|
|
2449
|
+
fields,
|
|
2450
|
+
error: merged.length > 0 ? merged : void 0,
|
|
2451
|
+
firstError: merged[0],
|
|
2452
|
+
touched: fields.some((field) => field.touched),
|
|
2453
|
+
dirty: fields.some((field) => field.dirty),
|
|
2454
|
+
isValidating: fields.some((field) => field.isValidating),
|
|
2455
|
+
onBlur: () => {
|
|
2456
|
+
fields.forEach((field) => {
|
|
2457
|
+
field.onBlur();
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
};
|
|
2461
|
+
}, fields);
|
|
2095
2462
|
}
|
|
2096
2463
|
function useIsDirty(form, path) {
|
|
2097
2464
|
return useStore(
|
|
@@ -2159,6 +2526,9 @@ var createFormHooks = (form, name) => {
|
|
|
2159
2526
|
);
|
|
2160
2527
|
const useBoundSubmitCount = () => useSubmitCount(structural);
|
|
2161
2528
|
const useBoundSteps = (steps) => useFormSteps(form, steps);
|
|
2529
|
+
const useBoundAction = (onValid, options) => useFormAction(form, onValid, options);
|
|
2530
|
+
const useBoundActionState = (action, initialState, options) => useFormActionState(form, action, initialState, options);
|
|
2531
|
+
const useBoundPersist = (options) => usePersistForm(form, options);
|
|
2162
2532
|
const prefix = capitalize(name ?? "");
|
|
2163
2533
|
return {
|
|
2164
2534
|
[`use${prefix}Field`]: useBoundField,
|
|
@@ -2175,7 +2545,10 @@ var createFormHooks = (form, name) => {
|
|
|
2175
2545
|
[`use${prefix}SubmitCount`]: useBoundSubmitCount,
|
|
2176
2546
|
[`use${prefix}Values`]: useBoundValues,
|
|
2177
2547
|
[`use${prefix}Fields`]: useBoundFields,
|
|
2178
|
-
[`use${prefix}Steps`]: useBoundSteps
|
|
2548
|
+
[`use${prefix}Steps`]: useBoundSteps,
|
|
2549
|
+
[`use${prefix}Action`]: useBoundAction,
|
|
2550
|
+
[`use${prefix}ActionState`]: useBoundActionState,
|
|
2551
|
+
[`use${prefix}Persist`]: useBoundPersist
|
|
2179
2552
|
};
|
|
2180
2553
|
};
|
|
2181
2554
|
var useFieldA11y = (error) => {
|
|
@@ -2470,6 +2843,6 @@ var focusField = (path, root) => {
|
|
|
2470
2843
|
return path === "" ? (root !== void 0 || document.querySelectorAll("form").length <= 1) && focusFirstOf(namedControls(scope).filter(isFocusCandidate)) : focusFirstOf(pathCandidates(scope, [path]).filter(isFocusCandidate));
|
|
2471
2844
|
};
|
|
2472
2845
|
|
|
2473
|
-
export { CheckboxField, DateField, NumberField, SelectField, TextField, checkboxProps, createForm, createFormContext, createFormHooks, dateInputProps, dateToInputText, emptyValueForSchema, flattenIssues, focusField, focusFirstError, hasFieldError, isAsyncRequiredError, isClearableSchema, numberInputProps, numberToInputText, parseDateText, parseNumberText, persistForm, selectProps, textInputProps, useField, useFieldArray, useFields, useForm, useFormAction, useFormActionState, useFormError, useFormSteps, useFormValues, useIsDirty, useIsSubmitting, useIsValid, useIsValidating, useMaskedInput, useNumberInput, usePersistForm, useSubmitCount, useVariantField, useVariantFieldArray, validateAsync, validateSync };
|
|
2846
|
+
export { CheckboxField, DateField, NumberField, SelectField, TextField, checkboxProps, createForm, createFormContext, createFormHooks, dateInputProps, dateToInputText, emptyValueForSchema, flattenIssues, focusField, focusFirstError, getAtPath, hasFieldError, isAsyncRequiredError, isClearableSchema, numberInputProps, numberToInputText, parseDateText, parseNumberText, parsePath, persistForm, selectProps, setAtPath, textInputProps, useField, useFieldArray, useFields, useForm, useFormAction, useFormActionState, useFormError, useFormSteps, useFormValues, useIsDirty, useIsSubmitting, useIsValid, useIsValidating, useMaskedInput, useNumberInput, usePersistForm, useSubmitCount, useVariantField, useVariantFieldArray, validateAsync, validateSync };
|
|
2474
2847
|
//# sourceMappingURL=index.js.map
|
|
2475
2848
|
//# sourceMappingURL=index.js.map
|