formstand 0.2.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 +82 -0
- package/LICENSE +21 -0
- package/README.md +414 -0
- package/dist/index.cjs +1418 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +330 -0
- package/dist/index.d.ts +330 -0
- package/dist/index.js +1386 -0
- package/dist/index.js.map +1 -0
- package/package.json +89 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1418 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var vanilla = require('zustand/vanilla');
|
|
4
|
+
var zod = require('zod');
|
|
5
|
+
var react = require('react');
|
|
6
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
7
|
+
var react$1 = require('zustand/react');
|
|
8
|
+
var shallow = require('zustand/react/shallow');
|
|
9
|
+
|
|
10
|
+
// src/core/createForm.ts
|
|
11
|
+
|
|
12
|
+
// src/core/array.ts
|
|
13
|
+
var removeAt = (i) => (n) => n === i ? null : n > i ? n - 1 : n;
|
|
14
|
+
var insertAt = (i) => (n) => n >= i ? n + 1 : n;
|
|
15
|
+
var moveFromTo = (from, to) => (n) => {
|
|
16
|
+
if (n === from) return to;
|
|
17
|
+
if (from < to && n > from && n <= to) return n - 1;
|
|
18
|
+
if (from > to && n >= to && n < from) return n + 1;
|
|
19
|
+
return n;
|
|
20
|
+
};
|
|
21
|
+
var swapIndices = (a, b) => (n) => n === a ? b : n === b ? a : n;
|
|
22
|
+
var indexAndTail = (rest) => {
|
|
23
|
+
const dotIdx = rest.indexOf(".");
|
|
24
|
+
const idxStr = dotIdx === -1 ? rest : rest.slice(0, dotIdx);
|
|
25
|
+
const tail = dotIdx === -1 ? "" : rest.slice(dotIdx);
|
|
26
|
+
const idx = Number(idxStr);
|
|
27
|
+
if (!Number.isInteger(idx) || idx < 0 || String(idx) !== idxStr) return null;
|
|
28
|
+
return { index: idx, tail };
|
|
29
|
+
};
|
|
30
|
+
var reKeyByArrayPath = (map, basePath, mapper) => {
|
|
31
|
+
const prefix = basePath === "" ? "" : `${basePath}.`;
|
|
32
|
+
return Object.fromEntries(
|
|
33
|
+
Object.entries(map).flatMap(([key, value]) => {
|
|
34
|
+
if (key === basePath) return [[key, value]];
|
|
35
|
+
if (!key.startsWith(prefix)) return [[key, value]];
|
|
36
|
+
const parsed = indexAndTail(key.slice(prefix.length));
|
|
37
|
+
if (parsed === null) return [[key, value]];
|
|
38
|
+
const next = mapper(parsed.index);
|
|
39
|
+
if (next === null) return [];
|
|
40
|
+
return [[`${prefix}${next}${parsed.tail}`, value]];
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// src/core/equality.ts
|
|
46
|
+
var isPlainObject = (v) => {
|
|
47
|
+
if (typeof v !== "object" || v === null) return false;
|
|
48
|
+
const proto = Object.getPrototypeOf(v);
|
|
49
|
+
return proto === Object.prototype || proto === null;
|
|
50
|
+
};
|
|
51
|
+
var valuesEqual = (a, b) => {
|
|
52
|
+
if (Object.is(a, b)) return true;
|
|
53
|
+
if (a instanceof Date && b instanceof Date) {
|
|
54
|
+
return a.getTime() === b.getTime();
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
57
|
+
return a.length === b.length && a.every((v, i) => valuesEqual(v, b[i]));
|
|
58
|
+
}
|
|
59
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
60
|
+
const aKeys = Object.keys(a);
|
|
61
|
+
const bKeys = Object.keys(b);
|
|
62
|
+
return aKeys.length === bKeys.length && aKeys.every((k) => valuesEqual(a[k], b[k]));
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
};
|
|
66
|
+
var dirtyCache = /* @__PURE__ */ new WeakMap();
|
|
67
|
+
var MAX_VERDICTS = 4;
|
|
68
|
+
var isFieldDirty = (value, initialValue) => {
|
|
69
|
+
if (Object.is(value, initialValue)) return false;
|
|
70
|
+
if (typeof value !== "object" || value === null) {
|
|
71
|
+
return !valuesEqual(value, initialValue);
|
|
72
|
+
}
|
|
73
|
+
const cached = dirtyCache.get(value) ?? [];
|
|
74
|
+
const hit = cached.find((v) => Object.is(v.initial, initialValue));
|
|
75
|
+
if (hit !== void 0) return hit.dirty;
|
|
76
|
+
const dirty = !valuesEqual(value, initialValue);
|
|
77
|
+
dirtyCache.set(
|
|
78
|
+
value,
|
|
79
|
+
[{ initial: initialValue, dirty }, ...cached].slice(0, MAX_VERDICTS)
|
|
80
|
+
);
|
|
81
|
+
return dirty;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// src/core/path.ts
|
|
85
|
+
var MAX_ARRAY_INDEX = 1e5;
|
|
86
|
+
var isIndexSegment = (s) => {
|
|
87
|
+
const n = Number(s);
|
|
88
|
+
return Number.isInteger(n) && n >= 0 && String(n) === s;
|
|
89
|
+
};
|
|
90
|
+
var parsePath = (path) => path === "" ? [] : path.split(".").map((s) => isIndexSegment(s) ? Number(s) : s);
|
|
91
|
+
var isPlainRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
92
|
+
var readSegment = (obj, segment) => {
|
|
93
|
+
if (obj == null) return void 0;
|
|
94
|
+
if (Array.isArray(obj)) {
|
|
95
|
+
return typeof segment === "number" ? obj[segment] : void 0;
|
|
96
|
+
}
|
|
97
|
+
return isPlainRecord(obj) ? obj[String(segment)] : void 0;
|
|
98
|
+
};
|
|
99
|
+
var getAtPath = (obj, path) => parsePath(path).reduce(readSegment, obj);
|
|
100
|
+
var slotAtPath = (obj, path) => {
|
|
101
|
+
const walked = parsePath(path).reduce(
|
|
102
|
+
(acc, segment) => {
|
|
103
|
+
const outOfBounds = typeof segment === "number" && (Array.isArray(acc.current) ? segment >= acc.current.length : !isPlainRecord(acc.current));
|
|
104
|
+
return !acc.ok || outOfBounds ? { ok: false, current: void 0 } : { ok: true, current: readSegment(acc.current, segment) };
|
|
105
|
+
},
|
|
106
|
+
{ ok: true, current: obj }
|
|
107
|
+
);
|
|
108
|
+
return walked.ok ? { exists: true, value: walked.current } : { exists: false };
|
|
109
|
+
};
|
|
110
|
+
var writeIntoArray = (arr, index, rest, value) => {
|
|
111
|
+
const length = Math.max(arr.length, index + 1);
|
|
112
|
+
return Array.from(
|
|
113
|
+
{ length },
|
|
114
|
+
(_, i) => i === index ? writeSegments(arr[i], rest, value) : arr[i]
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
var refusesHugeIndex = (index) => {
|
|
118
|
+
if (index > MAX_ARRAY_INDEX) {
|
|
119
|
+
console.warn(
|
|
120
|
+
`[formstand] refusing to write array index ${index} (max ${MAX_ARRAY_INDEX}); value left unchanged.`
|
|
121
|
+
);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
};
|
|
126
|
+
var writeSegments = (obj, segments, value) => {
|
|
127
|
+
const [head, ...rest] = segments;
|
|
128
|
+
if (head === void 0) return value;
|
|
129
|
+
if (Array.isArray(obj)) {
|
|
130
|
+
if (typeof head !== "number") {
|
|
131
|
+
console.warn(
|
|
132
|
+
`[formstand] cannot write string key "${head}" into an array; value left unchanged.`
|
|
133
|
+
);
|
|
134
|
+
return obj;
|
|
135
|
+
}
|
|
136
|
+
if (refusesHugeIndex(head)) return obj;
|
|
137
|
+
return writeIntoArray(obj, head, rest, value);
|
|
138
|
+
}
|
|
139
|
+
if (isPlainRecord(obj)) {
|
|
140
|
+
const key = String(head);
|
|
141
|
+
return { ...obj, [key]: writeSegments(obj[key], rest, value) };
|
|
142
|
+
}
|
|
143
|
+
if (typeof head === "number") {
|
|
144
|
+
if (refusesHugeIndex(head)) return obj;
|
|
145
|
+
return writeIntoArray([], head, rest, value);
|
|
146
|
+
}
|
|
147
|
+
return { [head]: writeSegments(void 0, rest, value) };
|
|
148
|
+
};
|
|
149
|
+
var setAtPath = (obj, path, value) => writeSegments(obj, parsePath(path), value);
|
|
150
|
+
var issuePathToFormPath = (path) => path.filter((segment) => typeof segment !== "symbol").join(".");
|
|
151
|
+
var expandIssue = (issue, prefix) => {
|
|
152
|
+
const at = [...prefix, ...issue.path];
|
|
153
|
+
if (issue.code === "invalid_union") {
|
|
154
|
+
const nested = issue.errors.flatMap(
|
|
155
|
+
(branch) => branch.flatMap((sub) => expandIssue(sub, at))
|
|
156
|
+
);
|
|
157
|
+
if (nested.length > 0) return nested;
|
|
158
|
+
}
|
|
159
|
+
return [{ path: issuePathToFormPath(at), message: issue.message }];
|
|
160
|
+
};
|
|
161
|
+
var flattenIssues = (issues) => issues.flatMap((issue) => expandIssue(issue, [])).reduce((acc, { path, message }) => {
|
|
162
|
+
const existing = acc[path] ?? [];
|
|
163
|
+
return existing.includes(message) ? acc : { ...acc, [path]: [...existing, message] };
|
|
164
|
+
}, {});
|
|
165
|
+
var validateSync = (schema, values) => {
|
|
166
|
+
const result = schema.safeParse(values);
|
|
167
|
+
return result.success ? { kind: "valid", data: result.data } : { kind: "invalid", errors: flattenIssues(result.error.issues) };
|
|
168
|
+
};
|
|
169
|
+
var validateAsync = async (schema, values) => {
|
|
170
|
+
const result = await schema.safeParseAsync(values);
|
|
171
|
+
return result.success ? { kind: "valid", data: result.data } : { kind: "invalid", errors: flattenIssues(result.error.issues) };
|
|
172
|
+
};
|
|
173
|
+
var isAsyncRequiredError = (e) => e instanceof zod.z.core.$ZodAsyncError || e instanceof Error && /Encountered Promise during synchronous parse/i.test(e.message);
|
|
174
|
+
var isPathOrChild = (key, path) => key === path || key.startsWith(`${path}.`);
|
|
175
|
+
var prefixErrorKeys = (errors, path) => path === "" ? errors : Object.fromEntries(
|
|
176
|
+
Object.entries(errors).map(([k, v]) => [
|
|
177
|
+
k === "" ? path : `${path}.${k}`,
|
|
178
|
+
v
|
|
179
|
+
])
|
|
180
|
+
);
|
|
181
|
+
var emptyValueForSchema = (schema) => {
|
|
182
|
+
const walk = (s, sawNullable) => {
|
|
183
|
+
if (s instanceof zod.z.ZodOptional) return void 0;
|
|
184
|
+
if (s instanceof zod.z.ZodNullable) {
|
|
185
|
+
return walk(s.unwrap(), true);
|
|
186
|
+
}
|
|
187
|
+
return sawNullable ? null : void 0;
|
|
188
|
+
};
|
|
189
|
+
return walk(schema, false);
|
|
190
|
+
};
|
|
191
|
+
var hasChecks = (s) => {
|
|
192
|
+
const checks = s.def.checks;
|
|
193
|
+
return checks !== void 0 && checks.length > 0;
|
|
194
|
+
};
|
|
195
|
+
var fieldSchemaAtPath = (schema, path) => parsePath(path).reduce((current, segment) => {
|
|
196
|
+
if (current === null || hasChecks(current)) return null;
|
|
197
|
+
if (current instanceof zod.z.ZodObject && typeof segment === "string") {
|
|
198
|
+
const shape = current.shape;
|
|
199
|
+
return shape[segment] ?? null;
|
|
200
|
+
}
|
|
201
|
+
if (current instanceof zod.z.ZodArray && typeof segment === "number") {
|
|
202
|
+
return current.element;
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}, schema);
|
|
206
|
+
|
|
207
|
+
// src/core/createForm.ts
|
|
208
|
+
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;
|
|
209
|
+
var emptyErrors = {};
|
|
210
|
+
var emptyBools = {};
|
|
211
|
+
var identityMapper = (n) => n;
|
|
212
|
+
var omitKey = (map, key) => Object.fromEntries(Object.entries(map).filter(([k]) => k !== key));
|
|
213
|
+
var inScopeOfAny = (key, paths) => paths.some((p) => isPathOrChild(key, p));
|
|
214
|
+
var omitScope = (map, paths) => {
|
|
215
|
+
const entries = Object.entries(map);
|
|
216
|
+
const kept = entries.filter(([k]) => !inScopeOfAny(k, paths));
|
|
217
|
+
return kept.length === entries.length ? map : Object.fromEntries(kept);
|
|
218
|
+
};
|
|
219
|
+
var pickScope = (map, paths) => Object.fromEntries(
|
|
220
|
+
Object.entries(map).filter(([k]) => inScopeOfAny(k, paths))
|
|
221
|
+
);
|
|
222
|
+
var mergeScopedErrors = (prev, scoped, paths) => ({ ...omitScope(prev, paths), ...scoped });
|
|
223
|
+
var releaseAncestorEntries = (map, path) => {
|
|
224
|
+
const entries = Object.entries(map);
|
|
225
|
+
const kept = entries.filter(([k]) => k !== "" && !isPathOrChild(path, k));
|
|
226
|
+
return kept.length === entries.length ? map : Object.fromEntries(kept);
|
|
227
|
+
};
|
|
228
|
+
var dirtyPathsOf = (values, initial, prefix) => {
|
|
229
|
+
if (valuesEqual(values, initial)) return [];
|
|
230
|
+
if (isPlainObject(values) && isPlainObject(initial)) {
|
|
231
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(values), ...Object.keys(initial)]);
|
|
232
|
+
return [...keys].flatMap(
|
|
233
|
+
(k) => dirtyPathsOf(
|
|
234
|
+
values[k],
|
|
235
|
+
initial[k],
|
|
236
|
+
prefix === "" ? k : `${prefix}.${k}`
|
|
237
|
+
)
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
return [prefix];
|
|
241
|
+
};
|
|
242
|
+
var FORM_SEQ_KEY = "__form__";
|
|
243
|
+
var stringArraysEqual = (a, b) => a === b || a.length === b.length && a.every((v, i) => v === b[i]);
|
|
244
|
+
var reuseErrorRefs = (next, prev) => {
|
|
245
|
+
if (next === prev) return prev;
|
|
246
|
+
const entries = Object.entries(next).map(([k, v]) => {
|
|
247
|
+
const old = prev[k];
|
|
248
|
+
return [
|
|
249
|
+
k,
|
|
250
|
+
old !== void 0 && stringArraysEqual(old, v) ? old : v
|
|
251
|
+
];
|
|
252
|
+
});
|
|
253
|
+
const sameShape = entries.length === Object.keys(prev).length && entries.every(([k, v]) => prev[k] === v);
|
|
254
|
+
return sameShape ? prev : Object.fromEntries(entries);
|
|
255
|
+
};
|
|
256
|
+
var scopeSettledResult = (result, path, viaSubschema) => result.kind !== "invalid" ? emptyErrors : viaSubschema ? prefixErrorKeys(result.errors, path) : path === "" ? result.errors : pickScope(result.errors, [path]);
|
|
257
|
+
var mergeErrorChannels = (schema, server) => {
|
|
258
|
+
const extra = Object.entries(server).filter(([k]) => schema[k] === void 0);
|
|
259
|
+
return extra.length === 0 ? schema : { ...schema, ...Object.fromEntries(extra) };
|
|
260
|
+
};
|
|
261
|
+
var errorChannels = (state, patch) => {
|
|
262
|
+
const schemaErrors = patch.schemaErrors ?? state.schemaErrors;
|
|
263
|
+
const serverErrors = patch.serverErrors ?? state.serverErrors;
|
|
264
|
+
if (schemaErrors === state.schemaErrors && serverErrors === state.serverErrors) {
|
|
265
|
+
return { errors: state.errors, schemaErrors, serverErrors };
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
schemaErrors,
|
|
269
|
+
serverErrors,
|
|
270
|
+
errors: reuseErrorRefs(
|
|
271
|
+
mergeErrorChannels(schemaErrors, serverErrors),
|
|
272
|
+
state.errors
|
|
273
|
+
)
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
var releaseSpine = (server, path) => path === "" ? emptyErrors : releaseAncestorEntries(omitScope(server, [path]), path);
|
|
277
|
+
var releaseChangedSlices = (server, prevValues, nextValues) => {
|
|
278
|
+
const entries = Object.entries(server);
|
|
279
|
+
const kept = entries.filter(
|
|
280
|
+
([k]) => valuesEqual(getAtPath(prevValues, k), getAtPath(nextValues, k))
|
|
281
|
+
);
|
|
282
|
+
return kept.length === entries.length ? server : Object.fromEntries(kept);
|
|
283
|
+
};
|
|
284
|
+
var createForm = (schema, options) => {
|
|
285
|
+
const initialMode = options.mode ?? "onBlur";
|
|
286
|
+
const initialReValidateMode = options.reValidateMode ?? "onChange";
|
|
287
|
+
const initial = {
|
|
288
|
+
values: options.initialValues,
|
|
289
|
+
initialValues: options.initialValues,
|
|
290
|
+
errors: emptyErrors,
|
|
291
|
+
schemaErrors: emptyErrors,
|
|
292
|
+
serverErrors: emptyErrors,
|
|
293
|
+
touched: emptyBools,
|
|
294
|
+
isSubmitting: false,
|
|
295
|
+
submitCount: 0,
|
|
296
|
+
isValidating: emptyBools,
|
|
297
|
+
isValidatingForm: false,
|
|
298
|
+
mode: initialMode,
|
|
299
|
+
reValidateMode: initialReValidateMode
|
|
300
|
+
};
|
|
301
|
+
const store = vanilla.createStore(() => initial);
|
|
302
|
+
const commitFullPass = (state, next) => errorChannels(state, {
|
|
303
|
+
schemaErrors: reuseErrorRefs(next, state.schemaErrors)
|
|
304
|
+
});
|
|
305
|
+
const validate = () => {
|
|
306
|
+
try {
|
|
307
|
+
const result = validateSync(schema, store.getState().values);
|
|
308
|
+
store.setState((state) => ({
|
|
309
|
+
...state,
|
|
310
|
+
...commitFullPass(
|
|
311
|
+
state,
|
|
312
|
+
result.kind === "invalid" ? result.errors : emptyErrors
|
|
313
|
+
)
|
|
314
|
+
}));
|
|
315
|
+
return result;
|
|
316
|
+
} catch (e) {
|
|
317
|
+
if (isAsyncRequiredError(e)) {
|
|
318
|
+
return { kind: "pending", promise: validateAsyncOnForm() };
|
|
319
|
+
}
|
|
320
|
+
throw e;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
const validateFields = (paths) => {
|
|
324
|
+
if (paths.length === 0) return true;
|
|
325
|
+
if (paths.some((p) => p === "")) {
|
|
326
|
+
const result = validate();
|
|
327
|
+
return result.kind === "pending" ? result.promise.then((r) => r.kind === "valid") : result.kind === "valid";
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
const result = validateSync(schema, store.getState().values);
|
|
331
|
+
const fullErrors = result.kind === "invalid" ? result.errors : emptyErrors;
|
|
332
|
+
store.setState((state) => ({
|
|
333
|
+
...state,
|
|
334
|
+
...errorChannels(state, {
|
|
335
|
+
schemaErrors: reuseErrorRefs(
|
|
336
|
+
mergeScopedErrors(
|
|
337
|
+
state.schemaErrors,
|
|
338
|
+
pickScope(fullErrors, paths),
|
|
339
|
+
paths
|
|
340
|
+
),
|
|
341
|
+
state.schemaErrors
|
|
342
|
+
),
|
|
343
|
+
// A validation explicitly targeting these paths supersedes any
|
|
344
|
+
// server verdict on them.
|
|
345
|
+
serverErrors: omitScope(state.serverErrors, paths)
|
|
346
|
+
})
|
|
347
|
+
}));
|
|
348
|
+
return Object.keys(fullErrors).every((k) => !inScopeOfAny(k, paths));
|
|
349
|
+
} catch (e) {
|
|
350
|
+
if (isAsyncRequiredError(e)) return validateFieldsAsync(paths);
|
|
351
|
+
throw e;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
const subschemaCache = /* @__PURE__ */ new Map();
|
|
355
|
+
const subschemaAt = (path) => {
|
|
356
|
+
const cached = subschemaCache.get(path);
|
|
357
|
+
if (cached !== void 0) return cached;
|
|
358
|
+
const sub = fieldSchemaAtPath(schema, path);
|
|
359
|
+
subschemaCache.set(path, sub);
|
|
360
|
+
return sub;
|
|
361
|
+
};
|
|
362
|
+
const fieldScopeFor = (path, values) => {
|
|
363
|
+
const sub = path === "" ? null : subschemaAt(path);
|
|
364
|
+
if (sub === null) {
|
|
365
|
+
return { kind: "parse", schema, value: values, viaSubschema: false };
|
|
366
|
+
}
|
|
367
|
+
const slot = slotAtPath(values, path);
|
|
368
|
+
return slot.exists ? { kind: "parse", schema: sub, value: slot.value, viaSubschema: true } : { kind: "skip" };
|
|
369
|
+
};
|
|
370
|
+
const scopedFieldErrors = (path) => {
|
|
371
|
+
const values = store.getState().values;
|
|
372
|
+
const scope = fieldScopeFor(path, values);
|
|
373
|
+
return scope.kind === "skip" ? emptyErrors : scopeSettledResult(
|
|
374
|
+
validateSync(scope.schema, scope.value),
|
|
375
|
+
path,
|
|
376
|
+
scope.viaSubschema
|
|
377
|
+
);
|
|
378
|
+
};
|
|
379
|
+
const scopedFieldErrorsAsync = async (path, values) => {
|
|
380
|
+
const scope = fieldScopeFor(path, values);
|
|
381
|
+
return scope.kind === "skip" ? emptyErrors : scopeSettledResult(
|
|
382
|
+
await validateAsync(scope.schema, scope.value),
|
|
383
|
+
path,
|
|
384
|
+
scope.viaSubschema
|
|
385
|
+
);
|
|
386
|
+
};
|
|
387
|
+
const fieldResult = (scoped) => {
|
|
388
|
+
const messages = Object.values(scoped).flat();
|
|
389
|
+
return messages.length === 0 ? { kind: "valid" } : { kind: "invalid", errors: messages };
|
|
390
|
+
};
|
|
391
|
+
const commitFieldErrors = (state, scoped, path) => path === "" ? commitFullPass(state, scoped) : errorChannels(state, {
|
|
392
|
+
schemaErrors: reuseErrorRefs(
|
|
393
|
+
mergeScopedErrors(state.schemaErrors, scoped, [path]),
|
|
394
|
+
state.schemaErrors
|
|
395
|
+
),
|
|
396
|
+
serverErrors: omitScope(state.serverErrors, [path])
|
|
397
|
+
});
|
|
398
|
+
const validateField = (path) => {
|
|
399
|
+
try {
|
|
400
|
+
const scoped = scopedFieldErrors(path);
|
|
401
|
+
store.setState((state) => ({
|
|
402
|
+
...state,
|
|
403
|
+
...commitFieldErrors(state, scoped, path)
|
|
404
|
+
}));
|
|
405
|
+
return fieldResult(scoped);
|
|
406
|
+
} catch (e) {
|
|
407
|
+
if (isAsyncRequiredError(e)) {
|
|
408
|
+
return { kind: "pending", promise: validateFieldAsync(path) };
|
|
409
|
+
}
|
|
410
|
+
throw e;
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
const snapshotField = (state, path) => {
|
|
414
|
+
const value = getAtPath(state.values, path);
|
|
415
|
+
return {
|
|
416
|
+
value,
|
|
417
|
+
error: state.errors[path],
|
|
418
|
+
touched: state.touched[path] ?? false,
|
|
419
|
+
dirty: isFieldDirty(value, getAtPath(state.initialValues, path)),
|
|
420
|
+
isValidating: state.isValidating[path] ?? false
|
|
421
|
+
};
|
|
422
|
+
};
|
|
423
|
+
const sequences = /* @__PURE__ */ new Map();
|
|
424
|
+
const nextSeq = (key) => {
|
|
425
|
+
const next = (sequences.get(key) ?? 0) + 1;
|
|
426
|
+
sequences.set(key, next);
|
|
427
|
+
return next;
|
|
428
|
+
};
|
|
429
|
+
const validateAsyncOnForm = async () => {
|
|
430
|
+
const seq = nextSeq(FORM_SEQ_KEY);
|
|
431
|
+
const valuesAtStart = store.getState().values;
|
|
432
|
+
store.setState((state) => ({ ...state, isValidatingForm: true }));
|
|
433
|
+
const result = await validateAsync(schema, valuesAtStart);
|
|
434
|
+
const live = store.getState();
|
|
435
|
+
const stillOwns = sequences.get(FORM_SEQ_KEY) === seq;
|
|
436
|
+
if (!stillOwns || live.values !== valuesAtStart) {
|
|
437
|
+
if (stillOwns) {
|
|
438
|
+
store.setState((state) => ({ ...state, isValidatingForm: false }));
|
|
439
|
+
}
|
|
440
|
+
return result;
|
|
441
|
+
}
|
|
442
|
+
store.setState((state) => ({
|
|
443
|
+
...state,
|
|
444
|
+
...commitFullPass(
|
|
445
|
+
state,
|
|
446
|
+
result.kind === "invalid" ? result.errors : emptyErrors
|
|
447
|
+
),
|
|
448
|
+
isValidatingForm: false
|
|
449
|
+
}));
|
|
450
|
+
return result;
|
|
451
|
+
};
|
|
452
|
+
const validateFieldsAsync = async (paths) => {
|
|
453
|
+
if (paths.length === 0) return true;
|
|
454
|
+
if (paths.some((p) => p === "")) {
|
|
455
|
+
const result2 = await validateAsyncOnForm();
|
|
456
|
+
return result2.kind === "valid";
|
|
457
|
+
}
|
|
458
|
+
const seqs = new Map(paths.map((p) => [p, nextSeq(p)]));
|
|
459
|
+
const valuesAtStart = store.getState().values;
|
|
460
|
+
store.setState((state) => ({
|
|
461
|
+
...state,
|
|
462
|
+
isValidating: paths.reduce(
|
|
463
|
+
(acc, p) => ({ ...acc, [p]: true }),
|
|
464
|
+
{ ...state.isValidating }
|
|
465
|
+
)
|
|
466
|
+
}));
|
|
467
|
+
const result = await validateAsync(schema, valuesAtStart);
|
|
468
|
+
const fullErrors = result.kind === "invalid" ? result.errors : emptyErrors;
|
|
469
|
+
const requestedValid = Object.keys(fullErrors).every(
|
|
470
|
+
(k) => !inScopeOfAny(k, paths)
|
|
471
|
+
);
|
|
472
|
+
const live = store.getState();
|
|
473
|
+
const stillCurrent = paths.filter((p) => sequences.get(p) === seqs.get(p));
|
|
474
|
+
if (live.values !== valuesAtStart) {
|
|
475
|
+
if (stillCurrent.length > 0) {
|
|
476
|
+
store.setState((state) => ({
|
|
477
|
+
...state,
|
|
478
|
+
isValidating: stillCurrent.reduce(
|
|
479
|
+
(acc, p) => omitKey(acc, p),
|
|
480
|
+
state.isValidating
|
|
481
|
+
)
|
|
482
|
+
}));
|
|
483
|
+
}
|
|
484
|
+
return requestedValid;
|
|
485
|
+
}
|
|
486
|
+
store.setState((state) => ({
|
|
487
|
+
...state,
|
|
488
|
+
...errorChannels(state, {
|
|
489
|
+
schemaErrors: reuseErrorRefs(
|
|
490
|
+
mergeScopedErrors(
|
|
491
|
+
state.schemaErrors,
|
|
492
|
+
pickScope(fullErrors, stillCurrent),
|
|
493
|
+
stillCurrent
|
|
494
|
+
),
|
|
495
|
+
state.schemaErrors
|
|
496
|
+
),
|
|
497
|
+
serverErrors: omitScope(state.serverErrors, stillCurrent)
|
|
498
|
+
}),
|
|
499
|
+
isValidating: stillCurrent.reduce(
|
|
500
|
+
(acc, p) => omitKey(acc, p),
|
|
501
|
+
state.isValidating
|
|
502
|
+
)
|
|
503
|
+
}));
|
|
504
|
+
return requestedValid;
|
|
505
|
+
};
|
|
506
|
+
const validateFieldAsync = async (path) => {
|
|
507
|
+
const seq = nextSeq(path);
|
|
508
|
+
const valuesAtStart = store.getState().values;
|
|
509
|
+
store.setState((state) => ({
|
|
510
|
+
...state,
|
|
511
|
+
isValidating: { ...state.isValidating, [path]: true }
|
|
512
|
+
}));
|
|
513
|
+
const scoped = await scopedFieldErrorsAsync(path, valuesAtStart);
|
|
514
|
+
const live = store.getState();
|
|
515
|
+
const stillOwns = sequences.get(path) === seq;
|
|
516
|
+
if (!stillOwns || live.values !== valuesAtStart) {
|
|
517
|
+
if (stillOwns) {
|
|
518
|
+
store.setState((state) => ({
|
|
519
|
+
...state,
|
|
520
|
+
isValidating: omitKey(state.isValidating, path)
|
|
521
|
+
}));
|
|
522
|
+
}
|
|
523
|
+
return fieldResult(scoped);
|
|
524
|
+
}
|
|
525
|
+
store.setState((state) => ({
|
|
526
|
+
...state,
|
|
527
|
+
...commitFieldErrors(state, scoped, path),
|
|
528
|
+
isValidating: omitKey(state.isValidating, path)
|
|
529
|
+
}));
|
|
530
|
+
return fieldResult(scoped);
|
|
531
|
+
};
|
|
532
|
+
const applyArrayOp = (path, nextArray, mapper, indexInBounds) => {
|
|
533
|
+
const current = getAtPath(store.getState().values, path);
|
|
534
|
+
if (current !== void 0 && !Array.isArray(current)) {
|
|
535
|
+
console.warn(
|
|
536
|
+
`[formstand] array op on "${path}" but the value at that path is not an array (got ${typeof current}). Operation skipped.`
|
|
537
|
+
);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const length = Array.isArray(current) ? current.length : 0;
|
|
541
|
+
if (indexInBounds !== void 0 && !indexInBounds(length)) {
|
|
542
|
+
console.warn(
|
|
543
|
+
`[formstand] array op on "${path}" with an out-of-range or non-integer index (length ${length}). Operation skipped.`
|
|
544
|
+
);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
store.setState((state) => {
|
|
548
|
+
const live = getAtPath(state.values, path);
|
|
549
|
+
const arr = Array.isArray(live) ? live : [];
|
|
550
|
+
return {
|
|
551
|
+
...state,
|
|
552
|
+
values: setAtPath(state.values, path, nextArray(arr)),
|
|
553
|
+
...errorChannels(state, {
|
|
554
|
+
schemaErrors: reKeyByArrayPath(state.schemaErrors, path, mapper),
|
|
555
|
+
// Server entries on rows follow their rows through the index
|
|
556
|
+
// mapping — a verdict on "items.1.name" is still vouched for after
|
|
557
|
+
// the op moves it to "items.0.name" (that row's value didn't
|
|
558
|
+
// change). Entries at the array itself or an ancestor release: the
|
|
559
|
+
// op changed that value.
|
|
560
|
+
serverErrors: releaseAncestorEntries(
|
|
561
|
+
reKeyByArrayPath(state.serverErrors, path, mapper),
|
|
562
|
+
path
|
|
563
|
+
)
|
|
564
|
+
}),
|
|
565
|
+
touched: reKeyByArrayPath(state.touched, path, mapper),
|
|
566
|
+
isValidating: reKeyByArrayPath(state.isValidating, path, mapper)
|
|
567
|
+
};
|
|
568
|
+
});
|
|
569
|
+
};
|
|
570
|
+
const inFlight = {
|
|
571
|
+
count: 0,
|
|
572
|
+
baseline: false
|
|
573
|
+
};
|
|
574
|
+
const submit = async (onValid, onInvalid, submitOptions) => {
|
|
575
|
+
if (inFlight.count > 0 && submitOptions?.force !== true) {
|
|
576
|
+
return { kind: "skipped" };
|
|
577
|
+
}
|
|
578
|
+
if (inFlight.count === 0) {
|
|
579
|
+
inFlight.baseline = store.getState().isSubmitting;
|
|
580
|
+
}
|
|
581
|
+
inFlight.count += 1;
|
|
582
|
+
store.setState((state) => ({
|
|
583
|
+
...state,
|
|
584
|
+
isSubmitting: true,
|
|
585
|
+
submitCount: state.submitCount + 1
|
|
586
|
+
}));
|
|
587
|
+
try {
|
|
588
|
+
const result = await validateAsync(schema, store.getState().values);
|
|
589
|
+
if (result.kind === "invalid") {
|
|
590
|
+
store.setState((state) => {
|
|
591
|
+
const committed = commitFullPass(state, result.errors);
|
|
592
|
+
return {
|
|
593
|
+
...state,
|
|
594
|
+
...committed,
|
|
595
|
+
// Mark every errored field touched so touched-gated error UIs
|
|
596
|
+
// show something after the canonical first failed submit. The
|
|
597
|
+
// "" key (schema-level refine) has no field to touch.
|
|
598
|
+
touched: {
|
|
599
|
+
...state.touched,
|
|
600
|
+
...Object.fromEntries(
|
|
601
|
+
Object.keys(committed.errors).filter((k) => k !== "").map((k) => [k, true])
|
|
602
|
+
)
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
});
|
|
606
|
+
const merged = store.getState().errors;
|
|
607
|
+
onInvalid?.(merged);
|
|
608
|
+
return { kind: "invalid", errors: merged };
|
|
609
|
+
}
|
|
610
|
+
store.setState((state) => ({
|
|
611
|
+
...state,
|
|
612
|
+
...commitFullPass(state, emptyErrors)
|
|
613
|
+
}));
|
|
614
|
+
await onValid(result.data);
|
|
615
|
+
return { kind: "valid", data: result.data };
|
|
616
|
+
} finally {
|
|
617
|
+
inFlight.count -= 1;
|
|
618
|
+
if (inFlight.count === 0) {
|
|
619
|
+
const baseline = inFlight.baseline;
|
|
620
|
+
store.setState((state) => ({ ...state, isSubmitting: baseline }));
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
if (options.validateOnMount === true) {
|
|
625
|
+
validate();
|
|
626
|
+
}
|
|
627
|
+
return Object.freeze({
|
|
628
|
+
schema,
|
|
629
|
+
store,
|
|
630
|
+
getState: store.getState,
|
|
631
|
+
subscribe: store.subscribe,
|
|
632
|
+
getField: (path) => getAtPath(store.getState().values, path),
|
|
633
|
+
getFieldState: (path) => snapshotField(store.getState(), path),
|
|
634
|
+
watchField: (path, listener) => {
|
|
635
|
+
const ref = {
|
|
636
|
+
current: snapshotField(store.getState(), path)
|
|
637
|
+
};
|
|
638
|
+
return store.subscribe((state) => {
|
|
639
|
+
const next = snapshotField(state, path);
|
|
640
|
+
if (!shallowFieldEqual(ref.current, next)) {
|
|
641
|
+
ref.current = next;
|
|
642
|
+
listener(next);
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
},
|
|
646
|
+
watchValue: (path, listener) => {
|
|
647
|
+
const ref = {
|
|
648
|
+
current: getAtPath(store.getState().values, path)
|
|
649
|
+
};
|
|
650
|
+
return store.subscribe((state) => {
|
|
651
|
+
const next = getAtPath(state.values, path);
|
|
652
|
+
if (!Object.is(ref.current, next)) {
|
|
653
|
+
const prev = ref.current;
|
|
654
|
+
ref.current = next;
|
|
655
|
+
listener(
|
|
656
|
+
next,
|
|
657
|
+
prev
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
},
|
|
662
|
+
watchValues: (listener) => {
|
|
663
|
+
const ref = { current: store.getState().values };
|
|
664
|
+
return store.subscribe((state) => {
|
|
665
|
+
if (state.values !== ref.current) {
|
|
666
|
+
const prev = ref.current;
|
|
667
|
+
ref.current = state.values;
|
|
668
|
+
listener(state.values, prev);
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
},
|
|
672
|
+
setValue: (path, value) => store.setState((state) => ({
|
|
673
|
+
...state,
|
|
674
|
+
values: setAtPath(state.values, path, value),
|
|
675
|
+
...errorChannels(state, {
|
|
676
|
+
serverErrors: releaseSpine(state.serverErrors, path)
|
|
677
|
+
})
|
|
678
|
+
})),
|
|
679
|
+
setValues: (next) => store.setState((state) => ({
|
|
680
|
+
...state,
|
|
681
|
+
values: next,
|
|
682
|
+
...errorChannels(state, {
|
|
683
|
+
serverErrors: releaseChangedSlices(
|
|
684
|
+
state.serverErrors,
|
|
685
|
+
state.values,
|
|
686
|
+
next
|
|
687
|
+
)
|
|
688
|
+
})
|
|
689
|
+
})),
|
|
690
|
+
setTouched: (path, touched = true) => store.setState((state) => ({
|
|
691
|
+
...state,
|
|
692
|
+
touched: { ...state.touched, [path]: touched }
|
|
693
|
+
})),
|
|
694
|
+
setSubmitting: (value) => store.setState((state) => ({ ...state, isSubmitting: value })),
|
|
695
|
+
setMode: (mode) => store.setState((state) => ({ ...state, mode })),
|
|
696
|
+
setReValidateMode: (reValidateMode) => store.setState((state) => ({ ...state, reValidateMode })),
|
|
697
|
+
setError: (path, errors) => {
|
|
698
|
+
const list = typeof errors === "string" ? [errors] : errors;
|
|
699
|
+
store.setState((state) => ({
|
|
700
|
+
...state,
|
|
701
|
+
...errorChannels(state, {
|
|
702
|
+
serverErrors: list.length === 0 ? omitKey(state.serverErrors, path) : { ...state.serverErrors, [path]: list }
|
|
703
|
+
})
|
|
704
|
+
}));
|
|
705
|
+
},
|
|
706
|
+
setErrors: (errors) => store.setState((state) => ({
|
|
707
|
+
...state,
|
|
708
|
+
...errorChannels(state, { serverErrors: errors })
|
|
709
|
+
})),
|
|
710
|
+
clearErrors: (path) => store.setState((state) => {
|
|
711
|
+
const scrub = (map) => path === void 0 ? emptyErrors : omitScope(map, [path]);
|
|
712
|
+
return {
|
|
713
|
+
...state,
|
|
714
|
+
...errorChannels(state, {
|
|
715
|
+
schemaErrors: scrub(state.schemaErrors),
|
|
716
|
+
serverErrors: scrub(state.serverErrors)
|
|
717
|
+
})
|
|
718
|
+
};
|
|
719
|
+
}),
|
|
720
|
+
updateState: (updater) => store.setState((state) => {
|
|
721
|
+
const patch = updater(state);
|
|
722
|
+
if (Object.keys(patch).length === 0) return state;
|
|
723
|
+
if (patch.errors !== void 0 && patch.errors !== state.errors) {
|
|
724
|
+
console.warn(
|
|
725
|
+
"[formstand] `errors` is derived from schemaErrors/serverErrors \u2014 patch those channels instead; the direct `errors` patch is ignored."
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
const next = { ...state, ...patch };
|
|
729
|
+
return {
|
|
730
|
+
...next,
|
|
731
|
+
...errorChannels(state, {
|
|
732
|
+
schemaErrors: next.schemaErrors,
|
|
733
|
+
serverErrors: next.serverErrors
|
|
734
|
+
})
|
|
735
|
+
};
|
|
736
|
+
}),
|
|
737
|
+
adoptValues: (values) => {
|
|
738
|
+
sequences.clear();
|
|
739
|
+
store.setState((state) => ({
|
|
740
|
+
...state,
|
|
741
|
+
values,
|
|
742
|
+
initialValues: values,
|
|
743
|
+
...errorChannels(state, {
|
|
744
|
+
schemaErrors: emptyErrors,
|
|
745
|
+
serverErrors: emptyErrors
|
|
746
|
+
})
|
|
747
|
+
}));
|
|
748
|
+
},
|
|
749
|
+
reset: (nextInitial, resetOptions) => {
|
|
750
|
+
sequences.clear();
|
|
751
|
+
store.setState((state) => {
|
|
752
|
+
const init = nextInitial === void 0 ? state.initialValues : isPlainObject(state.initialValues) && isPlainObject(nextInitial) ? {
|
|
753
|
+
...state.initialValues,
|
|
754
|
+
...nextInitial
|
|
755
|
+
} : nextInitial;
|
|
756
|
+
return {
|
|
757
|
+
...state,
|
|
758
|
+
values: init,
|
|
759
|
+
initialValues: init,
|
|
760
|
+
...resetOptions?.keepErrors === true ? {} : errorChannels(state, {
|
|
761
|
+
schemaErrors: emptyErrors,
|
|
762
|
+
serverErrors: emptyErrors
|
|
763
|
+
}),
|
|
764
|
+
touched: resetOptions?.keepTouched === true ? state.touched : emptyBools,
|
|
765
|
+
isSubmitting: false,
|
|
766
|
+
submitCount: resetOptions?.keepSubmitCount === true ? state.submitCount : 0,
|
|
767
|
+
isValidating: emptyBools,
|
|
768
|
+
isValidatingForm: false
|
|
769
|
+
};
|
|
770
|
+
});
|
|
771
|
+
},
|
|
772
|
+
resetField: (path) => store.setState((state) => ({
|
|
773
|
+
...state,
|
|
774
|
+
values: setAtPath(
|
|
775
|
+
state.values,
|
|
776
|
+
path,
|
|
777
|
+
getAtPath(state.initialValues, path)
|
|
778
|
+
),
|
|
779
|
+
// A runtime "" path scopes the whole form (the imperative surface is
|
|
780
|
+
// string-typed even though FieldPath excludes ""). Server entries
|
|
781
|
+
// follow the setValue release contract (releaseSpine).
|
|
782
|
+
...errorChannels(state, {
|
|
783
|
+
schemaErrors: path === "" ? emptyErrors : omitScope(state.schemaErrors, [path]),
|
|
784
|
+
serverErrors: releaseSpine(state.serverErrors, path)
|
|
785
|
+
}),
|
|
786
|
+
touched: path === "" ? emptyBools : omitScope(state.touched, [path]),
|
|
787
|
+
isValidating: path === "" ? emptyBools : omitScope(state.isValidating, [path])
|
|
788
|
+
})),
|
|
789
|
+
validate,
|
|
790
|
+
validateField,
|
|
791
|
+
validateFields,
|
|
792
|
+
validateAsync: validateAsyncOnForm,
|
|
793
|
+
validateFieldAsync,
|
|
794
|
+
validateFieldsAsync,
|
|
795
|
+
submit,
|
|
796
|
+
arrayPush: (path, item) => applyArrayOp(path, (arr) => [...arr, item], identityMapper),
|
|
797
|
+
arrayRemove: (path, index) => applyArrayOp(
|
|
798
|
+
path,
|
|
799
|
+
(arr) => [...arr.slice(0, index), ...arr.slice(index + 1)],
|
|
800
|
+
removeAt(index),
|
|
801
|
+
(len) => Number.isInteger(index) && index >= 0 && index < len
|
|
802
|
+
),
|
|
803
|
+
arrayInsert: (path, index, item) => applyArrayOp(
|
|
804
|
+
path,
|
|
805
|
+
(arr) => [...arr.slice(0, index), item, ...arr.slice(index)],
|
|
806
|
+
insertAt(index),
|
|
807
|
+
(len) => Number.isInteger(index) && index >= 0 && index <= len
|
|
808
|
+
),
|
|
809
|
+
arrayMove: (path, from, to) => applyArrayOp(
|
|
810
|
+
path,
|
|
811
|
+
(arr) => {
|
|
812
|
+
const without = [...arr.slice(0, from), ...arr.slice(from + 1)];
|
|
813
|
+
return [...without.slice(0, to), arr[from], ...without.slice(to)];
|
|
814
|
+
},
|
|
815
|
+
moveFromTo(from, to),
|
|
816
|
+
(len) => Number.isInteger(from) && Number.isInteger(to) && from >= 0 && from < len && to >= 0 && to < len
|
|
817
|
+
),
|
|
818
|
+
arraySwap: (path, a, b) => applyArrayOp(
|
|
819
|
+
path,
|
|
820
|
+
(arr) => arr.map((v, i) => i === a ? arr[b] : i === b ? arr[a] : v),
|
|
821
|
+
swapIndices(a, b),
|
|
822
|
+
(len) => Number.isInteger(a) && Number.isInteger(b) && a >= 0 && a < len && b >= 0 && b < len
|
|
823
|
+
),
|
|
824
|
+
handleSubmit: (onValid, onInvalid, opts) => async (event) => {
|
|
825
|
+
event?.preventDefault();
|
|
826
|
+
return submit(onValid, onInvalid, opts);
|
|
827
|
+
},
|
|
828
|
+
diff: () => {
|
|
829
|
+
const state = store.getState();
|
|
830
|
+
return Object.fromEntries(
|
|
831
|
+
dirtyPathsOf(state.values, state.initialValues, "").map((path) => [
|
|
832
|
+
path,
|
|
833
|
+
getAtPath(state.values, path)
|
|
834
|
+
])
|
|
835
|
+
);
|
|
836
|
+
},
|
|
837
|
+
dirtyFields: () => {
|
|
838
|
+
const state = store.getState();
|
|
839
|
+
return dirtyPathsOf(state.values, state.initialValues, "");
|
|
840
|
+
},
|
|
841
|
+
snapshot: () => store.getState(),
|
|
842
|
+
restore: (snap) => store.setState(() => {
|
|
843
|
+
const schemaErrors = snap.schemaErrors ?? emptyErrors;
|
|
844
|
+
const serverErrors = snap.serverErrors ?? emptyErrors;
|
|
845
|
+
return {
|
|
846
|
+
...snap,
|
|
847
|
+
schemaErrors,
|
|
848
|
+
serverErrors,
|
|
849
|
+
errors: reuseErrorRefs(
|
|
850
|
+
mergeErrorChannels(schemaErrors, serverErrors),
|
|
851
|
+
snap.errors ?? emptyErrors
|
|
852
|
+
)
|
|
853
|
+
};
|
|
854
|
+
})
|
|
855
|
+
});
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
// src/core/mode.ts
|
|
859
|
+
var shouldValidateOn = (trigger, mode, reValidateMode, submitAttempted, touched = false) => {
|
|
860
|
+
const effective = submitAttempted ? reValidateMode : mode;
|
|
861
|
+
switch (effective) {
|
|
862
|
+
case "onChange":
|
|
863
|
+
return trigger === "change";
|
|
864
|
+
case "onBlur":
|
|
865
|
+
return trigger === "blur";
|
|
866
|
+
case "onSubmit":
|
|
867
|
+
return false;
|
|
868
|
+
case "onTouched":
|
|
869
|
+
return trigger === "blur" || touched;
|
|
870
|
+
case "all":
|
|
871
|
+
return true;
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
var useForm = (schema, options) => {
|
|
875
|
+
const formRef = react.useRef(null);
|
|
876
|
+
if (formRef.current === null) {
|
|
877
|
+
formRef.current = createForm(schema, options);
|
|
878
|
+
}
|
|
879
|
+
const form = formRef.current;
|
|
880
|
+
const warnedRef = react.useRef(false);
|
|
881
|
+
react.useEffect(() => {
|
|
882
|
+
if (!warnedRef.current && form.schema !== schema) {
|
|
883
|
+
warnedRef.current = true;
|
|
884
|
+
console.warn(
|
|
885
|
+
"[formstand] useForm received a different schema reference after the first render; schema and options changes are ignored for the life of the component. If the schema is defined inline this is harmless (but consider hoisting it to module scope). For initial values that arrive later (e.g. from a fetch), call form.adoptValues(values) or form.reset(values) instead."
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
}, [form, schema]);
|
|
889
|
+
return form;
|
|
890
|
+
};
|
|
891
|
+
var createFormContext = () => {
|
|
892
|
+
const Context = react.createContext(null);
|
|
893
|
+
const Provider = ({ form, children }) => /* @__PURE__ */ jsxRuntime.jsx(Context.Provider, { value: form, children });
|
|
894
|
+
const useFormContext = () => {
|
|
895
|
+
const ctx = react.useContext(Context);
|
|
896
|
+
if (ctx === null) {
|
|
897
|
+
throw new Error(
|
|
898
|
+
"useFormContext must be used inside the matching <Provider>"
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
return ctx;
|
|
902
|
+
};
|
|
903
|
+
return { Provider, useFormContext };
|
|
904
|
+
};
|
|
905
|
+
function useFormSelector(form, selector) {
|
|
906
|
+
return react$1.useStore(form.store, selector);
|
|
907
|
+
}
|
|
908
|
+
function useFormSelectorShallow(form, selector) {
|
|
909
|
+
return react$1.useStore(form.store, shallow.useShallow(selector));
|
|
910
|
+
}
|
|
911
|
+
var useFormState = useFormSelector;
|
|
912
|
+
var useFormStateShallow = useFormSelectorShallow;
|
|
913
|
+
var useFormError = (form) => react$1.useStore(
|
|
914
|
+
form.store,
|
|
915
|
+
shallow.useShallow((state) => state.errors[""])
|
|
916
|
+
);
|
|
917
|
+
var useIsDirty = (form) => react$1.useStore(
|
|
918
|
+
form.store,
|
|
919
|
+
(state) => isFieldDirty(state.values, state.initialValues)
|
|
920
|
+
);
|
|
921
|
+
var useIsValid = (form) => react$1.useStore(form.store, (state) => {
|
|
922
|
+
for (const k in state.errors) {
|
|
923
|
+
const errs = state.errors[k];
|
|
924
|
+
if (errs !== void 0 && errs.length > 0) return false;
|
|
925
|
+
}
|
|
926
|
+
return true;
|
|
927
|
+
});
|
|
928
|
+
var useIsSubmitting = (form) => react$1.useStore(form.store, (state) => state.isSubmitting);
|
|
929
|
+
var useSubmitCount = (form) => react$1.useStore(form.store, (state) => state.submitCount);
|
|
930
|
+
function useField(form, pathArg, options) {
|
|
931
|
+
const slice = react$1.useStore(
|
|
932
|
+
form.store,
|
|
933
|
+
shallow.useShallow((state) => {
|
|
934
|
+
const p = typeof pathArg === "function" ? pathArg(state) : pathArg;
|
|
935
|
+
const value = getAtPath(state.values, p);
|
|
936
|
+
const initialValue2 = getAtPath(state.initialValues, p);
|
|
937
|
+
return {
|
|
938
|
+
path: p,
|
|
939
|
+
value,
|
|
940
|
+
initialValue: initialValue2,
|
|
941
|
+
error: state.errors[p],
|
|
942
|
+
touched: state.touched[p] ?? false,
|
|
943
|
+
dirty: isFieldDirty(value, initialValue2),
|
|
944
|
+
isValidating: state.isValidating[p] ?? false
|
|
945
|
+
};
|
|
946
|
+
})
|
|
947
|
+
);
|
|
948
|
+
const path = slice.path;
|
|
949
|
+
const formSchema = form.schema;
|
|
950
|
+
const initialValue = slice.initialValue;
|
|
951
|
+
const emptyValue = react.useMemo(() => {
|
|
952
|
+
if (formSchema !== void 0) {
|
|
953
|
+
const sub = fieldSchemaAtPath(formSchema, path);
|
|
954
|
+
if (sub !== null) return emptyValueForSchema(sub);
|
|
955
|
+
}
|
|
956
|
+
return initialValue === null ? null : void 0;
|
|
957
|
+
}, [formSchema, path, initialValue]);
|
|
958
|
+
const debounceMs = options?.debounceMs;
|
|
959
|
+
const debounceTimerRef = react.useRef(null);
|
|
960
|
+
react.useEffect(() => {
|
|
961
|
+
return () => {
|
|
962
|
+
if (debounceTimerRef.current !== null) {
|
|
963
|
+
clearTimeout(debounceTimerRef.current);
|
|
964
|
+
debounceTimerRef.current = null;
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
}, [form, path, debounceMs]);
|
|
968
|
+
const triggerValidate = react.useCallback(() => {
|
|
969
|
+
if (debounceMs !== void 0 && debounceMs > 0) {
|
|
970
|
+
if (debounceTimerRef.current !== null) {
|
|
971
|
+
clearTimeout(debounceTimerRef.current);
|
|
972
|
+
}
|
|
973
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
974
|
+
void form.validateFieldAsync(path);
|
|
975
|
+
debounceTimerRef.current = null;
|
|
976
|
+
}, debounceMs);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
try {
|
|
980
|
+
form.validateField(path);
|
|
981
|
+
} catch (e) {
|
|
982
|
+
if (isAsyncRequiredError(e)) {
|
|
983
|
+
void form.validateFieldAsync(path);
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
throw e;
|
|
987
|
+
}
|
|
988
|
+
}, [form, path, debounceMs]);
|
|
989
|
+
const setValue = react.useCallback(
|
|
990
|
+
(value) => {
|
|
991
|
+
form.setValue(path, value);
|
|
992
|
+
const state = form.store.getState();
|
|
993
|
+
if (shouldValidateOn(
|
|
994
|
+
"change",
|
|
995
|
+
state.mode,
|
|
996
|
+
state.reValidateMode,
|
|
997
|
+
state.submitCount > 0,
|
|
998
|
+
state.touched[path] ?? false
|
|
999
|
+
)) {
|
|
1000
|
+
triggerValidate();
|
|
1001
|
+
}
|
|
1002
|
+
},
|
|
1003
|
+
[form, path, triggerValidate]
|
|
1004
|
+
);
|
|
1005
|
+
const setTouched = react.useCallback(
|
|
1006
|
+
(touched) => form.setTouched(path, touched),
|
|
1007
|
+
[form, path]
|
|
1008
|
+
);
|
|
1009
|
+
const setError = react.useCallback(
|
|
1010
|
+
(errors) => form.setError(path, errors),
|
|
1011
|
+
[form, path]
|
|
1012
|
+
);
|
|
1013
|
+
const clearError = react.useCallback(
|
|
1014
|
+
() => form.clearErrors(path),
|
|
1015
|
+
[form, path]
|
|
1016
|
+
);
|
|
1017
|
+
const validate = react.useCallback(() => form.validateField(path), [form, path]);
|
|
1018
|
+
const validateAsync2 = react.useCallback(
|
|
1019
|
+
() => form.validateFieldAsync(path),
|
|
1020
|
+
[form, path]
|
|
1021
|
+
);
|
|
1022
|
+
const onBlur = react.useCallback(() => {
|
|
1023
|
+
form.setTouched(path, true);
|
|
1024
|
+
const state = form.store.getState();
|
|
1025
|
+
if (shouldValidateOn(
|
|
1026
|
+
"blur",
|
|
1027
|
+
state.mode,
|
|
1028
|
+
state.reValidateMode,
|
|
1029
|
+
state.submitCount > 0,
|
|
1030
|
+
true
|
|
1031
|
+
)) {
|
|
1032
|
+
triggerValidate();
|
|
1033
|
+
}
|
|
1034
|
+
}, [form, path, triggerValidate]);
|
|
1035
|
+
return react.useMemo(
|
|
1036
|
+
() => ({
|
|
1037
|
+
path: slice.path,
|
|
1038
|
+
value: slice.value,
|
|
1039
|
+
initialValue: slice.initialValue,
|
|
1040
|
+
emptyValue,
|
|
1041
|
+
error: slice.error,
|
|
1042
|
+
touched: slice.touched,
|
|
1043
|
+
dirty: slice.dirty,
|
|
1044
|
+
isValidating: slice.isValidating,
|
|
1045
|
+
setValue,
|
|
1046
|
+
setTouched,
|
|
1047
|
+
setError,
|
|
1048
|
+
clearError,
|
|
1049
|
+
validate,
|
|
1050
|
+
validateAsync: validateAsync2,
|
|
1051
|
+
onBlur
|
|
1052
|
+
}),
|
|
1053
|
+
[slice, emptyValue, setValue, setTouched, setError, clearError, validate, validateAsync2, onBlur]
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// src/react/inputProps.ts
|
|
1058
|
+
var ariaInvalid = (field) => field.error !== void 0 && field.error.length > 0 ? true : void 0;
|
|
1059
|
+
var numberToInputText = (value) => value === void 0 || value === null || Number.isNaN(value) ? "" : String(value);
|
|
1060
|
+
var parseNumberText = (text) => {
|
|
1061
|
+
if (text.trim() === "") return { kind: "empty" };
|
|
1062
|
+
const parsed = Number(text);
|
|
1063
|
+
return Number.isFinite(parsed) ? { kind: "number", value: parsed } : { kind: "invalid" };
|
|
1064
|
+
};
|
|
1065
|
+
var textInputProps = (field) => ({
|
|
1066
|
+
name: field.path,
|
|
1067
|
+
value: field.value ?? "",
|
|
1068
|
+
"aria-invalid": ariaInvalid(field),
|
|
1069
|
+
onChange: (e) => {
|
|
1070
|
+
const text = e.target.value;
|
|
1071
|
+
field.setValue(
|
|
1072
|
+
text === "" && field.emptyValue === null ? null : text
|
|
1073
|
+
);
|
|
1074
|
+
},
|
|
1075
|
+
onBlur: field.onBlur
|
|
1076
|
+
});
|
|
1077
|
+
var numberInputProps = (field) => ({
|
|
1078
|
+
type: "number",
|
|
1079
|
+
name: field.path,
|
|
1080
|
+
value: numberToInputText(field.value),
|
|
1081
|
+
"aria-invalid": ariaInvalid(field),
|
|
1082
|
+
onChange: (e) => {
|
|
1083
|
+
const parsed = parseNumberText(e.target.value);
|
|
1084
|
+
field.setValue(
|
|
1085
|
+
parsed.kind === "number" ? parsed.value : field.emptyValue
|
|
1086
|
+
);
|
|
1087
|
+
},
|
|
1088
|
+
onBlur: field.onBlur
|
|
1089
|
+
});
|
|
1090
|
+
var checkboxProps = (field) => ({
|
|
1091
|
+
type: "checkbox",
|
|
1092
|
+
name: field.path,
|
|
1093
|
+
checked: field.value ?? false,
|
|
1094
|
+
"aria-invalid": ariaInvalid(field),
|
|
1095
|
+
onChange: (e) => field.setValue(e.target.checked),
|
|
1096
|
+
onBlur: field.onBlur
|
|
1097
|
+
});
|
|
1098
|
+
var selectProps = (field) => ({
|
|
1099
|
+
name: field.path,
|
|
1100
|
+
value: field.value ?? "",
|
|
1101
|
+
"aria-invalid": ariaInvalid(field),
|
|
1102
|
+
onChange: (e) => {
|
|
1103
|
+
const v = e.target.value;
|
|
1104
|
+
field.setValue(
|
|
1105
|
+
v === "" && field.emptyValue === null ? null : v
|
|
1106
|
+
);
|
|
1107
|
+
},
|
|
1108
|
+
onBlur: field.onBlur
|
|
1109
|
+
});
|
|
1110
|
+
var hasError = (error) => error !== void 0 && error.length > 0;
|
|
1111
|
+
var ErrorText = ({
|
|
1112
|
+
id,
|
|
1113
|
+
error
|
|
1114
|
+
}) => hasError(error) ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "zf-error", id, role: "alert", children: error?.[0] }) : null;
|
|
1115
|
+
var TextField = ({
|
|
1116
|
+
form,
|
|
1117
|
+
path,
|
|
1118
|
+
label,
|
|
1119
|
+
placeholder,
|
|
1120
|
+
type = "text",
|
|
1121
|
+
autoComplete,
|
|
1122
|
+
ref
|
|
1123
|
+
}) => {
|
|
1124
|
+
const id = react.useId();
|
|
1125
|
+
const errorId = `${id}-error`;
|
|
1126
|
+
const field = useField(form, path);
|
|
1127
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "zf-field", children: [
|
|
1128
|
+
label !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: id, className: "zf-label", children: label }) : null,
|
|
1129
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1130
|
+
"input",
|
|
1131
|
+
{
|
|
1132
|
+
id,
|
|
1133
|
+
ref,
|
|
1134
|
+
...textInputProps(field),
|
|
1135
|
+
type,
|
|
1136
|
+
placeholder,
|
|
1137
|
+
autoComplete,
|
|
1138
|
+
"aria-describedby": hasError(field.error) ? errorId : void 0
|
|
1139
|
+
}
|
|
1140
|
+
),
|
|
1141
|
+
/* @__PURE__ */ jsxRuntime.jsx(ErrorText, { id: errorId, error: field.error })
|
|
1142
|
+
] });
|
|
1143
|
+
};
|
|
1144
|
+
var IDLE_EDIT = { raw: null, pushed: void 0 };
|
|
1145
|
+
var useNumberInput = (field) => {
|
|
1146
|
+
const [edit, setEdit] = react.useState(IDLE_EDIT);
|
|
1147
|
+
const externallyChanged = edit.raw !== null && !Object.is(field.value, edit.pushed);
|
|
1148
|
+
if (externallyChanged) {
|
|
1149
|
+
setEdit(IDLE_EDIT);
|
|
1150
|
+
}
|
|
1151
|
+
const raw = externallyChanged ? null : edit.raw;
|
|
1152
|
+
return {
|
|
1153
|
+
name: field.path,
|
|
1154
|
+
value: raw ?? numberToInputText(field.value),
|
|
1155
|
+
inputMode: "decimal",
|
|
1156
|
+
"aria-invalid": hasError(field.error) ? true : void 0,
|
|
1157
|
+
onChange: (e) => {
|
|
1158
|
+
const text = e.target.value;
|
|
1159
|
+
const parsed = parseNumberText(text);
|
|
1160
|
+
switch (parsed.kind) {
|
|
1161
|
+
case "empty": {
|
|
1162
|
+
const empty = field.emptyValue;
|
|
1163
|
+
setEdit({ raw: text, pushed: empty });
|
|
1164
|
+
field.setValue(empty);
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
case "number":
|
|
1168
|
+
setEdit({ raw: text, pushed: parsed.value });
|
|
1169
|
+
field.setValue(parsed.value);
|
|
1170
|
+
return;
|
|
1171
|
+
case "invalid":
|
|
1172
|
+
setEdit({ raw: text, pushed: field.value });
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
},
|
|
1176
|
+
onBlur: () => {
|
|
1177
|
+
setEdit(IDLE_EDIT);
|
|
1178
|
+
field.onBlur();
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
};
|
|
1182
|
+
var NumberField = ({
|
|
1183
|
+
form,
|
|
1184
|
+
path,
|
|
1185
|
+
label,
|
|
1186
|
+
placeholder,
|
|
1187
|
+
ref
|
|
1188
|
+
}) => {
|
|
1189
|
+
const id = react.useId();
|
|
1190
|
+
const errorId = `${id}-error`;
|
|
1191
|
+
const field = useField(form, path);
|
|
1192
|
+
const binding = useNumberInput(field);
|
|
1193
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "zf-field", children: [
|
|
1194
|
+
label !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: id, className: "zf-label", children: label }) : null,
|
|
1195
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1196
|
+
"input",
|
|
1197
|
+
{
|
|
1198
|
+
id,
|
|
1199
|
+
ref,
|
|
1200
|
+
type: "text",
|
|
1201
|
+
...binding,
|
|
1202
|
+
placeholder,
|
|
1203
|
+
"aria-describedby": hasError(field.error) ? errorId : void 0
|
|
1204
|
+
}
|
|
1205
|
+
),
|
|
1206
|
+
/* @__PURE__ */ jsxRuntime.jsx(ErrorText, { id: errorId, error: field.error })
|
|
1207
|
+
] });
|
|
1208
|
+
};
|
|
1209
|
+
var CheckboxField = ({ form, path, label, ref }) => {
|
|
1210
|
+
const id = react.useId();
|
|
1211
|
+
const errorId = `${id}-error`;
|
|
1212
|
+
const field = useField(form, path);
|
|
1213
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "zf-field", children: [
|
|
1214
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { htmlFor: id, className: "zf-label", children: [
|
|
1215
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1216
|
+
"input",
|
|
1217
|
+
{
|
|
1218
|
+
id,
|
|
1219
|
+
ref,
|
|
1220
|
+
...checkboxProps(field),
|
|
1221
|
+
"aria-describedby": hasError(field.error) ? errorId : void 0
|
|
1222
|
+
}
|
|
1223
|
+
),
|
|
1224
|
+
" ",
|
|
1225
|
+
label
|
|
1226
|
+
] }),
|
|
1227
|
+
/* @__PURE__ */ jsxRuntime.jsx(ErrorText, { id: errorId, error: field.error })
|
|
1228
|
+
] });
|
|
1229
|
+
};
|
|
1230
|
+
var SelectField = ({
|
|
1231
|
+
form,
|
|
1232
|
+
path,
|
|
1233
|
+
label,
|
|
1234
|
+
options,
|
|
1235
|
+
placeholder,
|
|
1236
|
+
ref
|
|
1237
|
+
}) => {
|
|
1238
|
+
const id = react.useId();
|
|
1239
|
+
const errorId = `${id}-error`;
|
|
1240
|
+
const field = useField(form, path);
|
|
1241
|
+
const showEmptyOption = field.value === void 0 || field.value === null || placeholder !== void 0;
|
|
1242
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "zf-field", children: [
|
|
1243
|
+
label !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: id, className: "zf-label", children: label }) : null,
|
|
1244
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1245
|
+
"select",
|
|
1246
|
+
{
|
|
1247
|
+
id,
|
|
1248
|
+
ref,
|
|
1249
|
+
...selectProps(field),
|
|
1250
|
+
"aria-describedby": hasError(field.error) ? errorId : void 0,
|
|
1251
|
+
children: [
|
|
1252
|
+
showEmptyOption ? /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", disabled: true, children: placeholder ?? "" }) : null,
|
|
1253
|
+
options.map((opt) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: opt.value, children: opt.label }, opt.value))
|
|
1254
|
+
]
|
|
1255
|
+
}
|
|
1256
|
+
),
|
|
1257
|
+
/* @__PURE__ */ jsxRuntime.jsx(ErrorText, { id: errorId, error: field.error })
|
|
1258
|
+
] });
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
// src/react/focusError.ts
|
|
1262
|
+
var focusFirstError = (errors, root) => {
|
|
1263
|
+
const scope = root ?? document;
|
|
1264
|
+
const erroredPaths = Object.keys(errors).filter(
|
|
1265
|
+
(k) => k !== "" && (errors[k]?.length ?? 0) > 0
|
|
1266
|
+
);
|
|
1267
|
+
const hasRootError = (errors[""]?.length ?? 0) > 0;
|
|
1268
|
+
const controls = [
|
|
1269
|
+
...scope.querySelectorAll(
|
|
1270
|
+
"input[name], select[name], textarea[name]"
|
|
1271
|
+
)
|
|
1272
|
+
].filter((el) => !el.matches(':disabled, [type="hidden"]'));
|
|
1273
|
+
const fieldMatch = controls.find((el) => {
|
|
1274
|
+
const name = el.getAttribute("name");
|
|
1275
|
+
return name !== null && erroredPaths.some((k) => isPathOrChild(name, k));
|
|
1276
|
+
});
|
|
1277
|
+
const target = fieldMatch ?? (hasRootError ? controls[0] : void 0);
|
|
1278
|
+
target?.focus();
|
|
1279
|
+
return target !== void 0;
|
|
1280
|
+
};
|
|
1281
|
+
var EMPTY_ID_STATE = { items: [], ids: [], counter: 0 };
|
|
1282
|
+
var EMPTY_ITEMS = [];
|
|
1283
|
+
var reconcileIds = (prev, nextItems) => {
|
|
1284
|
+
if (prev.items === nextItems) return prev;
|
|
1285
|
+
if (prev.items.length === nextItems.length && prev.items.every((item, i) => Object.is(item, nextItems[i]))) {
|
|
1286
|
+
return prev;
|
|
1287
|
+
}
|
|
1288
|
+
const buckets = prev.items.reduce((acc, item, i) => {
|
|
1289
|
+
const bucket = acc.get(item);
|
|
1290
|
+
if (bucket === void 0) {
|
|
1291
|
+
acc.set(item, [i]);
|
|
1292
|
+
} else {
|
|
1293
|
+
bucket.push(i);
|
|
1294
|
+
}
|
|
1295
|
+
return acc;
|
|
1296
|
+
}, /* @__PURE__ */ new Map());
|
|
1297
|
+
const matchedIds = nextItems.map((item) => {
|
|
1298
|
+
const prevIndex = buckets.get(item)?.shift();
|
|
1299
|
+
return prevIndex === void 0 ? null : prev.ids[prevIndex] ?? null;
|
|
1300
|
+
});
|
|
1301
|
+
const leftover = [...buckets.values()].flat().sort((a, b) => a - b).map((i) => prev.ids[i]).filter((id) => id !== void 0);
|
|
1302
|
+
const ordinals = matchedIds.reduce((acc, id) => {
|
|
1303
|
+
const count = acc.length === 0 ? 0 : acc[acc.length - 1] ?? 0;
|
|
1304
|
+
acc.push(id === null ? count + 1 : count);
|
|
1305
|
+
return acc;
|
|
1306
|
+
}, []);
|
|
1307
|
+
const unmatched = ordinals.length === 0 ? 0 : ordinals[ordinals.length - 1] ?? 0;
|
|
1308
|
+
const ids = matchedIds.map((id, i) => {
|
|
1309
|
+
if (id !== null) return id;
|
|
1310
|
+
const ordinal = ordinals[i] ?? 0;
|
|
1311
|
+
return leftover[ordinal - 1] ?? `__zfa_${prev.counter + (ordinal - leftover.length)}`;
|
|
1312
|
+
});
|
|
1313
|
+
return {
|
|
1314
|
+
items: nextItems,
|
|
1315
|
+
ids,
|
|
1316
|
+
counter: prev.counter + Math.max(0, unmatched - leftover.length)
|
|
1317
|
+
};
|
|
1318
|
+
};
|
|
1319
|
+
var useFieldArray = (form, pathArg) => {
|
|
1320
|
+
const path = react$1.useStore(
|
|
1321
|
+
form.store,
|
|
1322
|
+
(state) => typeof pathArg === "function" ? pathArg(state) : pathArg
|
|
1323
|
+
);
|
|
1324
|
+
const slice = react$1.useStore(
|
|
1325
|
+
form.store,
|
|
1326
|
+
shallow.useShallow((state) => {
|
|
1327
|
+
const value = getAtPath(state.values, path);
|
|
1328
|
+
return {
|
|
1329
|
+
items: Array.isArray(value) ? value : EMPTY_ITEMS,
|
|
1330
|
+
error: state.errors[path]
|
|
1331
|
+
};
|
|
1332
|
+
})
|
|
1333
|
+
);
|
|
1334
|
+
const items = slice.items;
|
|
1335
|
+
const error = slice.error;
|
|
1336
|
+
const [idEntry, setIdEntry] = react.useState({ form, path, state: EMPTY_ID_STATE });
|
|
1337
|
+
const base = idEntry.form === form && idEntry.path === path ? idEntry.state : { ...EMPTY_ID_STATE, counter: idEntry.state.counter };
|
|
1338
|
+
const nextIdState = reconcileIds(base, items);
|
|
1339
|
+
if (idEntry.form !== form || idEntry.path !== path || idEntry.state !== nextIdState) {
|
|
1340
|
+
setIdEntry({ form, path, state: nextIdState });
|
|
1341
|
+
}
|
|
1342
|
+
const ids = nextIdState.ids;
|
|
1343
|
+
const push = react.useCallback(
|
|
1344
|
+
(item) => form.arrayPush(path, item),
|
|
1345
|
+
[form, path]
|
|
1346
|
+
);
|
|
1347
|
+
const remove = react.useCallback(
|
|
1348
|
+
(index) => form.arrayRemove(path, index),
|
|
1349
|
+
[form, path]
|
|
1350
|
+
);
|
|
1351
|
+
const insert = react.useCallback(
|
|
1352
|
+
(index, item) => form.arrayInsert(path, index, item),
|
|
1353
|
+
[form, path]
|
|
1354
|
+
);
|
|
1355
|
+
const move = react.useCallback(
|
|
1356
|
+
(from, to) => form.arrayMove(path, from, to),
|
|
1357
|
+
[form, path]
|
|
1358
|
+
);
|
|
1359
|
+
const swap = react.useCallback(
|
|
1360
|
+
(a, b) => form.arraySwap(path, a, b),
|
|
1361
|
+
[form, path]
|
|
1362
|
+
);
|
|
1363
|
+
const fields = react.useMemo(
|
|
1364
|
+
() => items.map((value, index) => ({
|
|
1365
|
+
id: ids[index] ?? `__zfa_fallback_${index}`,
|
|
1366
|
+
value
|
|
1367
|
+
})),
|
|
1368
|
+
[items, ids]
|
|
1369
|
+
);
|
|
1370
|
+
return react.useMemo(
|
|
1371
|
+
() => ({
|
|
1372
|
+
fields,
|
|
1373
|
+
items,
|
|
1374
|
+
length: items.length,
|
|
1375
|
+
error,
|
|
1376
|
+
push,
|
|
1377
|
+
remove,
|
|
1378
|
+
insert,
|
|
1379
|
+
move,
|
|
1380
|
+
swap
|
|
1381
|
+
}),
|
|
1382
|
+
[fields, items, error, push, remove, insert, move, swap]
|
|
1383
|
+
);
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
exports.CheckboxField = CheckboxField;
|
|
1387
|
+
exports.NumberField = NumberField;
|
|
1388
|
+
exports.SelectField = SelectField;
|
|
1389
|
+
exports.TextField = TextField;
|
|
1390
|
+
exports.checkboxProps = checkboxProps;
|
|
1391
|
+
exports.createForm = createForm;
|
|
1392
|
+
exports.createFormContext = createFormContext;
|
|
1393
|
+
exports.flattenIssues = flattenIssues;
|
|
1394
|
+
exports.focusFirstError = focusFirstError;
|
|
1395
|
+
exports.getAtPath = getAtPath;
|
|
1396
|
+
exports.isAsyncRequiredError = isAsyncRequiredError;
|
|
1397
|
+
exports.numberInputProps = numberInputProps;
|
|
1398
|
+
exports.parsePath = parsePath;
|
|
1399
|
+
exports.selectProps = selectProps;
|
|
1400
|
+
exports.setAtPath = setAtPath;
|
|
1401
|
+
exports.shouldValidateOn = shouldValidateOn;
|
|
1402
|
+
exports.textInputProps = textInputProps;
|
|
1403
|
+
exports.useField = useField;
|
|
1404
|
+
exports.useFieldArray = useFieldArray;
|
|
1405
|
+
exports.useForm = useForm;
|
|
1406
|
+
exports.useFormError = useFormError;
|
|
1407
|
+
exports.useFormSelector = useFormSelector;
|
|
1408
|
+
exports.useFormSelectorShallow = useFormSelectorShallow;
|
|
1409
|
+
exports.useFormState = useFormState;
|
|
1410
|
+
exports.useFormStateShallow = useFormStateShallow;
|
|
1411
|
+
exports.useIsDirty = useIsDirty;
|
|
1412
|
+
exports.useIsSubmitting = useIsSubmitting;
|
|
1413
|
+
exports.useIsValid = useIsValid;
|
|
1414
|
+
exports.useSubmitCount = useSubmitCount;
|
|
1415
|
+
exports.validateAsync = validateAsync;
|
|
1416
|
+
exports.validateSync = validateSync;
|
|
1417
|
+
//# sourceMappingURL=index.cjs.map
|
|
1418
|
+
//# sourceMappingURL=index.cjs.map
|