formstand 0.25.0 → 0.26.1
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 +205 -0
- package/dist/chunk-XJ6W5CKB.js +22 -0
- package/dist/chunk-XJ6W5CKB.js.map +1 -0
- package/dist/{createForm-CMVALnc3.d.cts → createForm-BXDd9Tg4.d.cts} +18 -7
- package/dist/{createForm-CMVALnc3.d.ts → createForm-BXDd9Tg4.d.ts} +18 -7
- package/dist/devtools/index.cjs +40 -56
- package/dist/devtools/index.cjs.map +1 -1
- package/dist/devtools/index.js +41 -23
- package/dist/devtools/index.js.map +1 -1
- package/dist/index.cjs +535 -163
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -11
- package/dist/index.d.ts +30 -11
- package/dist/index.js +682 -158
- package/dist/index.js.map +1 -1
- package/dist/testing/index.cjs +7 -11
- 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 +7 -11
- 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,162 @@ 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
|
+
const child = writeSegments(arr[index], rest, value, kindAt, [
|
|
204
|
+
...prefix,
|
|
205
|
+
index
|
|
206
|
+
]);
|
|
207
|
+
if (index < arr.length && child === arr[index]) return arr;
|
|
208
|
+
return Array.from({ length }, (_, i) => i === index ? child : arr[i]);
|
|
209
|
+
};
|
|
210
|
+
var refusesHugeIndex = (index) => {
|
|
211
|
+
if (index > MAX_ARRAY_INDEX) {
|
|
212
|
+
console.warn(
|
|
213
|
+
`[formstand] refusing to write array index ${index} (max ${MAX_ARRAY_INDEX}); value left unchanged.`
|
|
214
|
+
);
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
};
|
|
219
|
+
var writeSegments = (obj, segments, value, kindAt, prefix) => {
|
|
220
|
+
const [head, ...rest] = segments;
|
|
221
|
+
if (head === void 0) return value;
|
|
222
|
+
if (Array.isArray(obj)) {
|
|
223
|
+
if (typeof head !== "number") {
|
|
224
|
+
console.warn(
|
|
225
|
+
`[formstand] cannot write string key "${head}" into an array; value left unchanged.`
|
|
226
|
+
);
|
|
227
|
+
return obj;
|
|
228
|
+
}
|
|
229
|
+
if (refusesHugeIndex(head)) return obj;
|
|
230
|
+
return writeIntoArray(obj, head, rest, value, kindAt, prefix);
|
|
231
|
+
}
|
|
232
|
+
if (isPlainRecord(obj)) {
|
|
233
|
+
if (!isPlainObject(obj)) {
|
|
234
|
+
console.warn(
|
|
235
|
+
`[formstand] cannot write key "${String(head)}" through a non-plain object (Date/Map/Set/class instance); value left unchanged.`
|
|
236
|
+
);
|
|
237
|
+
return obj;
|
|
238
|
+
}
|
|
239
|
+
const key2 = String(head);
|
|
240
|
+
const child = writeSegments(obj[key2], rest, value, kindAt, [
|
|
241
|
+
...prefix,
|
|
242
|
+
key2
|
|
243
|
+
]);
|
|
244
|
+
if (child === obj[key2] && Object.hasOwn(obj, key2)) return obj;
|
|
245
|
+
return { ...obj, [key2]: child };
|
|
246
|
+
}
|
|
247
|
+
const kind = kindAt?.(prefix.map(String).join(".")) ?? (typeof head === "number" ? "array" : "record");
|
|
248
|
+
if (kind === "array") {
|
|
249
|
+
if (typeof head !== "number") {
|
|
250
|
+
console.warn(
|
|
251
|
+
`[formstand] cannot write string key "${head}" into an array; value left unchanged.`
|
|
252
|
+
);
|
|
253
|
+
return obj;
|
|
254
|
+
}
|
|
255
|
+
if (refusesHugeIndex(head)) return obj;
|
|
256
|
+
return writeIntoArray([], head, rest, value, kindAt, prefix);
|
|
257
|
+
}
|
|
258
|
+
const key = String(head);
|
|
259
|
+
return { [key]: writeSegments(void 0, rest, value, kindAt, [...prefix, key]) };
|
|
260
|
+
};
|
|
261
|
+
var setAtPath = (obj, path, value, containerKindAt2) => writeSegments(obj, parsePath(path), value, containerKindAt2, []);
|
|
106
262
|
var issuePathToFormPath = (path) => path.filter((segment) => typeof segment !== "symbol").join(".");
|
|
107
263
|
var expandIssue = (issue, prefix) => {
|
|
108
264
|
const at = [...prefix, ...issue.path];
|
|
@@ -134,6 +290,12 @@ var validateAsync = async (schema, values) => {
|
|
|
134
290
|
return result.success ? { kind: "valid", data: result.data } : { kind: "invalid", errors: flattenIssues(result.error.issues) };
|
|
135
291
|
};
|
|
136
292
|
var isAsyncRequiredError = (e) => e instanceof z.core.$ZodAsyncError || e instanceof Error && /Encountered Promise during synchronous parse/i.test(e.message);
|
|
293
|
+
var reportBackgroundFailure = (error) => {
|
|
294
|
+
console.error(
|
|
295
|
+
"[formstand] a background validation pass threw (an async refinement rejected). Its in-flight flags were released. The error:",
|
|
296
|
+
error
|
|
297
|
+
);
|
|
298
|
+
};
|
|
137
299
|
var isPathOrChild = (key, path) => key === path || key.startsWith(`${path}.`);
|
|
138
300
|
var prefixErrorKeys = (errors, path) => path === "" ? errors : Object.fromEntries(
|
|
139
301
|
Object.entries(errors).map(([k, v]) => [
|
|
@@ -159,7 +321,11 @@ var unwrapForWalk = (s) => {
|
|
|
159
321
|
if (s instanceof z.ZodDefault || s instanceof z.ZodPrefault || s instanceof z.ZodCatch || s instanceof z.ZodReadonly || s instanceof z.ZodNonOptional) {
|
|
160
322
|
return s.def.innerType;
|
|
161
323
|
}
|
|
162
|
-
if (s instanceof z.ZodPipe)
|
|
324
|
+
if (s instanceof z.ZodPipe) {
|
|
325
|
+
const input = s.def.in;
|
|
326
|
+
return input instanceof z.ZodTransform ? s.def.out : input;
|
|
327
|
+
}
|
|
328
|
+
if (s instanceof z.ZodLazy) return s.def.getter();
|
|
163
329
|
return null;
|
|
164
330
|
};
|
|
165
331
|
var isLeafSchema = (s) => s instanceof z.ZodString || s instanceof z.ZodNumber || s instanceof z.ZodBoolean || s instanceof z.ZodBigInt || s instanceof z.ZodDate || s instanceof z.ZodEnum || s instanceof z.ZodLiteral || s instanceof z.ZodNull || s instanceof z.ZodUndefined || s instanceof z.ZodVoid || s instanceof z.ZodNaN || s instanceof z.ZodSymbol;
|
|
@@ -185,14 +351,71 @@ var schemaHasPath = (schema, path) => {
|
|
|
185
351
|
};
|
|
186
352
|
return walk(schema, parsePath(path), SCHEMA_WALK_BUDGET);
|
|
187
353
|
};
|
|
354
|
+
var schemaAllowsKindChangeAt = (schema, path) => {
|
|
355
|
+
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;
|
|
356
|
+
const strip = (s) => {
|
|
357
|
+
if (s instanceof z.ZodOptional || s instanceof z.ZodNullable) {
|
|
358
|
+
return strip(s.unwrap());
|
|
359
|
+
}
|
|
360
|
+
if (s instanceof z.ZodDefault || s instanceof z.ZodPrefault || s instanceof z.ZodReadonly || s instanceof z.ZodNonOptional) {
|
|
361
|
+
return strip(s.def.innerType);
|
|
362
|
+
}
|
|
363
|
+
return s;
|
|
364
|
+
};
|
|
365
|
+
const walk = (s, segments, budget) => {
|
|
366
|
+
if (budget <= 0) return true;
|
|
367
|
+
const node = strip(s);
|
|
368
|
+
if (flexible(node)) return true;
|
|
369
|
+
const [head, ...rest] = segments;
|
|
370
|
+
if (head === void 0) return false;
|
|
371
|
+
if (node instanceof z.ZodObject) {
|
|
372
|
+
const shape = node.shape;
|
|
373
|
+
const child = Object.hasOwn(shape, String(head)) ? shape[String(head)] : void 0;
|
|
374
|
+
return child === void 0 ? true : walk(child, rest, budget - 1);
|
|
375
|
+
}
|
|
376
|
+
if (node instanceof z.ZodArray) {
|
|
377
|
+
return typeof head === "number" ? walk(node.element, rest, budget - 1) : true;
|
|
378
|
+
}
|
|
379
|
+
if (node instanceof z.ZodTuple && typeof head === "number") {
|
|
380
|
+
const item = node.def.items[head];
|
|
381
|
+
return item === void 0 ? true : walk(item, rest, budget - 1);
|
|
382
|
+
}
|
|
383
|
+
if (node instanceof z.ZodRecord) {
|
|
384
|
+
return walk(node.valueType, rest, budget - 1);
|
|
385
|
+
}
|
|
386
|
+
return true;
|
|
387
|
+
};
|
|
388
|
+
return walk(schema, parsePath(path), SCHEMA_WALK_BUDGET);
|
|
389
|
+
};
|
|
188
390
|
var hasChecks = (s) => {
|
|
189
391
|
const checks = s.def.checks;
|
|
190
392
|
return checks !== void 0 && checks.length > 0;
|
|
191
393
|
};
|
|
192
|
-
var
|
|
193
|
-
const
|
|
394
|
+
var optionClaims = (option, discriminator, tag) => {
|
|
395
|
+
const strip = (s) => {
|
|
396
|
+
const inner = unwrapForWalk(s);
|
|
397
|
+
return inner === null ? s : strip(inner);
|
|
398
|
+
};
|
|
399
|
+
const node = strip(option);
|
|
400
|
+
if (!(node instanceof z.ZodObject)) return false;
|
|
401
|
+
const shape = node.shape;
|
|
402
|
+
const at = Object.hasOwn(shape, discriminator) ? shape[discriminator] : void 0;
|
|
403
|
+
if (at === void 0) return false;
|
|
404
|
+
const leaf = strip(at);
|
|
405
|
+
if (leaf instanceof z.ZodLiteral) {
|
|
406
|
+
return leaf.def.values.includes(tag);
|
|
407
|
+
}
|
|
408
|
+
if (leaf instanceof z.ZodEnum) {
|
|
409
|
+
return Object.values(leaf.def.entries).includes(
|
|
410
|
+
tag
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
return false;
|
|
414
|
+
};
|
|
415
|
+
var fieldSchemaForIntrospection = (schema, path, values) => {
|
|
416
|
+
const childOf = (s, segment, holder) => {
|
|
194
417
|
const inner = unwrapForWalk(s);
|
|
195
|
-
if (inner !== null) return childOf(inner, segment);
|
|
418
|
+
if (inner !== null) return childOf(inner, segment, holder);
|
|
196
419
|
if (s instanceof z.ZodObject && typeof segment === "string") {
|
|
197
420
|
const shape = s.shape;
|
|
198
421
|
return Object.hasOwn(shape, segment) ? shape[segment] ?? null : null;
|
|
@@ -206,19 +429,45 @@ var fieldSchemaForIntrospection = (schema, path) => {
|
|
|
206
429
|
if (s instanceof z.ZodRecord) {
|
|
207
430
|
return s.valueType;
|
|
208
431
|
}
|
|
432
|
+
if (s instanceof z.ZodIntersection) {
|
|
433
|
+
return childOf(s.def.left, segment, holder) ?? childOf(s.def.right, segment, holder);
|
|
434
|
+
}
|
|
209
435
|
if (s instanceof z.ZodDiscriminatedUnion || s instanceof z.ZodUnion) {
|
|
210
436
|
const options = s.def.options;
|
|
211
|
-
|
|
212
|
-
|
|
437
|
+
const discriminator = s instanceof z.ZodDiscriminatedUnion ? s.def.discriminator : null;
|
|
438
|
+
const tag = discriminator !== null && typeof holder === "object" && holder !== null ? holder[discriminator] : void 0;
|
|
439
|
+
const live = discriminator === null || tag === void 0 ? void 0 : options.find((option) => optionClaims(option, discriminator, tag));
|
|
440
|
+
const candidates = live === void 0 ? options : [live];
|
|
441
|
+
return candidates.reduce(
|
|
442
|
+
(found, option) => found ?? childOf(option, segment, holder),
|
|
213
443
|
null
|
|
214
444
|
);
|
|
215
445
|
}
|
|
216
446
|
return null;
|
|
217
447
|
};
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
448
|
+
const segments = parsePath(path);
|
|
449
|
+
return segments.reduce(
|
|
450
|
+
(acc, segment) => acc.s === null ? acc : {
|
|
451
|
+
s: childOf(
|
|
452
|
+
acc.s,
|
|
453
|
+
segment,
|
|
454
|
+
values === void 0 ? void 0 : getAtPath(values, acc.prefix)
|
|
455
|
+
),
|
|
456
|
+
prefix: acc.prefix === "" ? String(segment) : `${acc.prefix}.${String(segment)}`
|
|
457
|
+
},
|
|
458
|
+
{ s: schema, prefix: "" }
|
|
459
|
+
).s;
|
|
460
|
+
};
|
|
461
|
+
var schemaAcceptsUndefined = (schema) => schema instanceof z.ZodOptional || schema instanceof z.ZodDefault || schema instanceof z.ZodPrefault || schema instanceof z.ZodNullable && schemaAcceptsUndefined(schema.unwrap());
|
|
462
|
+
var isObjectSchemaAt = (schema, path) => {
|
|
463
|
+
const sub = fieldSchemaForIntrospection(schema, path);
|
|
464
|
+
if (sub === null) return true;
|
|
465
|
+
const strip = (s) => {
|
|
466
|
+
const inner = unwrapForWalk(s);
|
|
467
|
+
return inner === null ? s : strip(inner);
|
|
468
|
+
};
|
|
469
|
+
const node = strip(sub);
|
|
470
|
+
return !(node instanceof z.ZodUnion || node instanceof z.ZodDiscriminatedUnion || node instanceof z.ZodRecord || node instanceof z.ZodMap || node instanceof z.ZodSet || node instanceof z.ZodArray || node instanceof z.ZodTuple || isLeafSchema(node));
|
|
222
471
|
};
|
|
223
472
|
var containerKindAt = (schema) => (prefix) => {
|
|
224
473
|
const unwrapAll = (s) => {
|
|
@@ -269,7 +518,7 @@ var fieldSchemaAtPath = (schema, path) => parsePath(path).reduce((current, segme
|
|
|
269
518
|
}, schema);
|
|
270
519
|
|
|
271
520
|
// 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;
|
|
521
|
+
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
522
|
var emptyErrors = {};
|
|
274
523
|
var emptyBools = {};
|
|
275
524
|
var identityMapper = (n) => n;
|
|
@@ -327,7 +576,27 @@ var reuseErrorRefs = (next, prev) => {
|
|
|
327
576
|
const sameShape = entries.length === Object.keys(prev).length && entries.every(([k, v]) => entryAt(prev, k) === v);
|
|
328
577
|
return sameShape ? prev : Object.fromEntries(entries);
|
|
329
578
|
};
|
|
330
|
-
var
|
|
579
|
+
var noScopeErrors = { scoped: emptyErrors, full: null };
|
|
580
|
+
var scopeParse = (result, path, viaSubschema) => {
|
|
581
|
+
const errors = result.kind === "invalid" ? result.errors : emptyErrors;
|
|
582
|
+
return viaSubschema ? { scoped: prefixErrorKeys(errors, path), full: null } : {
|
|
583
|
+
scoped: path === "" ? errors : pickScope(errors, [path]),
|
|
584
|
+
full: errors
|
|
585
|
+
};
|
|
586
|
+
};
|
|
587
|
+
var pruneUnreported = (prev, full) => {
|
|
588
|
+
const entries = Object.entries(prev);
|
|
589
|
+
const kept = entries.filter(([k]) => Object.hasOwn(full, k));
|
|
590
|
+
const unchanged = kept.length === entries.length && kept.every(([k, list]) => full[k] === list);
|
|
591
|
+
return unchanged ? prev : Object.fromEntries(kept.map(([k, list]) => [k, full[k] ?? list]));
|
|
592
|
+
};
|
|
593
|
+
var dropEmptyLists = (map) => {
|
|
594
|
+
const entries = Object.entries(map);
|
|
595
|
+
const kept = entries.filter(
|
|
596
|
+
([, list]) => list !== void 0 && list.length > 0
|
|
597
|
+
);
|
|
598
|
+
return kept.length === entries.length ? map : Object.fromEntries(kept);
|
|
599
|
+
};
|
|
331
600
|
var mergeErrorChannels = (schema, server) => {
|
|
332
601
|
const extra = Object.entries(server).filter(
|
|
333
602
|
([k]) => !Object.hasOwn(schema, k)
|
|
@@ -437,17 +706,17 @@ var createForm = (schema, options) => {
|
|
|
437
706
|
const merged = scopes.reduce(
|
|
438
707
|
(acc, { path, scope }) => scope.kind === "skip" ? acc : {
|
|
439
708
|
...acc,
|
|
440
|
-
...
|
|
709
|
+
...scopeParse(
|
|
441
710
|
validateSyncLatched(path, scope.schema, scope.value),
|
|
442
711
|
path,
|
|
443
712
|
true
|
|
444
|
-
)
|
|
713
|
+
).scoped
|
|
445
714
|
},
|
|
446
715
|
{}
|
|
447
716
|
);
|
|
448
717
|
store.setState((state) => ({
|
|
449
718
|
...state,
|
|
450
|
-
...commitScopedErrors(state, merged, paths)
|
|
719
|
+
...commitScopedErrors(state, merged, paths, null)
|
|
451
720
|
}));
|
|
452
721
|
return settledFields(merged);
|
|
453
722
|
}
|
|
@@ -456,7 +725,7 @@ var createForm = (schema, options) => {
|
|
|
456
725
|
const requestedErrors = pickScope(fullErrors, paths);
|
|
457
726
|
store.setState((state) => ({
|
|
458
727
|
...state,
|
|
459
|
-
...commitScopedErrors(state, requestedErrors, paths)
|
|
728
|
+
...commitScopedErrors(state, requestedErrors, paths, fullErrors)
|
|
460
729
|
}));
|
|
461
730
|
return settledFields(requestedErrors);
|
|
462
731
|
} catch (e) {
|
|
@@ -501,7 +770,7 @@ var createForm = (schema, options) => {
|
|
|
501
770
|
const slot = slotAtPath(values, path);
|
|
502
771
|
return slot.exists ? { kind: "parse", schema: sub, value: slot.value, viaSubschema: true } : { kind: "skip" };
|
|
503
772
|
};
|
|
504
|
-
const parseScopeAsync = async (path, scope) => scope.kind === "skip" ?
|
|
773
|
+
const parseScopeAsync = async (path, scope) => scope.kind === "skip" ? noScopeErrors : scopeParse(
|
|
505
774
|
await validateAsync(scope.schema, scope.value),
|
|
506
775
|
path,
|
|
507
776
|
scope.viaSubschema
|
|
@@ -521,14 +790,18 @@ var createForm = (schema, options) => {
|
|
|
521
790
|
};
|
|
522
791
|
const settledFields = (scoped) => Object.keys(scoped).length === 0 ? { kind: "valid" } : { kind: "invalid", errors: scoped };
|
|
523
792
|
const settledFormResult = (result) => settledFields(result.kind === "invalid" ? result.errors : emptyErrors);
|
|
524
|
-
const commitScopedErrors = (state, scoped, paths) => errorChannels(state, {
|
|
793
|
+
const commitScopedErrors = (state, scoped, paths, full) => errorChannels(state, {
|
|
525
794
|
schemaErrors: reuseErrorRefs(
|
|
526
|
-
mergeScopedErrors(
|
|
795
|
+
mergeScopedErrors(
|
|
796
|
+
full === null ? state.schemaErrors : pruneUnreported(state.schemaErrors, full),
|
|
797
|
+
scoped,
|
|
798
|
+
paths
|
|
799
|
+
),
|
|
527
800
|
state.schemaErrors
|
|
528
801
|
),
|
|
529
802
|
serverErrors: omitScope(state.serverErrors, paths)
|
|
530
803
|
});
|
|
531
|
-
const commitFieldErrors = (state,
|
|
804
|
+
const commitFieldErrors = (state, parse, path) => path === "" ? commitFullPass(state, parse.scoped) : commitScopedErrors(state, parse.scoped, [path], parse.full);
|
|
532
805
|
const validateField = (path) => {
|
|
533
806
|
warnMissingSchemaPath("validateField", path);
|
|
534
807
|
const scope = fieldScopeFor(path, store.getState().values);
|
|
@@ -536,7 +809,7 @@ var createForm = (schema, options) => {
|
|
|
536
809
|
return { kind: "pending", promise: validateFieldAsync(path) };
|
|
537
810
|
}
|
|
538
811
|
try {
|
|
539
|
-
const
|
|
812
|
+
const parse = scope.kind === "skip" ? noScopeErrors : scopeParse(
|
|
540
813
|
validateSyncLatched(
|
|
541
814
|
scope.viaSubschema ? path : "",
|
|
542
815
|
scope.schema,
|
|
@@ -547,9 +820,9 @@ var createForm = (schema, options) => {
|
|
|
547
820
|
);
|
|
548
821
|
store.setState((state) => ({
|
|
549
822
|
...state,
|
|
550
|
-
...commitFieldErrors(state,
|
|
823
|
+
...commitFieldErrors(state, parse, path)
|
|
551
824
|
}));
|
|
552
|
-
return fieldResult(scoped);
|
|
825
|
+
return fieldResult(parse.scoped);
|
|
553
826
|
} catch (e) {
|
|
554
827
|
if (isAsyncRequiredError(e)) {
|
|
555
828
|
return { kind: "pending", promise: validateFieldAsync(path) };
|
|
@@ -559,11 +832,15 @@ var createForm = (schema, options) => {
|
|
|
559
832
|
};
|
|
560
833
|
const snapshotField = (state, path) => {
|
|
561
834
|
const value = getAtPath(state.values, path);
|
|
835
|
+
const initialValue = getAtPath(state.initialValues, path);
|
|
836
|
+
const error = entryAt(state.errors, path);
|
|
562
837
|
return {
|
|
563
838
|
value,
|
|
564
|
-
|
|
839
|
+
initialValue,
|
|
840
|
+
error,
|
|
841
|
+
firstError: error?.[0],
|
|
565
842
|
touched: flagAt(state.touched, path),
|
|
566
|
-
dirty: isFieldDirty(value,
|
|
843
|
+
dirty: isFieldDirty(value, initialValue),
|
|
567
844
|
isValidating: flagAt(state.isValidating, path)
|
|
568
845
|
};
|
|
569
846
|
};
|
|
@@ -577,7 +854,14 @@ var createForm = (schema, options) => {
|
|
|
577
854
|
const pass = claimPass(FORM_SEQ_KEY);
|
|
578
855
|
const valuesAtStart = store.getState().values;
|
|
579
856
|
store.setState((state) => ({ ...state, isValidatingForm: true }));
|
|
580
|
-
const result = await validateAsync(schema, valuesAtStart)
|
|
857
|
+
const result = await validateAsync(schema, valuesAtStart).catch(
|
|
858
|
+
(error) => {
|
|
859
|
+
if (sequences.get(FORM_SEQ_KEY) === pass) {
|
|
860
|
+
store.setState((state) => ({ ...state, isValidatingForm: false }));
|
|
861
|
+
}
|
|
862
|
+
throw error;
|
|
863
|
+
}
|
|
864
|
+
);
|
|
581
865
|
const live = store.getState();
|
|
582
866
|
const stillOwns = sequences.get(FORM_SEQ_KEY) === pass;
|
|
583
867
|
if (!stillOwns || live.values !== valuesAtStart) {
|
|
@@ -617,12 +901,15 @@ var createForm = (schema, options) => {
|
|
|
617
901
|
...Object.fromEntries(paths.map((p) => [p, true]))
|
|
618
902
|
}
|
|
619
903
|
}));
|
|
620
|
-
const
|
|
904
|
+
const parseBatch = async () => allViaSubschema ? {
|
|
621
905
|
kind: "slots",
|
|
622
906
|
byPath: new Map(
|
|
623
907
|
await Promise.all(
|
|
624
908
|
scopes.map(
|
|
625
|
-
async ({ path, scope }) => [
|
|
909
|
+
async ({ path, scope }) => [
|
|
910
|
+
path,
|
|
911
|
+
(await parseScopeAsync(path, scope)).scoped
|
|
912
|
+
]
|
|
626
913
|
)
|
|
627
914
|
)
|
|
628
915
|
)
|
|
@@ -632,6 +919,18 @@ var createForm = (schema, options) => {
|
|
|
632
919
|
(result) => result.kind === "invalid" ? result.errors : emptyErrors
|
|
633
920
|
)
|
|
634
921
|
};
|
|
922
|
+
const parsed = await parseBatch().catch((error) => {
|
|
923
|
+
const stillOwned = new Set(
|
|
924
|
+
paths.filter((p) => sequences.get(p) === passes.get(p))
|
|
925
|
+
);
|
|
926
|
+
if (stillOwned.size > 0) {
|
|
927
|
+
store.setState((state) => ({
|
|
928
|
+
...state,
|
|
929
|
+
isValidating: omitKeys(state.isValidating, stillOwned)
|
|
930
|
+
}));
|
|
931
|
+
}
|
|
932
|
+
throw error;
|
|
933
|
+
});
|
|
635
934
|
const errorsFor = (path) => {
|
|
636
935
|
switch (parsed.kind) {
|
|
637
936
|
case "slots":
|
|
@@ -660,7 +959,15 @@ var createForm = (schema, options) => {
|
|
|
660
959
|
Object.fromEntries(
|
|
661
960
|
fresh.flatMap((path) => Object.entries(errorsFor(path)))
|
|
662
961
|
),
|
|
663
|
-
fresh
|
|
962
|
+
fresh,
|
|
963
|
+
// The prune map describes the values the batch STARTED with.
|
|
964
|
+
// A skip scope (an index into an empty array) still reads as
|
|
965
|
+
// fresh after an unrelated change, so a batch can commit under
|
|
966
|
+
// changed values; pruning with the stale map then dropped
|
|
967
|
+
// errors committed elsewhere in flight (an email error landing
|
|
968
|
+
// while a wizard step with such a path validated). The prune
|
|
969
|
+
// rides only while the whole values reference held.
|
|
970
|
+
parsed.kind === "full" && live.values === valuesAtStart ? parsed.errors : null
|
|
664
971
|
),
|
|
665
972
|
isValidating: omitKeys(state.isValidating, ownedKeys)
|
|
666
973
|
}));
|
|
@@ -681,24 +988,32 @@ var createForm = (schema, options) => {
|
|
|
681
988
|
...state,
|
|
682
989
|
isValidating: { ...state.isValidating, [path]: true }
|
|
683
990
|
}));
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
const stillOwns = sequences.get(path) === pass;
|
|
687
|
-
if (!stillOwns || scopeStale(scope, path, valuesAtStart, live.values)) {
|
|
688
|
-
if (stillOwns) {
|
|
991
|
+
const releaseOwnFlag = () => {
|
|
992
|
+
if (sequences.get(path) === pass) {
|
|
689
993
|
store.setState((state) => ({
|
|
690
994
|
...state,
|
|
691
995
|
isValidating: omitKey(state.isValidating, path)
|
|
692
996
|
}));
|
|
693
997
|
}
|
|
694
|
-
|
|
998
|
+
};
|
|
999
|
+
const parse = await parseScopeAsync(path, scope).catch(
|
|
1000
|
+
(error) => {
|
|
1001
|
+
releaseOwnFlag();
|
|
1002
|
+
throw error;
|
|
1003
|
+
}
|
|
1004
|
+
);
|
|
1005
|
+
const live = store.getState();
|
|
1006
|
+
const stillOwns = sequences.get(path) === pass;
|
|
1007
|
+
if (!stillOwns || scopeStale(scope, path, valuesAtStart, live.values)) {
|
|
1008
|
+
releaseOwnFlag();
|
|
1009
|
+
return fieldResult(parse.scoped);
|
|
695
1010
|
}
|
|
696
1011
|
store.setState((state) => ({
|
|
697
1012
|
...state,
|
|
698
|
-
...commitFieldErrors(state,
|
|
1013
|
+
...commitFieldErrors(state, parse, path),
|
|
699
1014
|
isValidating: omitKey(state.isValidating, path)
|
|
700
1015
|
}));
|
|
701
|
-
return fieldResult(scoped);
|
|
1016
|
+
return fieldResult(parse.scoped);
|
|
702
1017
|
};
|
|
703
1018
|
const applyArrayOp = (path, nextArray, mapper, indexInBounds) => {
|
|
704
1019
|
const current = getAtPath(store.getState().values, path);
|
|
@@ -757,7 +1072,9 @@ var createForm = (schema, options) => {
|
|
|
757
1072
|
count: 0,
|
|
758
1073
|
baseline: false
|
|
759
1074
|
};
|
|
760
|
-
const submit = async (onValid,
|
|
1075
|
+
const submit = async (onValid, onInvalidOrOptions, positionalOptions) => {
|
|
1076
|
+
const submitOptions = typeof onInvalidOrOptions === "object" ? onInvalidOrOptions : positionalOptions;
|
|
1077
|
+
const onInvalid = typeof onInvalidOrOptions === "function" ? onInvalidOrOptions : submitOptions?.onInvalid;
|
|
761
1078
|
if (inFlight.count > 0 && submitOptions?.force !== true) {
|
|
762
1079
|
return { kind: "skipped" };
|
|
763
1080
|
}
|
|
@@ -798,6 +1115,7 @@ var createForm = (schema, options) => {
|
|
|
798
1115
|
try {
|
|
799
1116
|
onInvalid?.(merged);
|
|
800
1117
|
} catch (error) {
|
|
1118
|
+
submitOptions?.onError?.(error);
|
|
801
1119
|
return { kind: "error", error };
|
|
802
1120
|
}
|
|
803
1121
|
return { kind: "invalid", errors: merged };
|
|
@@ -823,9 +1141,14 @@ var createForm = (schema, options) => {
|
|
|
823
1141
|
}
|
|
824
1142
|
}
|
|
825
1143
|
};
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1144
|
+
const judgeBaseline = () => {
|
|
1145
|
+
if (options.validateOnMount !== true) return;
|
|
1146
|
+
const result = validate();
|
|
1147
|
+
if (result.kind === "pending") {
|
|
1148
|
+
void result.promise.catch(reportBackgroundFailure);
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
judgeBaseline();
|
|
829
1152
|
return Object.freeze({
|
|
830
1153
|
schema,
|
|
831
1154
|
store,
|
|
@@ -840,8 +1163,12 @@ var createForm = (schema, options) => {
|
|
|
840
1163
|
return store.subscribe((state) => {
|
|
841
1164
|
const next = snapshotField(state, path);
|
|
842
1165
|
if (!shallowFieldEqual(ref.current, next)) {
|
|
1166
|
+
const prev = ref.current;
|
|
843
1167
|
ref.current = next;
|
|
844
|
-
listener(
|
|
1168
|
+
listener(
|
|
1169
|
+
next,
|
|
1170
|
+
prev
|
|
1171
|
+
);
|
|
845
1172
|
}
|
|
846
1173
|
});
|
|
847
1174
|
},
|
|
@@ -876,10 +1203,12 @@ var createForm = (schema, options) => {
|
|
|
876
1203
|
if (Object.is(getAtPath(currentValues, path), value) && (value !== void 0 || hasOwnSlot(currentValues, path))) {
|
|
877
1204
|
return;
|
|
878
1205
|
}
|
|
1206
|
+
const next = setAtPath(currentValues, path, value, kindAt);
|
|
1207
|
+
if (next === currentValues) return;
|
|
879
1208
|
clearArrayOpsUnder(store, path);
|
|
880
1209
|
store.setState((state) => ({
|
|
881
1210
|
...state,
|
|
882
|
-
values:
|
|
1211
|
+
values: next,
|
|
883
1212
|
...errorChannels(state, {
|
|
884
1213
|
serverErrors: releaseSpine(state.serverErrors, path)
|
|
885
1214
|
})
|
|
@@ -917,7 +1246,7 @@ var createForm = (schema, options) => {
|
|
|
917
1246
|
},
|
|
918
1247
|
setErrors: (errors) => store.setState((state) => ({
|
|
919
1248
|
...state,
|
|
920
|
-
...errorChannels(state, { serverErrors: errors })
|
|
1249
|
+
...errorChannels(state, { serverErrors: dropEmptyLists(errors) })
|
|
921
1250
|
})),
|
|
922
1251
|
addErrors: (errors) => store.setState((state) => {
|
|
923
1252
|
const entries = Object.entries(errors);
|
|
@@ -960,11 +1289,11 @@ var createForm = (schema, options) => {
|
|
|
960
1289
|
if (next.values !== state.values) {
|
|
961
1290
|
clearArrayOpsUnder(store, "");
|
|
962
1291
|
}
|
|
963
|
-
const serverErrors = patch.serverErrors !== void 0 ? patch.serverErrors : next.values !== state.values ? releaseChangedSlices(state.serverErrors, state.values, next.values) : state.serverErrors;
|
|
1292
|
+
const serverErrors = patch.serverErrors !== void 0 ? dropEmptyLists(patch.serverErrors) : next.values !== state.values ? releaseChangedSlices(state.serverErrors, state.values, next.values) : state.serverErrors;
|
|
964
1293
|
store.setState(() => ({
|
|
965
1294
|
...next,
|
|
966
1295
|
...errorChannels(state, {
|
|
967
|
-
schemaErrors:
|
|
1296
|
+
schemaErrors: patch.schemaErrors === void 0 ? state.schemaErrors : dropEmptyLists(patch.schemaErrors),
|
|
968
1297
|
serverErrors
|
|
969
1298
|
})
|
|
970
1299
|
}));
|
|
@@ -986,6 +1315,7 @@ var createForm = (schema, options) => {
|
|
|
986
1315
|
isValidating: emptyBools,
|
|
987
1316
|
isValidatingForm: false
|
|
988
1317
|
}));
|
|
1318
|
+
judgeBaseline();
|
|
989
1319
|
},
|
|
990
1320
|
reset: (nextInitial, resetOptions) => {
|
|
991
1321
|
sequences.clear();
|
|
@@ -1010,6 +1340,7 @@ var createForm = (schema, options) => {
|
|
|
1010
1340
|
isValidatingForm: false
|
|
1011
1341
|
};
|
|
1012
1342
|
});
|
|
1343
|
+
if (resetOptions?.keepErrors !== true) judgeBaseline();
|
|
1013
1344
|
},
|
|
1014
1345
|
resetField: (path) => {
|
|
1015
1346
|
const initialSlot = slotAtPath(
|
|
@@ -1073,9 +1404,14 @@ var createForm = (schema, options) => {
|
|
|
1073
1404
|
swapIndices(a, b),
|
|
1074
1405
|
(len) => Number.isInteger(a) && Number.isInteger(b) && a >= 0 && a < len && b >= 0 && b < len
|
|
1075
1406
|
),
|
|
1076
|
-
handleSubmit: (onValid,
|
|
1407
|
+
handleSubmit: (onValid, onInvalidOrOptions, positionalOptions) => async (event) => {
|
|
1077
1408
|
event?.preventDefault();
|
|
1078
|
-
const
|
|
1409
|
+
const opts = typeof onInvalidOrOptions === "object" ? onInvalidOrOptions : positionalOptions;
|
|
1410
|
+
const result = await submit(
|
|
1411
|
+
onValid,
|
|
1412
|
+
onInvalidOrOptions,
|
|
1413
|
+
positionalOptions
|
|
1414
|
+
);
|
|
1079
1415
|
if (result.kind === "error" && opts?.onError === void 0 && isDevBuild()) {
|
|
1080
1416
|
console.warn(
|
|
1081
1417
|
"[formstand] submit handler threw and nothing observed it \u2014 the form shows no feedback. Pass { onError } to handleSubmit (or setError inside your handler):",
|
|
@@ -1099,10 +1435,11 @@ var createForm = (schema, options) => {
|
|
|
1099
1435
|
},
|
|
1100
1436
|
snapshot: () => store.getState(),
|
|
1101
1437
|
restore: (snap) => {
|
|
1438
|
+
sequences.clear();
|
|
1102
1439
|
clearArrayOpsUnder(store, "");
|
|
1103
1440
|
store.setState(() => {
|
|
1104
|
-
const schemaErrors = snap.schemaErrors ?? emptyErrors;
|
|
1105
|
-
const serverErrors = snap.serverErrors ?? emptyErrors;
|
|
1441
|
+
const schemaErrors = dropEmptyLists(snap.schemaErrors ?? emptyErrors);
|
|
1442
|
+
const serverErrors = dropEmptyLists(snap.serverErrors ?? emptyErrors);
|
|
1106
1443
|
return {
|
|
1107
1444
|
...snap,
|
|
1108
1445
|
schemaErrors,
|
|
@@ -1132,11 +1469,13 @@ var createDebouncer = (ms) => {
|
|
|
1132
1469
|
const timer = {
|
|
1133
1470
|
current: null
|
|
1134
1471
|
};
|
|
1472
|
+
const scheduled = { current: null };
|
|
1135
1473
|
const cancel = () => {
|
|
1136
1474
|
if (timer.current !== null) {
|
|
1137
1475
|
clearTimeout(timer.current);
|
|
1138
1476
|
timer.current = null;
|
|
1139
1477
|
}
|
|
1478
|
+
scheduled.current = null;
|
|
1140
1479
|
};
|
|
1141
1480
|
const schedule = (run) => {
|
|
1142
1481
|
cancel();
|
|
@@ -1144,37 +1483,50 @@ var createDebouncer = (ms) => {
|
|
|
1144
1483
|
run();
|
|
1145
1484
|
return;
|
|
1146
1485
|
}
|
|
1486
|
+
scheduled.current = run;
|
|
1147
1487
|
timer.current = setTimeout(() => {
|
|
1148
1488
|
timer.current = null;
|
|
1489
|
+
scheduled.current = null;
|
|
1149
1490
|
run();
|
|
1150
1491
|
}, ms);
|
|
1151
1492
|
};
|
|
1493
|
+
const flush = () => {
|
|
1494
|
+
const run = scheduled.current;
|
|
1495
|
+
if (run === null) return;
|
|
1496
|
+
cancel();
|
|
1497
|
+
run();
|
|
1498
|
+
};
|
|
1152
1499
|
return Object.freeze({
|
|
1153
1500
|
schedule,
|
|
1154
1501
|
cancel,
|
|
1502
|
+
flush,
|
|
1155
1503
|
pending: () => timer.current !== null
|
|
1156
1504
|
});
|
|
1157
1505
|
};
|
|
1158
1506
|
|
|
1159
1507
|
// src/core/persist.ts
|
|
1160
1508
|
var kindOf = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
|
|
1161
|
-
var
|
|
1162
|
-
|
|
1163
|
-
if (
|
|
1509
|
+
var joinPath = (base, key) => base === "" ? String(key) : `${base}.${String(key)}`;
|
|
1510
|
+
var conflictPath = (draft, reference, path, flexibleAt) => {
|
|
1511
|
+
if (draft === void 0 || draft === null) return null;
|
|
1512
|
+
if (reference === void 0 || reference === null) return null;
|
|
1164
1513
|
const draftKind = kindOf(draft);
|
|
1165
1514
|
const referenceKind = kindOf(reference);
|
|
1166
|
-
if (draftKind !== referenceKind) return
|
|
1515
|
+
if (draftKind !== referenceKind) return flexibleAt(path) ? null : path;
|
|
1167
1516
|
if (draftKind === "array") {
|
|
1168
1517
|
const [d] = draft;
|
|
1169
1518
|
const [r] = reference;
|
|
1170
|
-
return
|
|
1519
|
+
return conflictPath(d, r, joinPath(path, 0), flexibleAt);
|
|
1171
1520
|
}
|
|
1172
1521
|
if (draftKind === "object") {
|
|
1173
1522
|
const d = draft;
|
|
1174
1523
|
const r = reference;
|
|
1175
|
-
return Object.keys(d).
|
|
1524
|
+
return Object.keys(d).reduce(
|
|
1525
|
+
(found, key) => found ?? conflictPath(d[key], r[key], joinPath(path, key), flexibleAt),
|
|
1526
|
+
null
|
|
1527
|
+
);
|
|
1176
1528
|
}
|
|
1177
|
-
return
|
|
1529
|
+
return null;
|
|
1178
1530
|
};
|
|
1179
1531
|
var defaultStorage = () => {
|
|
1180
1532
|
try {
|
|
@@ -1187,7 +1539,49 @@ var persistForm = (form, options) => {
|
|
|
1187
1539
|
const storage = options.storage ?? defaultStorage();
|
|
1188
1540
|
const debounceMs = options.debounceMs ?? 300;
|
|
1189
1541
|
const apply = options.apply ?? "adopt";
|
|
1190
|
-
const
|
|
1542
|
+
const schema = form.schema;
|
|
1543
|
+
const fillRequiredGaps = (draft, initial, path) => {
|
|
1544
|
+
if (!isPlainObject(draft) || !isPlainObject(initial)) return draft;
|
|
1545
|
+
if (!isObjectSchemaAt(schema, path)) return draft;
|
|
1546
|
+
return Object.keys(initial).reduce(
|
|
1547
|
+
(acc, key) => {
|
|
1548
|
+
const childPath = joinPath(path, key);
|
|
1549
|
+
if (Object.hasOwn(draft, key)) {
|
|
1550
|
+
const next = fillRequiredGaps(draft[key], initial[key], childPath);
|
|
1551
|
+
return Object.is(next, draft[key]) ? acc : { ...acc, [key]: next };
|
|
1552
|
+
}
|
|
1553
|
+
if (initial[key] === void 0) return acc;
|
|
1554
|
+
const sub = fieldSchemaForIntrospection(schema, childPath);
|
|
1555
|
+
return sub !== null && schemaAcceptsUndefined(sub) ? acc : { ...acc, [key]: initial[key] };
|
|
1556
|
+
},
|
|
1557
|
+
draft
|
|
1558
|
+
);
|
|
1559
|
+
};
|
|
1560
|
+
const applying = { current: false };
|
|
1561
|
+
const applyDraft = (values) => {
|
|
1562
|
+
const filled = fillRequiredGaps(
|
|
1563
|
+
values,
|
|
1564
|
+
form.getState().initialValues,
|
|
1565
|
+
""
|
|
1566
|
+
);
|
|
1567
|
+
applying.current = true;
|
|
1568
|
+
try {
|
|
1569
|
+
if (apply === "edit" || apply === "restore") {
|
|
1570
|
+
form.setValues(filled);
|
|
1571
|
+
} else {
|
|
1572
|
+
form.adoptValues(filled);
|
|
1573
|
+
}
|
|
1574
|
+
} finally {
|
|
1575
|
+
applying.current = false;
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1578
|
+
const isCurrent = (values) => {
|
|
1579
|
+
try {
|
|
1580
|
+
return JSON.stringify(values) === JSON.stringify(form.getState().values);
|
|
1581
|
+
} catch {
|
|
1582
|
+
return false;
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1191
1585
|
const referenceShape = () => {
|
|
1192
1586
|
try {
|
|
1193
1587
|
return JSON.parse(JSON.stringify(form.getState().initialValues));
|
|
@@ -1195,31 +1589,70 @@ var persistForm = (form, options) => {
|
|
|
1195
1589
|
return void 0;
|
|
1196
1590
|
}
|
|
1197
1591
|
};
|
|
1198
|
-
const
|
|
1592
|
+
const flexibleAt = (path) => schemaAllowsKindChangeAt(schema, path);
|
|
1593
|
+
const inspect = (skipWhenCurrent) => {
|
|
1594
|
+
const raw = (() => {
|
|
1595
|
+
try {
|
|
1596
|
+
return storage === null ? null : storage.getItem(options.key);
|
|
1597
|
+
} catch {
|
|
1598
|
+
return null;
|
|
1599
|
+
}
|
|
1600
|
+
})();
|
|
1601
|
+
if (raw === null) return { kind: "absent" };
|
|
1199
1602
|
try {
|
|
1200
|
-
const raw = storage === null ? null : storage.getItem(options.key);
|
|
1201
|
-
if (raw === null) return false;
|
|
1202
1603
|
const parsed = JSON.parse(raw);
|
|
1203
1604
|
const versioned = options.version !== void 0 && typeof parsed === "object" && parsed !== null && "__v" in parsed;
|
|
1204
|
-
if (options.version !== void 0 && !versioned)
|
|
1605
|
+
if (options.version !== void 0 && !versioned) {
|
|
1606
|
+
return {
|
|
1607
|
+
kind: "ignored",
|
|
1608
|
+
reason: `it carries no version and this configuration expects version ${String(options.version)}`
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1205
1611
|
if (options.version === void 0 && typeof parsed === "object" && parsed !== null && Object.hasOwn(parsed, "__v") && Object.hasOwn(parsed, "values")) {
|
|
1206
|
-
return
|
|
1612
|
+
return {
|
|
1613
|
+
kind: "ignored",
|
|
1614
|
+
reason: "it was written with a version and this configuration has none"
|
|
1615
|
+
};
|
|
1207
1616
|
}
|
|
1208
1617
|
const wrapper = parsed;
|
|
1209
|
-
if (versioned && wrapper.__v !== options.version)
|
|
1210
|
-
|
|
1618
|
+
if (versioned && wrapper.__v !== options.version) {
|
|
1619
|
+
return {
|
|
1620
|
+
kind: "ignored",
|
|
1621
|
+
reason: `its version (${String(wrapper.__v)}) is not the configured version (${String(options.version)})`
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
if (versioned && wrapper.values === void 0) {
|
|
1625
|
+
return { kind: "ignored", reason: "its version wrapper holds no values" };
|
|
1626
|
+
}
|
|
1211
1627
|
const values = versioned ? wrapper.values : parsed;
|
|
1212
|
-
if (values === null || values === void 0)
|
|
1628
|
+
if (values === null || values === void 0) {
|
|
1629
|
+
return { kind: "ignored", reason: "it holds no values" };
|
|
1630
|
+
}
|
|
1213
1631
|
const reference = referenceShape();
|
|
1214
|
-
|
|
1215
|
-
|
|
1632
|
+
const conflict = reference === void 0 ? null : conflictPath(values, reference, "", flexibleAt);
|
|
1633
|
+
if (conflict !== null) {
|
|
1634
|
+
return {
|
|
1635
|
+
kind: "ignored",
|
|
1636
|
+
reason: `its shape conflicts with the form's initial values at "${conflict}" (set the version option when the schema changes)`
|
|
1637
|
+
};
|
|
1216
1638
|
}
|
|
1639
|
+
if (skipWhenCurrent && isCurrent(values)) return { kind: "applied" };
|
|
1217
1640
|
applyDraft(values);
|
|
1218
|
-
return
|
|
1641
|
+
return { kind: "applied" };
|
|
1219
1642
|
} catch {
|
|
1220
|
-
return
|
|
1643
|
+
return { kind: "ignored", reason: "it is not valid JSON" };
|
|
1221
1644
|
}
|
|
1222
1645
|
};
|
|
1646
|
+
const loadWith = (skipWhenCurrent) => {
|
|
1647
|
+
const outcome = inspect(skipWhenCurrent);
|
|
1648
|
+
if (outcome.kind === "ignored" && isDevBuild()) {
|
|
1649
|
+
console.warn(
|
|
1650
|
+
`[formstand] persistForm: ignored the draft under "${options.key}": ${outcome.reason}. The next edit overwrites it.`
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
return outcome.kind === "applied";
|
|
1654
|
+
};
|
|
1655
|
+
const load = () => loadWith(false);
|
|
1223
1656
|
const writeDraft = (values) => {
|
|
1224
1657
|
try {
|
|
1225
1658
|
storage?.setItem(
|
|
@@ -1232,24 +1665,29 @@ var persistForm = (form, options) => {
|
|
|
1232
1665
|
}
|
|
1233
1666
|
};
|
|
1234
1667
|
const writes = createDebouncer(debounceMs);
|
|
1235
|
-
const cancelPending = () => writes.cancel();
|
|
1236
1668
|
if (apply !== "manual") {
|
|
1237
|
-
|
|
1669
|
+
loadWith(true);
|
|
1238
1670
|
}
|
|
1671
|
+
const cleared = { current: false };
|
|
1239
1672
|
const unsubscribe = form.watchValues((values) => {
|
|
1673
|
+
if (applying.current) return;
|
|
1674
|
+
if (cleared.current && values === form.getState().initialValues) return;
|
|
1675
|
+
cleared.current = false;
|
|
1240
1676
|
writes.schedule(() => writeDraft(values));
|
|
1241
1677
|
});
|
|
1242
1678
|
return Object.freeze({
|
|
1243
1679
|
load,
|
|
1680
|
+
flush: () => writes.flush(),
|
|
1244
1681
|
clear: () => {
|
|
1245
|
-
|
|
1682
|
+
writes.cancel();
|
|
1683
|
+
cleared.current = true;
|
|
1246
1684
|
try {
|
|
1247
1685
|
storage?.removeItem(options.key);
|
|
1248
1686
|
} catch {
|
|
1249
1687
|
}
|
|
1250
1688
|
},
|
|
1251
1689
|
dispose: () => {
|
|
1252
|
-
|
|
1690
|
+
writes.flush();
|
|
1253
1691
|
unsubscribe();
|
|
1254
1692
|
}
|
|
1255
1693
|
});
|
|
@@ -1261,17 +1699,31 @@ var usePersistForm = (form, options) => {
|
|
|
1261
1699
|
latestOptions.current = options;
|
|
1262
1700
|
});
|
|
1263
1701
|
const key = options.key;
|
|
1702
|
+
const hasStorage = options.storage !== void 0;
|
|
1703
|
+
const storageProxy = useMemo(
|
|
1704
|
+
() => ({
|
|
1705
|
+
getItem: (k) => latestOptions.current.storage?.getItem(k) ?? null,
|
|
1706
|
+
setItem: (k, v) => latestOptions.current.storage?.setItem(k, v),
|
|
1707
|
+
removeItem: (k) => latestOptions.current.storage?.removeItem(k)
|
|
1708
|
+
}),
|
|
1709
|
+
[]
|
|
1710
|
+
);
|
|
1264
1711
|
useEffect(() => {
|
|
1265
|
-
const
|
|
1712
|
+
const options2 = latestOptions.current;
|
|
1713
|
+
const handle = persistForm(form, {
|
|
1714
|
+
...options2,
|
|
1715
|
+
...options2.storage === void 0 ? {} : { storage: storageProxy }
|
|
1716
|
+
});
|
|
1266
1717
|
inner.current = handle;
|
|
1267
1718
|
return () => {
|
|
1268
1719
|
inner.current = null;
|
|
1269
1720
|
handle.dispose();
|
|
1270
1721
|
};
|
|
1271
|
-
}, [form, key]);
|
|
1722
|
+
}, [form, key, hasStorage, storageProxy]);
|
|
1272
1723
|
return useMemo(
|
|
1273
1724
|
() => ({
|
|
1274
1725
|
load: () => inner.current?.load() ?? false,
|
|
1726
|
+
flush: () => inner.current?.flush(),
|
|
1275
1727
|
clear: () => inner.current?.clear(),
|
|
1276
1728
|
// dispose stops THIS component's watching early; the effect cleanup
|
|
1277
1729
|
// still runs its own dispose harmlessly (persistForm's dispose is
|
|
@@ -1305,9 +1757,12 @@ var useForm = (schema, options) => {
|
|
|
1305
1757
|
var hasFieldError = (error) => error !== void 0 && error.length > 0;
|
|
1306
1758
|
var ariaInvalid = (field) => hasFieldError(field.error) ? true : void 0;
|
|
1307
1759
|
var numberToInputText = (value) => value === void 0 || value === null || Number.isNaN(value) ? "" : String(value);
|
|
1760
|
+
var DECIMAL_TEXT = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
|
|
1308
1761
|
var parseNumberText = (text) => {
|
|
1309
|
-
|
|
1310
|
-
|
|
1762
|
+
const trimmed = text.trim();
|
|
1763
|
+
if (trimmed === "") return { kind: "empty" };
|
|
1764
|
+
if (!DECIMAL_TEXT.test(trimmed)) return { kind: "invalid" };
|
|
1765
|
+
const parsed = Number(trimmed);
|
|
1311
1766
|
return Number.isFinite(parsed) ? { kind: "number", value: parsed } : { kind: "invalid" };
|
|
1312
1767
|
};
|
|
1313
1768
|
var textInputProps = (field) => ({
|
|
@@ -1335,11 +1790,17 @@ var numberInputProps = (field) => ({
|
|
|
1335
1790
|
onBlur: field.onBlur
|
|
1336
1791
|
});
|
|
1337
1792
|
var dateToInputText = (value) => {
|
|
1338
|
-
if (value === void 0 || value === null
|
|
1339
|
-
|
|
1793
|
+
if (value === void 0 || value === null) return "";
|
|
1794
|
+
if (typeof value === "string") {
|
|
1795
|
+
const text = value;
|
|
1796
|
+
const parsed = parseDateText(text);
|
|
1797
|
+
if (parsed.kind === "date") return dateToInputText(parsed.value);
|
|
1798
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(text.trim())) return "";
|
|
1340
1799
|
}
|
|
1800
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
1801
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
1341
1802
|
const pad = (part) => String(part).padStart(2, "0");
|
|
1342
|
-
return `${String(
|
|
1803
|
+
return `${String(date.getFullYear()).padStart(4, "0")}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
1343
1804
|
};
|
|
1344
1805
|
var parseDateText = (text) => {
|
|
1345
1806
|
if (text.trim() === "") return { kind: "empty" };
|
|
@@ -1419,7 +1880,8 @@ var useMaskedInput = (field, options) => {
|
|
|
1419
1880
|
field.setValue(parsed.value);
|
|
1420
1881
|
return;
|
|
1421
1882
|
case "empty": {
|
|
1422
|
-
const
|
|
1883
|
+
const isStringField = typeof field.initialValue === "string" || typeof field.value === "string";
|
|
1884
|
+
const empty = field.clearable || !isStringField ? field.emptyValue : "";
|
|
1423
1885
|
setEdit({ raw: text, pushed: empty });
|
|
1424
1886
|
field.setValue(empty);
|
|
1425
1887
|
return;
|
|
@@ -1435,6 +1897,15 @@ var useMaskedInput = (field, options) => {
|
|
|
1435
1897
|
}
|
|
1436
1898
|
};
|
|
1437
1899
|
};
|
|
1900
|
+
var settleOnLiveValues = async (form, fields, round = 0) => {
|
|
1901
|
+
const before = fields.map((path) => form.getField(path));
|
|
1902
|
+
const result = await form.validateFieldsAsync(fields);
|
|
1903
|
+
const changed = fields.some(
|
|
1904
|
+
(path, i) => !Object.is(form.getField(path), before[i])
|
|
1905
|
+
);
|
|
1906
|
+
if (!changed) return result;
|
|
1907
|
+
return round < 3 ? settleOnLiveValues(form, fields, round + 1) : null;
|
|
1908
|
+
};
|
|
1438
1909
|
var useFormSteps = (form, steps) => {
|
|
1439
1910
|
const [step, setStep] = useState(0);
|
|
1440
1911
|
const [visited, setVisited] = useState(
|
|
@@ -1448,7 +1919,8 @@ var useFormSteps = (form, steps) => {
|
|
|
1448
1919
|
async (index) => {
|
|
1449
1920
|
const definition = latest.current[index];
|
|
1450
1921
|
if (definition === void 0) return true;
|
|
1451
|
-
const result = await form
|
|
1922
|
+
const result = await settleOnLiveValues(form, definition.fields);
|
|
1923
|
+
if (result === null) return false;
|
|
1452
1924
|
if (result.kind === "invalid") {
|
|
1453
1925
|
Object.keys(result.errors).forEach((key) => {
|
|
1454
1926
|
form.setTouched(key, true);
|
|
@@ -1460,45 +1932,49 @@ var useFormSteps = (form, steps) => {
|
|
|
1460
1932
|
[form]
|
|
1461
1933
|
);
|
|
1462
1934
|
const currentStep = useRef(step);
|
|
1463
|
-
|
|
1464
|
-
currentStep.current =
|
|
1465
|
-
|
|
1935
|
+
const navigate = useCallback((target) => {
|
|
1936
|
+
currentStep.current = target;
|
|
1937
|
+
setStep(target);
|
|
1938
|
+
}, []);
|
|
1466
1939
|
const next = useCallback(async () => {
|
|
1467
|
-
const
|
|
1940
|
+
const from = currentStep.current;
|
|
1941
|
+
const ok = await validateStep(from);
|
|
1468
1942
|
if (!ok) return false;
|
|
1469
|
-
if (currentStep.current !==
|
|
1470
|
-
const target = Math.min(
|
|
1471
|
-
|
|
1943
|
+
if (currentStep.current !== from) return false;
|
|
1944
|
+
const target = Math.max(0, Math.min(from + 1, latest.current.length - 1));
|
|
1945
|
+
navigate(target);
|
|
1472
1946
|
setVisited((prev) => /* @__PURE__ */ new Set([...prev, target]));
|
|
1473
1947
|
return true;
|
|
1474
|
-
}, [validateStep,
|
|
1948
|
+
}, [validateStep, navigate]);
|
|
1475
1949
|
const back = useCallback(() => {
|
|
1476
|
-
|
|
1477
|
-
}, []);
|
|
1950
|
+
navigate(Math.max(currentStep.current - 1, 0));
|
|
1951
|
+
}, [navigate]);
|
|
1478
1952
|
const goTo = useCallback(
|
|
1479
1953
|
async (target) => {
|
|
1480
1954
|
const count = latest.current.length;
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1955
|
+
if (!Number.isFinite(target)) return currentStep.current;
|
|
1956
|
+
const from = currentStep.current;
|
|
1957
|
+
const clamped = Math.max(0, Math.min(Math.trunc(target), count - 1));
|
|
1958
|
+
if (clamped <= from) {
|
|
1959
|
+
navigate(clamped);
|
|
1484
1960
|
return clamped;
|
|
1485
1961
|
}
|
|
1486
|
-
const walk = async (
|
|
1487
|
-
if (
|
|
1488
|
-
return await validateStep(
|
|
1962
|
+
const walk = async (at) => {
|
|
1963
|
+
if (at >= clamped) return clamped;
|
|
1964
|
+
return await validateStep(at) ? walk(at + 1) : at;
|
|
1489
1965
|
};
|
|
1490
|
-
const landed = await walk(
|
|
1491
|
-
if (currentStep.current !==
|
|
1492
|
-
|
|
1966
|
+
const landed = await walk(from);
|
|
1967
|
+
if (currentStep.current !== from) return currentStep.current;
|
|
1968
|
+
navigate(landed);
|
|
1493
1969
|
setVisited(
|
|
1494
1970
|
(prev) => /* @__PURE__ */ new Set([
|
|
1495
1971
|
...prev,
|
|
1496
|
-
...Array.from({ length: landed -
|
|
1972
|
+
...Array.from({ length: landed - from + 1 }, (_, i) => from + i)
|
|
1497
1973
|
])
|
|
1498
1974
|
);
|
|
1499
1975
|
return landed;
|
|
1500
1976
|
},
|
|
1501
|
-
[validateStep,
|
|
1977
|
+
[validateStep, navigate]
|
|
1502
1978
|
);
|
|
1503
1979
|
const errorFlags = useFormSelectorShallow(
|
|
1504
1980
|
form,
|
|
@@ -1551,8 +2027,7 @@ var useFormAction = (form, onValid, options) => {
|
|
|
1551
2027
|
const { onValid: handler, options: opts } = latest.current;
|
|
1552
2028
|
const result = await form.submit(
|
|
1553
2029
|
(data) => handler(data, formData),
|
|
1554
|
-
opts
|
|
1555
|
-
opts?.onError === void 0 ? void 0 : { onError: opts.onError }
|
|
2030
|
+
opts ?? {}
|
|
1556
2031
|
);
|
|
1557
2032
|
warnUnobserved(result, opts, "useFormAction");
|
|
1558
2033
|
},
|
|
@@ -1568,8 +2043,7 @@ var useFormActionState = (form, action, initialState, options) => {
|
|
|
1568
2043
|
async (data) => {
|
|
1569
2044
|
nextRef.current = await action(prev, data, formData);
|
|
1570
2045
|
},
|
|
1571
|
-
options
|
|
1572
|
-
options?.onError === void 0 ? void 0 : { onError: options.onError }
|
|
2046
|
+
options ?? {}
|
|
1573
2047
|
);
|
|
1574
2048
|
warnUnobserved(result, options, "useFormActionState");
|
|
1575
2049
|
return nextRef.current;
|
|
@@ -1593,6 +2067,7 @@ var createFormContext = () => {
|
|
|
1593
2067
|
return { Provider, useFormContext };
|
|
1594
2068
|
};
|
|
1595
2069
|
function useField(form, pathArg, options) {
|
|
2070
|
+
const formSchema = form.schema;
|
|
1596
2071
|
const slice = useStore(
|
|
1597
2072
|
form.store,
|
|
1598
2073
|
useShallow((state) => {
|
|
@@ -1608,25 +2083,26 @@ function useField(form, pathArg, options) {
|
|
|
1608
2083
|
error: entryAt(state.errors, p),
|
|
1609
2084
|
touched: flagAt(state.touched, p),
|
|
1610
2085
|
dirty: isFieldDirty(value, initialValue2),
|
|
1611
|
-
isValidating: flagAt(state.isValidating, p)
|
|
2086
|
+
isValidating: flagAt(state.isValidating, p),
|
|
2087
|
+
// Resolved inside the selector so a discriminant change re-reads
|
|
2088
|
+
// the ACTIVE variant's wrapper for a key the variants share; the
|
|
2089
|
+
// schema node is a stable reference, so the shallow compare holds.
|
|
2090
|
+
schema: formSchema === void 0 ? null : fieldSchemaForIntrospection(formSchema, p, state.values)
|
|
1612
2091
|
};
|
|
1613
2092
|
})
|
|
1614
2093
|
);
|
|
1615
2094
|
const path = slice.path;
|
|
1616
|
-
const
|
|
2095
|
+
const sub = slice.schema;
|
|
1617
2096
|
const initialValue = slice.initialValue;
|
|
1618
2097
|
const { emptyValue, clearable } = useMemo(() => {
|
|
1619
|
-
if (
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
clearable: isClearableSchema(sub)
|
|
1625
|
-
};
|
|
1626
|
-
}
|
|
2098
|
+
if (sub !== null) {
|
|
2099
|
+
return {
|
|
2100
|
+
emptyValue: emptyValueForSchema(sub),
|
|
2101
|
+
clearable: isClearableSchema(sub)
|
|
2102
|
+
};
|
|
1627
2103
|
}
|
|
1628
2104
|
return initialValue === null ? { emptyValue: null, clearable: true } : { emptyValue: void 0, clearable: false };
|
|
1629
|
-
}, [
|
|
2105
|
+
}, [sub, initialValue]);
|
|
1630
2106
|
const debounceMs = options?.debounceMs;
|
|
1631
2107
|
const debouncerRef = useRef(null);
|
|
1632
2108
|
useEffect(() => {
|
|
@@ -1635,6 +2111,12 @@ function useField(form, pathArg, options) {
|
|
|
1635
2111
|
debouncerRef.current = null;
|
|
1636
2112
|
};
|
|
1637
2113
|
}, [form, path, debounceMs]);
|
|
2114
|
+
const validateNow = useCallback(() => {
|
|
2115
|
+
const result = form.validateField(path);
|
|
2116
|
+
if (result.kind === "pending") {
|
|
2117
|
+
void result.promise.catch(reportBackgroundFailure);
|
|
2118
|
+
}
|
|
2119
|
+
}, [form, path]);
|
|
1638
2120
|
const triggerValidate = useCallback(() => {
|
|
1639
2121
|
if (debounceMs !== void 0 && debounceMs > 0) {
|
|
1640
2122
|
const debouncer = debouncerRef.current ?? createDebouncer(debounceMs);
|
|
@@ -1646,12 +2128,12 @@ function useField(form, pathArg, options) {
|
|
|
1646
2128
|
const rebased = state.values !== scheduledValues && state.values === state.initialValues;
|
|
1647
2129
|
if (rebased) return;
|
|
1648
2130
|
if (!Object.is(getAtPath(state.values, path), scheduled)) return;
|
|
1649
|
-
void form.validateFieldAsync(path);
|
|
2131
|
+
void form.validateFieldAsync(path).catch(reportBackgroundFailure);
|
|
1650
2132
|
});
|
|
1651
2133
|
return;
|
|
1652
2134
|
}
|
|
1653
|
-
|
|
1654
|
-
}, [form, path, debounceMs]);
|
|
2135
|
+
validateNow();
|
|
2136
|
+
}, [form, path, debounceMs, validateNow]);
|
|
1655
2137
|
const setValue = useCallback(
|
|
1656
2138
|
(value) => {
|
|
1657
2139
|
const before = form.store.getState();
|
|
@@ -1700,9 +2182,10 @@ function useField(form, pathArg, options) {
|
|
|
1700
2182
|
state.submitCount > 0,
|
|
1701
2183
|
true
|
|
1702
2184
|
)) {
|
|
1703
|
-
|
|
2185
|
+
debouncerRef.current?.cancel();
|
|
2186
|
+
validateNow();
|
|
1704
2187
|
}
|
|
1705
|
-
}, [form, path,
|
|
2188
|
+
}, [form, path, validateNow]);
|
|
1706
2189
|
return useMemo(
|
|
1707
2190
|
() => ({
|
|
1708
2191
|
path: slice.path,
|
|
@@ -1729,10 +2212,11 @@ function useField(form, pathArg, options) {
|
|
|
1729
2212
|
}
|
|
1730
2213
|
|
|
1731
2214
|
// src/react/useVariantField.ts
|
|
1732
|
-
function useVariantField(form, unionPath, field) {
|
|
2215
|
+
function useVariantField(form, unionPath, field, options) {
|
|
1733
2216
|
return useField(
|
|
1734
2217
|
form,
|
|
1735
|
-
`${unionPath}.${field}
|
|
2218
|
+
`${unionPath}.${field}`,
|
|
2219
|
+
options
|
|
1736
2220
|
);
|
|
1737
2221
|
}
|
|
1738
2222
|
var EMPTY_ITEMS = [];
|
|
@@ -1895,6 +2379,12 @@ function useFieldArray(form, pathArg) {
|
|
|
1895
2379
|
const items = slice.items;
|
|
1896
2380
|
const error = slice.error;
|
|
1897
2381
|
const ids = deriveIds(form.store, path, items).ids;
|
|
2382
|
+
const validateNow = useCallback(() => {
|
|
2383
|
+
const result = form.validateField?.(path);
|
|
2384
|
+
if (result?.kind === "pending") {
|
|
2385
|
+
void result.promise.catch(reportBackgroundFailure);
|
|
2386
|
+
}
|
|
2387
|
+
}, [form, path]);
|
|
1898
2388
|
const revalidate = useCallback(() => {
|
|
1899
2389
|
const state = form.store.getState();
|
|
1900
2390
|
if (shouldValidateOn(
|
|
@@ -1904,9 +2394,9 @@ function useFieldArray(form, pathArg) {
|
|
|
1904
2394
|
state.submitCount > 0,
|
|
1905
2395
|
flagAt(state.touched, path)
|
|
1906
2396
|
)) {
|
|
1907
|
-
|
|
2397
|
+
validateNow();
|
|
1908
2398
|
}
|
|
1909
|
-
}, [form, path]);
|
|
2399
|
+
}, [form, path, validateNow]);
|
|
1910
2400
|
const push = useCallback(
|
|
1911
2401
|
(item) => {
|
|
1912
2402
|
form.arrayPush(path, item);
|
|
@@ -1971,13 +2461,17 @@ function useFieldArray(form, pathArg) {
|
|
|
1971
2461
|
state.submitCount > 0,
|
|
1972
2462
|
true
|
|
1973
2463
|
)) {
|
|
1974
|
-
|
|
2464
|
+
validateNow();
|
|
1975
2465
|
}
|
|
1976
|
-
}, [form, path]);
|
|
2466
|
+
}, [form, path, validateNow]);
|
|
1977
2467
|
const validate = useCallback(
|
|
1978
2468
|
() => form.validateField?.(path) ?? { kind: "valid" },
|
|
1979
2469
|
[form, path]
|
|
1980
2470
|
);
|
|
2471
|
+
const validateAsync2 = useCallback(
|
|
2472
|
+
() => form.validateFieldAsync?.(path) ?? Promise.resolve({ kind: "valid" }),
|
|
2473
|
+
[form, path]
|
|
2474
|
+
);
|
|
1981
2475
|
const replace = useCallback(
|
|
1982
2476
|
(next) => {
|
|
1983
2477
|
form.setValue?.(path, next);
|
|
@@ -1987,7 +2481,15 @@ function useFieldArray(form, pathArg) {
|
|
|
1987
2481
|
);
|
|
1988
2482
|
const update = useCallback(
|
|
1989
2483
|
(index, item) => {
|
|
1990
|
-
form.
|
|
2484
|
+
const current = getAtPath(form.store.getState().values, path);
|
|
2485
|
+
const length = Array.isArray(current) ? current.length : 0;
|
|
2486
|
+
if (!Number.isInteger(index) || index < 0 || index >= length) {
|
|
2487
|
+
console.warn(
|
|
2488
|
+
`[formstand] array op on "${path}" with an out-of-range or non-integer index (length ${String(length)}). Operation skipped.`
|
|
2489
|
+
);
|
|
2490
|
+
return;
|
|
2491
|
+
}
|
|
2492
|
+
form.setValue?.(`${path}.${String(index)}`, item);
|
|
1991
2493
|
revalidate();
|
|
1992
2494
|
},
|
|
1993
2495
|
[form, path, revalidate]
|
|
@@ -2019,6 +2521,7 @@ function useFieldArray(form, pathArg) {
|
|
|
2019
2521
|
setTouched,
|
|
2020
2522
|
onBlur,
|
|
2021
2523
|
validate,
|
|
2524
|
+
validateAsync: validateAsync2,
|
|
2022
2525
|
push,
|
|
2023
2526
|
remove,
|
|
2024
2527
|
insert,
|
|
@@ -2042,6 +2545,7 @@ function useFieldArray(form, pathArg) {
|
|
|
2042
2545
|
setTouched,
|
|
2043
2546
|
onBlur,
|
|
2044
2547
|
validate,
|
|
2548
|
+
validateAsync2,
|
|
2045
2549
|
push,
|
|
2046
2550
|
remove,
|
|
2047
2551
|
insert,
|
|
@@ -2069,29 +2573,41 @@ var useFormError = (form) => useStore(
|
|
|
2069
2573
|
function useFormValues(form) {
|
|
2070
2574
|
return useStore(form.store, (state) => state.values);
|
|
2071
2575
|
}
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2576
|
+
function useFields(form, paths, options) {
|
|
2577
|
+
const [arity] = useState(paths.length);
|
|
2578
|
+
if (paths.length !== arity) {
|
|
2579
|
+
throw new Error(
|
|
2580
|
+
`[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.`
|
|
2581
|
+
);
|
|
2582
|
+
}
|
|
2075
2583
|
const fields = paths.map(
|
|
2076
2584
|
(path) => (
|
|
2077
2585
|
// 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
|
-
useField(
|
|
2586
|
+
useField(
|
|
2587
|
+
form,
|
|
2588
|
+
path,
|
|
2589
|
+
options
|
|
2590
|
+
)
|
|
2079
2591
|
)
|
|
2080
2592
|
);
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
fields.
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2593
|
+
return useMemo(() => {
|
|
2594
|
+
const merged = [
|
|
2595
|
+
...new Set(fields.flatMap((field) => field.error ?? []))
|
|
2596
|
+
];
|
|
2597
|
+
return {
|
|
2598
|
+
fields,
|
|
2599
|
+
error: merged.length > 0 ? merged : void 0,
|
|
2600
|
+
firstError: merged[0],
|
|
2601
|
+
touched: fields.some((field) => field.touched),
|
|
2602
|
+
dirty: fields.some((field) => field.dirty),
|
|
2603
|
+
isValidating: fields.some((field) => field.isValidating),
|
|
2604
|
+
onBlur: () => {
|
|
2605
|
+
fields.forEach((field) => {
|
|
2606
|
+
field.onBlur();
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
};
|
|
2610
|
+
}, fields);
|
|
2095
2611
|
}
|
|
2096
2612
|
function useIsDirty(form, path) {
|
|
2097
2613
|
return useStore(
|
|
@@ -2134,10 +2650,11 @@ var capitalize = (name) => name.length === 0 ? name : name.charAt(0).toUpperCase
|
|
|
2134
2650
|
var createFormHooks = (form, name) => {
|
|
2135
2651
|
const structural = form;
|
|
2136
2652
|
const useBoundField = (path, options) => useField(structural, path, options);
|
|
2137
|
-
const useBoundVariantField = (unionPath, field) => useVariantField(
|
|
2653
|
+
const useBoundVariantField = (unionPath, field, options) => useVariantField(
|
|
2138
2654
|
structural,
|
|
2139
2655
|
unionPath,
|
|
2140
|
-
field
|
|
2656
|
+
field,
|
|
2657
|
+
options
|
|
2141
2658
|
);
|
|
2142
2659
|
const useBoundFieldArray = (path) => useFieldArray(structural, path);
|
|
2143
2660
|
const useBoundVariantFieldArray = (unionPath, field) => useVariantFieldArray(
|
|
@@ -2153,12 +2670,16 @@ var createFormHooks = (form, name) => {
|
|
|
2153
2670
|
const useBoundIsValidating = (path) => useIsValidating(structural, path);
|
|
2154
2671
|
const useBoundIsSubmitting = () => useIsSubmitting(structural);
|
|
2155
2672
|
const useBoundValues = () => useFormValues(structural);
|
|
2156
|
-
const useBoundFields = (paths) => useFields(
|
|
2673
|
+
const useBoundFields = (paths, options) => useFields(
|
|
2157
2674
|
structural,
|
|
2158
|
-
paths
|
|
2675
|
+
paths,
|
|
2676
|
+
options
|
|
2159
2677
|
);
|
|
2160
2678
|
const useBoundSubmitCount = () => useSubmitCount(structural);
|
|
2161
2679
|
const useBoundSteps = (steps) => useFormSteps(form, steps);
|
|
2680
|
+
const useBoundAction = (onValid, options) => useFormAction(form, onValid, options);
|
|
2681
|
+
const useBoundActionState = (action, initialState, options) => useFormActionState(form, action, initialState, options);
|
|
2682
|
+
const useBoundPersist = (options) => usePersistForm(form, options);
|
|
2162
2683
|
const prefix = capitalize(name ?? "");
|
|
2163
2684
|
return {
|
|
2164
2685
|
[`use${prefix}Field`]: useBoundField,
|
|
@@ -2175,7 +2696,10 @@ var createFormHooks = (form, name) => {
|
|
|
2175
2696
|
[`use${prefix}SubmitCount`]: useBoundSubmitCount,
|
|
2176
2697
|
[`use${prefix}Values`]: useBoundValues,
|
|
2177
2698
|
[`use${prefix}Fields`]: useBoundFields,
|
|
2178
|
-
[`use${prefix}Steps`]: useBoundSteps
|
|
2699
|
+
[`use${prefix}Steps`]: useBoundSteps,
|
|
2700
|
+
[`use${prefix}Action`]: useBoundAction,
|
|
2701
|
+
[`use${prefix}ActionState`]: useBoundActionState,
|
|
2702
|
+
[`use${prefix}Persist`]: useBoundPersist
|
|
2179
2703
|
};
|
|
2180
2704
|
};
|
|
2181
2705
|
var useFieldA11y = (error) => {
|
|
@@ -2199,7 +2723,7 @@ var FieldShell = ({
|
|
|
2199
2723
|
wrapLabel,
|
|
2200
2724
|
children
|
|
2201
2725
|
}) => /* @__PURE__ */ jsxs("div", { className: "zf-field", children: [
|
|
2202
|
-
wrapLabel === true ? /* @__PURE__ */ jsxs("label", { htmlFor: id, className: "zf-label", children: [
|
|
2726
|
+
wrapLabel === true && label !== void 0 ? /* @__PURE__ */ jsxs("label", { htmlFor: id, className: "zf-label", children: [
|
|
2203
2727
|
children,
|
|
2204
2728
|
" ",
|
|
2205
2729
|
label
|
|
@@ -2470,6 +2994,6 @@ var focusField = (path, root) => {
|
|
|
2470
2994
|
return path === "" ? (root !== void 0 || document.querySelectorAll("form").length <= 1) && focusFirstOf(namedControls(scope).filter(isFocusCandidate)) : focusFirstOf(pathCandidates(scope, [path]).filter(isFocusCandidate));
|
|
2471
2995
|
};
|
|
2472
2996
|
|
|
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 };
|
|
2997
|
+
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
2998
|
//# sourceMappingURL=index.js.map
|
|
2475
2999
|
//# sourceMappingURL=index.js.map
|