@homebound/form-state 2.33.0 → 3.0.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.
Files changed (57) hide show
  1. package/dist/index.d.ts +437 -9
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +1109 -14
  4. package/dist/index.js.map +1 -0
  5. package/package.json +32 -35
  6. package/dist/FormStateApp.d.ts +0 -5
  7. package/dist/FormStateApp.js +0 -54
  8. package/dist/FormStateApp.test.d.ts +0 -1
  9. package/dist/FormStateApp.test.js +0 -26
  10. package/dist/config.d.ts +0 -78
  11. package/dist/config.js +0 -2
  12. package/dist/configBuilders.d.ts +0 -67
  13. package/dist/configBuilders.js +0 -115
  14. package/dist/configBuilders.test.d.ts +0 -1
  15. package/dist/configBuilders.test.js +0 -65
  16. package/dist/fields/deepEquals.d.ts +0 -7
  17. package/dist/fields/deepEquals.js +0 -53
  18. package/dist/fields/fragmentField.d.ts +0 -4
  19. package/dist/fields/fragmentField.js +0 -58
  20. package/dist/fields/listField.d.ts +0 -11
  21. package/dist/fields/listField.js +0 -355
  22. package/dist/fields/objectField.d.ts +0 -63
  23. package/dist/fields/objectField.js +0 -277
  24. package/dist/fields/valueField.d.ts +0 -81
  25. package/dist/fields/valueField.js +0 -244
  26. package/dist/formState.stories.d.ts +0 -4
  27. package/dist/formState.stories.js +0 -12
  28. package/dist/formState.test.d.ts +0 -11
  29. package/dist/formState.test.js +0 -2109
  30. package/dist/formStateDomain.d.ts +0 -47
  31. package/dist/formStateDomain.js +0 -28
  32. package/dist/mobx-behavior.test.d.ts +0 -1
  33. package/dist/mobx-behavior.test.js +0 -51
  34. package/dist/proxies.d.ts +0 -5
  35. package/dist/proxies.js +0 -20
  36. package/dist/rules.d.ts +0 -10
  37. package/dist/rules.js +0 -11
  38. package/dist/setupTests.d.ts +0 -1
  39. package/dist/setupTests.js +0 -15
  40. package/dist/stackoverlfow.d.ts +0 -17
  41. package/dist/stackoverlfow.js +0 -17
  42. package/dist/temporal.d.ts +0 -6
  43. package/dist/temporal.js +0 -34
  44. package/dist/temporal.test.d.ts +0 -1
  45. package/dist/temporal.test.js +0 -97
  46. package/dist/useFormState.d.ts +0 -82
  47. package/dist/useFormState.js +0 -158
  48. package/dist/useFormState.test.d.ts +0 -1
  49. package/dist/useFormState.test.js +0 -787
  50. package/dist/useFormStates.d.ts +0 -68
  51. package/dist/useFormStates.js +0 -101
  52. package/dist/useFormStates.test.d.ts +0 -1
  53. package/dist/useFormStates.test.js +0 -223
  54. package/dist/utils.d.ts +0 -52
  55. package/dist/utils.js +0 -198
  56. package/dist/utils.test.d.ts +0 -1
  57. package/dist/utils.test.js +0 -102
package/dist/index.js CHANGED
@@ -1,14 +1,1109 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useFormStates = exports.useFormState = exports.required = exports.fragment = exports.createObjectState = exports.f = void 0;
4
- var configBuilders_1 = require("./configBuilders");
5
- Object.defineProperty(exports, "f", { enumerable: true, get: function () { return configBuilders_1.f; } });
6
- var objectField_1 = require("./fields/objectField");
7
- Object.defineProperty(exports, "createObjectState", { enumerable: true, get: function () { return objectField_1.createObjectState; } });
8
- Object.defineProperty(exports, "fragment", { enumerable: true, get: function () { return objectField_1.fragment; } });
9
- var rules_1 = require("./rules");
10
- Object.defineProperty(exports, "required", { enumerable: true, get: function () { return rules_1.required; } });
11
- var useFormState_1 = require("./useFormState");
12
- Object.defineProperty(exports, "useFormState", { enumerable: true, get: function () { return useFormState_1.useFormState; } });
13
- var useFormStates_1 = require("./useFormStates");
14
- Object.defineProperty(exports, "useFormStates", { enumerable: true, get: function () { return useFormStates_1.useFormStates; } });
1
+ import { action, computed, isObservable, makeAutoObservable, observable, reaction, toJS } from "mobx";
2
+ import { isPlainObject } from "is-plain-object";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import hash from "object-hash";
5
+ //#region src/proxies.ts
6
+ /**
7
+ * Creates a new combined object with keys in `overrides` taking precedence, and then
8
+ * any other keys falling back to `delegate`.
9
+ */
10
+ function newDelegateProxy(delegate, overrides) {
11
+ function pickTarget(key) {
12
+ return Reflect.has(overrides, key) ? overrides : delegate;
13
+ }
14
+ return new Proxy(delegate, {
15
+ get(object, key) {
16
+ return Reflect.get(pickTarget(key), key);
17
+ },
18
+ set(object, key, value) {
19
+ return Reflect.set(pickTarget(key), key, value);
20
+ }
21
+ });
22
+ }
23
+ //#endregion
24
+ //#region src/rules.ts
25
+ /** A rule that validates `value` is not `undefined`, `null`, or empty string. */
26
+ const required = action(({ value: v }) => {
27
+ const isEmptyString = typeof v === "string" ? v.trim() === "" : false;
28
+ return v !== void 0 && v !== null && !isEmptyString ? void 0 : "Required";
29
+ });
30
+ //#endregion
31
+ //#region src/temporal.ts
32
+ const temporalPlainDateTag = "Temporal.PlainDate";
33
+ const temporalZonedDateTimeTag = "Temporal.ZonedDateTime";
34
+ /** Detects the Temporal value types that form-state treats as builtins. */
35
+ function isSupportedTemporal(value) {
36
+ if (!value || typeof value !== "object") return false;
37
+ const record = value;
38
+ return isSupportedTemporalTag(record[Symbol.toStringTag]) && typeof record.equals === "function" && typeof record.toJSON === "function";
39
+ }
40
+ /** Compares supported Temporal values with Temporal's `.equals(...)` semantics. */
41
+ function areSupportedTemporalValuesEqual(a, b) {
42
+ const aIsTemporal = isSupportedTemporal(a);
43
+ const bIsTemporal = isSupportedTemporal(b);
44
+ if (!aIsTemporal && !bIsTemporal) return;
45
+ if (!aIsTemporal || !bIsTemporal || a[Symbol.toStringTag] !== b[Symbol.toStringTag]) return false;
46
+ return a.equals(b);
47
+ }
48
+ function isSupportedTemporalTag(value) {
49
+ return value === temporalPlainDateTag || value === temporalZonedDateTimeTag;
50
+ }
51
+ //#endregion
52
+ //#region src/fields/deepEquals.ts
53
+ /**
54
+ * Our own `deepEquals` because either `fast-deep-equals` or `dequal` or etc actually
55
+ * handle cyclic data structures, despite ChatGTP's assertions/hallucinations.
56
+ *
57
+ * Ported from https://github.com/KoryNunn/cyclic-deep-equal which is ISC.
58
+ */
59
+ function deepEquals(a, b, visited = /* @__PURE__ */ new Set()) {
60
+ const temporalEquals = areSupportedTemporalValuesEqual(a, b);
61
+ if (temporalEquals !== void 0) return temporalEquals;
62
+ const aType = typeof a;
63
+ if (aType !== typeof b) return false;
64
+ if (a == null || b == null || !(aType === "object" || aType === "function")) {
65
+ if (aType === "number" && isNaN(a) && isNaN(b)) return true;
66
+ return a === b;
67
+ }
68
+ if (hasToJSON$1(a) || hasToJSON$1(b)) return deepEquals(hasToJSON$1(a) ? a.toJSON() : a, hasToJSON$1(b) ? b.toJSON() : b, visited);
69
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
70
+ const aKeys = Object.keys(a), bKeys = Object.keys(b);
71
+ if (aKeys.length !== bKeys.length) return false;
72
+ let equal = true;
73
+ for (const key of aKeys) {
74
+ if (!(key in b)) {
75
+ equal = false;
76
+ break;
77
+ }
78
+ if (a[key] && a[key] instanceof Object) {
79
+ if (visited.has(a[key])) break;
80
+ visited.add(a[key]);
81
+ }
82
+ if (!deepEquals(a[key], b[key], visited)) {
83
+ equal = false;
84
+ break;
85
+ }
86
+ }
87
+ return equal;
88
+ }
89
+ function hasToJSON$1(o) {
90
+ return !!(o && typeof o === "object" && "toJSON" in o);
91
+ }
92
+ //#endregion
93
+ //#region src/useFormState.ts
94
+ let isAutoSaving$1 = false;
95
+ let pendingAutoSave = false;
96
+ /**
97
+ * Creates a formState instance for editing in a form.
98
+ */
99
+ function useFormState(opts) {
100
+ const { config, init, addRules, readOnly = false, loading, autoSave } = opts;
101
+ const autoSaveRef = useRef(autoSave);
102
+ autoSaveRef.current = autoSave;
103
+ const initRef = useRef(init);
104
+ initRef.current = init;
105
+ const firstRunRef = useRef(true);
106
+ const [firstInitValue] = useState(() => initValue(config, init));
107
+ const isWrappingMobxProxy = !isPlainObject(firstInitValue);
108
+ const dep = isInput(init) ? init.onlyOnce ? [] : makeArray(init.input) : isQuery(init) ? [init.query.data, init.query.loading] : [];
109
+ const form = useMemo(() => {
110
+ function maybeAutoSave() {
111
+ if (isAutoSaving$1 === "in-flight") pendingAutoSave = true;
112
+ else if (isAutoSaving$1 === "queued") return;
113
+ if (autoSaveRef.current && form.dirty && !isAutoSaving$1) {
114
+ if (!form.valid) {
115
+ console.debug("Skipping auto-save b/c form is invalid: ", form.errors);
116
+ return;
117
+ }
118
+ isAutoSaving$1 = "queued";
119
+ setTimeout(async () => {
120
+ try {
121
+ form._isAutoSaving = true;
122
+ const promise = autoSaveRef.current(form);
123
+ isAutoSaving$1 = "in-flight";
124
+ await promise;
125
+ } catch (e) {
126
+ throw e;
127
+ } finally {
128
+ isAutoSaving$1 = false;
129
+ form._isAutoSaving = false;
130
+ if (pendingAutoSave) {
131
+ pendingAutoSave = false;
132
+ setTimeout(maybeAutoSave, 0);
133
+ }
134
+ }
135
+ }, 0);
136
+ }
137
+ }
138
+ const value = firstRunRef.current ? firstInitValue : initValue(config, init);
139
+ const form = createObjectState(config, value, { maybeAutoSave });
140
+ form.update = function update(input) {
141
+ const init = initRef.current;
142
+ const updateInit = isInput(init) ? {
143
+ ...init,
144
+ input
145
+ } : isQuery(init) ? {
146
+ input,
147
+ map: init.map,
148
+ ifUndefined: init.ifUndefined
149
+ } : { input };
150
+ form.set(initValue(config, updateInit), { refreshing: true });
151
+ };
152
+ form.readOnly = readOnly;
153
+ setLoading(form, opts);
154
+ (addRules || (() => {}))(form);
155
+ firstRunRef.current = true;
156
+ return form;
157
+ }, [config, ...isWrappingMobxProxy ? dep : []]);
158
+ useEffect(() => {
159
+ if (firstRunRef.current || isWrappingMobxProxy) {
160
+ firstRunRef.current = false;
161
+ return;
162
+ }
163
+ setLoading(form, opts);
164
+ form.set(initValue(config, init), { refreshing: true });
165
+ }, [form, ...dep]);
166
+ useEffect(() => {
167
+ form.readOnly = readOnly;
168
+ if (loading !== void 0) form.loading = loading;
169
+ }, [
170
+ form,
171
+ readOnly,
172
+ loading
173
+ ]);
174
+ return form;
175
+ }
176
+ function setLoading(form, opts) {
177
+ const { loading, init } = opts;
178
+ if (loading !== void 0) form.loading = loading;
179
+ else if (isInput(init) && !init.ifUndefined) form.loading = init.input === void 0;
180
+ else if (isQuery(init)) form.loading = init.query.loading;
181
+ }
182
+ function makeArray(input) {
183
+ return Array.isArray(input) ? input : [input];
184
+ }
185
+ //#endregion
186
+ //#region src/utils.ts
187
+ function fail(message) {
188
+ throw new Error(message || "Failed");
189
+ }
190
+ function assertNever(x) {
191
+ throw new Error("Unexpected object: " + x);
192
+ }
193
+ /** Introspects the `init` prop to see if it has a `map` function/etc. and returns the form value. */
194
+ function initValue(config, init) {
195
+ let value;
196
+ if (isInput(init)) value = init.input ? init.map ? init.map(init.input) : init.input : init.ifUndefined;
197
+ else if (isQuery(init)) value = init.query.data ? init.map(init.query.data) : init.ifUndefined;
198
+ else if (init === void 0) {} else throw new Error("init must have an input or query key");
199
+ return pickFields(config, value ?? {});
200
+ }
201
+ function isInput(init) {
202
+ return !!init && typeof init === "object" && "input" in init;
203
+ }
204
+ function isQuery(init) {
205
+ return !!init && typeof init === "object" && "query" in init && "map" in init;
206
+ }
207
+ /**
208
+ * Picks ony fields out of `instance` that are in the `formConfig`.
209
+ *
210
+ * This is useful for creating a form input from a GraphQL query result,
211
+ * but ignoring extra data in the query that we don't want/need in the form.
212
+ *
213
+ * (Especially because the form.value will become our mutation's input type
214
+ * that goes on the wire, we don't want superfluous data in it.)
215
+ */
216
+ function pickFields(formConfig, instance) {
217
+ if (!isPlainObject(instance)) return instance;
218
+ return Object.fromEntries(Object.entries(formConfig).map(([key, _keyConfig]) => {
219
+ const keyConfig = _keyConfig;
220
+ const value = instance[key];
221
+ if (keyConfig.type === "object") {
222
+ if (value) return [key, pickFields(keyConfig.config, value)];
223
+ else return [key, value];
224
+ } else if (keyConfig.type === "list") {
225
+ if (isObservable(value)) return [key, value];
226
+ else if (value) return [key, value.map((u) => pickFields(keyConfig.config, u))];
227
+ else return [key, value];
228
+ } else if (keyConfig.type === "value") return [key, value];
229
+ else if (keyConfig.type === "fragment") return [key, value];
230
+ else return assertNever(keyConfig);
231
+ }));
232
+ }
233
+ function isNotUndefined(value) {
234
+ return value !== void 0;
235
+ }
236
+ function isEmpty(value) {
237
+ return value === void 0 || value === null || value === "";
238
+ }
239
+ /**
240
+ * An equals that does deep-ish equality.
241
+ *
242
+ * We only do non-identity equals for:
243
+ *
244
+ * - "plain" objects that have no custom prototype/i.e. are object literals
245
+ * - objects that implement `toJSON`
246
+ * - arrays
247
+ */
248
+ function areEqual(a, b, strictOrder) {
249
+ const temporalEquals = areSupportedTemporalValuesEqual(a, b);
250
+ if (temporalEquals !== void 0) return temporalEquals;
251
+ if (isPlainObject(a)) return deepEquals(toJS(a), toJS(b));
252
+ if (hasToJSON(a) || hasToJSON(b)) return deepEquals(hasToJSON(a) ? a.toJSON() : a, hasToJSON(b) ? b.toJSON() : b);
253
+ if (a && b && a instanceof Array && b instanceof Array) {
254
+ if (strictOrder !== false) return deepEquals(a, b);
255
+ if (a.length !== b.length) return false;
256
+ return a.every((a1) => b.some((b1) => areEqual(a1, b1)));
257
+ }
258
+ return a === b;
259
+ }
260
+ function hasToJSON(o) {
261
+ return !!(o && typeof o === "object" && "toJSON" in o);
262
+ }
263
+ /** Normalizes values into stable object-hash-safe data using our existing `toJSON` semantics. */
264
+ function normalizeHashValue(value, seen = /* @__PURE__ */ new WeakMap()) {
265
+ if (hasToJSON(value)) return normalizeHashValue(value.toJSON(), seen);
266
+ if (Array.isArray(value)) {
267
+ if (seen.has(value)) return seen.get(value);
268
+ const result = [];
269
+ seen.set(value, result);
270
+ value.forEach((entry) => result.push(normalizeHashValue(entry, seen)));
271
+ return result;
272
+ }
273
+ if (value && typeof value === "object" && isPlainObject(value)) {
274
+ if (seen.has(value)) return seen.get(value);
275
+ const result = {};
276
+ seen.set(value, result);
277
+ Object.entries(value).forEach(([key, entry]) => {
278
+ result[key] = normalizeHashValue(entry, seen);
279
+ });
280
+ return result;
281
+ }
282
+ return value;
283
+ }
284
+ /** Make a clone of `obj`, but only recurse into POJOs and Arrays...and stores. */
285
+ function deepClone(obj, map = /* @__PURE__ */ new WeakMap()) {
286
+ if (obj && typeof obj === "object" && (isPlainObject(obj) || Array.isArray(obj) || isObservable(obj))) {
287
+ if (map.has(obj)) return map.get(obj);
288
+ const result = Array.isArray(obj) ? [] : {};
289
+ map.set(obj, result);
290
+ Object.assign(result, ...getAllPropertyNames(obj).map((key) => ({ [key]: deepClone(obj[key], map) })));
291
+ return result;
292
+ } else return obj;
293
+ }
294
+ function groupBy(list, fn, valueFn) {
295
+ const result = /* @__PURE__ */ new Map();
296
+ list.forEach((o) => {
297
+ const group = fn(o);
298
+ if (!result.has(group)) result.set(group, []);
299
+ result.get(group).push(valueFn === void 0 ? o : valueFn(o));
300
+ });
301
+ return result;
302
+ }
303
+ /** Returns all property names, including mobx computeds (non-enumerable) & inherited. */
304
+ function getAllPropertyNames(obj) {
305
+ const proto = Object.getPrototypeOf(obj);
306
+ const inherited = proto && proto !== Object.prototype && !Array.isArray(obj) ? getAllPropertyNames(proto) : [];
307
+ return [...new Set(Object.getOwnPropertyNames(obj).concat(inherited))];
308
+ }
309
+ //#endregion
310
+ //#region src/fields/valueField.ts
311
+ function newValueFieldState(parentCopy, parentInstance, parentState, key, rules, isIdKey, isDeleteKey, isReadOnlyKey, isLocalOnly, computed, readOnly, strictOrder, maybeAutoSave) {
312
+ const _tick = observable({ value: 1 });
313
+ const _originalValueTick = observable({ value: 1 });
314
+ let hasInflightChangedValue = false;
315
+ let lastChangedValue = void 0;
316
+ const field = {
317
+ key,
318
+ touched: false,
319
+ /** Current readOnly value. */
320
+ _readOnly: readOnly || false,
321
+ _loading: false,
322
+ _kind: "value",
323
+ _computed: computed,
324
+ _focused: false,
325
+ _isIdKey: isIdKey,
326
+ _isDeleteKey: isDeleteKey,
327
+ _isReadOnlyKey: isReadOnlyKey,
328
+ _isLocalOnly: isLocalOnly,
329
+ rules,
330
+ get value() {
331
+ const value = _tick.value > 0 ? parentInstance[key] : fail();
332
+ return value === null && isEmpty(parentCopy[key]) ? void 0 : value;
333
+ },
334
+ set value(v) {
335
+ this.set(v);
336
+ },
337
+ get focused() {
338
+ return this._focused;
339
+ },
340
+ get dirty() {
341
+ return !areEqual(this.originalValue, this.value, strictOrder);
342
+ },
343
+ /** Returns whether this field is readOnly, although if our parent is readOnly then it trumps. */
344
+ get readOnly() {
345
+ return parentState().readOnly || this._readOnly;
346
+ },
347
+ /** Sets the field readOnly, but `loading` will still be or-d with the parent's readOnly state. */
348
+ set readOnly(readOnly) {
349
+ this._readOnly = readOnly;
350
+ },
351
+ /** Returns whether this field is loading, or if our parent is loading. */
352
+ get loading() {
353
+ return parentState().loading || this._loading;
354
+ },
355
+ /** Sets the field loading, but `loading` will still be or-d with the parent's loading state. */
356
+ set loading(loading) {
357
+ this._loading = loading;
358
+ },
359
+ get changedValue() {
360
+ hasInflightChangedValue = true;
361
+ lastChangedValue = this.value;
362
+ if (this.value === void 0 && this.originalValue !== void 0) return null;
363
+ return this.value;
364
+ },
365
+ get valid() {
366
+ const opts = {
367
+ value: this.value,
368
+ key,
369
+ originalValue: this.originalValue,
370
+ object: parentState()
371
+ };
372
+ return this.rules.every((r) => r(opts) === void 0);
373
+ },
374
+ get errors() {
375
+ const opts = {
376
+ value: this.value,
377
+ key,
378
+ originalValue: this.originalValue,
379
+ object: parentState()
380
+ };
381
+ return this.rules.map((r) => r(opts)).filter(isNotUndefined);
382
+ },
383
+ get required() {
384
+ return this.rules.some((rule) => rule === required);
385
+ },
386
+ get isNewEntity() {
387
+ return parentState().isNewEntity;
388
+ },
389
+ focus() {
390
+ this._focused = true;
391
+ },
392
+ blur() {
393
+ this.maybeAutoSave();
394
+ this._focused = false;
395
+ },
396
+ maybeAutoSave() {
397
+ this.maybeTrim();
398
+ this.touched = true;
399
+ maybeAutoSave();
400
+ },
401
+ set(value, opts = {}) {
402
+ if (this.readOnly && !opts.resetting && !opts.refreshing) throw new Error(`${String(key)} is currently readOnly`);
403
+ if (opts.refreshing && this.dirty) {
404
+ const isAckingUnset = this.value === null && (value === null || value === void 0);
405
+ const acceptServerAck = hasInflightChangedValue && this.value === lastChangedValue && value !== this.originalValue;
406
+ if (!areEqual(this.value, value, strictOrder) && !isAckingUnset && !acceptServerAck) return;
407
+ } else if (computed && (opts.resetting || opts.refreshing)) return;
408
+ const keepNull = !isEmpty(this.originalValue) && isEmpty(value) && !opts.refreshing;
409
+ const coerceEmptyList = value && value instanceof Array && value.length === 0 && isEmpty(this.originalValue);
410
+ const newValue = keepNull ? null : isEmpty(value) || coerceEmptyList ? void 0 : value;
411
+ hasInflightChangedValue = false;
412
+ const changed = !areEqual(newValue, this.value, strictOrder);
413
+ parentInstance[key] = newValue;
414
+ _tick.value++;
415
+ if (opts.refreshing) this.originalValue = newValue;
416
+ if (!this._focused && !opts.refreshing && !opts.resetting && this.dirty && changed && opts.autoSave !== false) this.maybeAutoSave();
417
+ },
418
+ adapt(adapter) {
419
+ return adapt(this, adapter);
420
+ },
421
+ revertChanges() {
422
+ if (!computed) this.set(this.originalValue, { resetting: true });
423
+ this.touched = false;
424
+ },
425
+ commitChanges() {
426
+ if (isPlainObject(this.originalValue)) this.originalValue = toJS(this.value);
427
+ else this.originalValue = this.value;
428
+ this.touched = false;
429
+ },
430
+ get originalValue() {
431
+ const value = _originalValueTick.value > -1 ? parentCopy[key] : parentCopy[key];
432
+ return value === null ? void 0 : value;
433
+ },
434
+ set originalValue(v) {
435
+ if (!(v === void 0 && !(key in parentCopy))) parentCopy[key] = v;
436
+ _originalValueTick.value++;
437
+ },
438
+ maybeTrim() {
439
+ if (typeof this.value === "string") {
440
+ let newValue = this.value.trim();
441
+ if (newValue === "") newValue = void 0;
442
+ if (newValue !== this.value) this.set(newValue);
443
+ }
444
+ }
445
+ };
446
+ if (isObservable(parentInstance)) reaction(() => parentInstance[key], () => {
447
+ if (!parentState().focused) maybeAutoSave();
448
+ });
449
+ return field;
450
+ }
451
+ /**
452
+ * Returns a proxy that looks exactly like the original `field`, in terms of valid/touched/errors/etc., but
453
+ * has any methods that use `V` overridden to use be `V2`.
454
+ *
455
+ * Note that `V2` can be a new type, like string -> number, or just a transformation on the same
456
+ * type, i.e. feet -> inches where both are `number`s.
457
+ */
458
+ function adapt(field, adapter) {
459
+ return newDelegateProxy(field, {
460
+ rules: [],
461
+ get value() {
462
+ return adapter.toValue(field.value);
463
+ },
464
+ set value(v) {
465
+ field.value = adapter.fromValue(v);
466
+ },
467
+ set: (v) => {
468
+ field.value = adapter.fromValue(v);
469
+ },
470
+ get changedValue() {
471
+ return this.value;
472
+ },
473
+ adapt(adapter) {
474
+ return adapt(this, adapter);
475
+ },
476
+ get originalValue() {
477
+ return adapter.toValue(field.originalValue);
478
+ }
479
+ });
480
+ }
481
+ //#endregion
482
+ //#region src/fields/fragmentField.ts
483
+ function newFragmentField(parentInstance, key) {
484
+ const _tick = observable({ value: 1 });
485
+ let value = parentInstance[key];
486
+ delete parentInstance[key];
487
+ return makeAutoObservable({
488
+ key,
489
+ _isIdKey: false,
490
+ _isDeleteKey: false,
491
+ _isReadOnlyKey: false,
492
+ _isLocalOnly: false,
493
+ touched: false,
494
+ valid: true,
495
+ readOnly: true,
496
+ required: false,
497
+ loading: false,
498
+ dirty: false,
499
+ focused: false,
500
+ originalValue: void 0,
501
+ changedValue: void 0,
502
+ errors: [],
503
+ rules: [],
504
+ isNewEntity: false,
505
+ focus: () => {},
506
+ blur: () => {},
507
+ maybeAutoSave: () => {},
508
+ commitChanges: () => {},
509
+ revertChanges: () => {},
510
+ get value() {
511
+ if (key in parentInstance) {
512
+ value = parentInstance[key];
513
+ delete parentInstance[key];
514
+ }
515
+ _tick.value > 0 || fail();
516
+ return value;
517
+ },
518
+ set value(v) {
519
+ value = v;
520
+ _tick.value++;
521
+ },
522
+ set(value) {
523
+ this.value = value;
524
+ },
525
+ adapt(value) {
526
+ throw new Error("FragmentField does not support adapt");
527
+ }
528
+ }, { value: false });
529
+ }
530
+ //#endregion
531
+ //#region src/fields/listField.ts
532
+ function newListFieldState(parentCopy, parentInstance, parentState, key, rules, listConfig, config, strictOrder, maybeAutoSave, deepExhaustive) {
533
+ const rowMap = /* @__PURE__ */ new Map();
534
+ const addedRows = /* @__PURE__ */ new Set();
535
+ const _tick = observable({ value: 1 });
536
+ const _originalValueTick = observable({ value: 1 });
537
+ const _childTick = observable({ value: 1 });
538
+ const copyMap = /* @__PURE__ */ new Map();
539
+ (parentCopy[key] ?? []).forEach((copy, i) => {
540
+ copyMap.set(copy, parentInstance[key][i]);
541
+ });
542
+ function getOrCreateChildState(child, opts) {
543
+ let childState = rowMap.get(child);
544
+ if (!childState && copyMap.has(child)) childState = rowMap.get(copyMap.get(child));
545
+ if (!childState) {
546
+ for (const [, otherState] of rowMap.entries()) if (otherState.isSameEntity(child)) {
547
+ if (!opts?.skipSet) otherState.set(child, opts);
548
+ rowMap.set(child, otherState);
549
+ return otherState;
550
+ }
551
+ }
552
+ if (!childState) {
553
+ childState = newObjectState(config, parentState, void 0, list, child, void 0, maybeAutoSave, listConfig.update === "deep-exhaustive");
554
+ rowMap.set(child, childState);
555
+ }
556
+ return childState;
557
+ }
558
+ const list = {
559
+ key,
560
+ get value() {
561
+ return _tick.value > 0 && _childTick.value > 0 ? parentInstance[key] ?? [] : fail();
562
+ },
563
+ _kind: "list",
564
+ _focused: false,
565
+ _readOnly: false,
566
+ _loading: false,
567
+ get readOnly() {
568
+ return this._readOnly || parentState().readOnly;
569
+ },
570
+ set readOnly(readOnly) {
571
+ this._readOnly = readOnly;
572
+ },
573
+ get loading() {
574
+ return this._loading || parentState().loading;
575
+ },
576
+ set loading(loading) {
577
+ this._loading = loading;
578
+ },
579
+ set value(v) {
580
+ this.set(v);
581
+ },
582
+ get dirty() {
583
+ return this.rows.some((r) => r.dirty) || this.hasChanged();
584
+ },
585
+ get hasNewEntity() {
586
+ return this.rows.some((r) => r.isNewEntity);
587
+ },
588
+ get focused() {
589
+ return this.rows.some((r) => r.focused);
590
+ },
591
+ get required() {
592
+ return this.rules.some((rule) => rule === required);
593
+ },
594
+ get isNewEntity() {
595
+ return parentState().isNewEntity;
596
+ },
597
+ /** Returns whether a row has been added/removed. */
598
+ hasChanged() {
599
+ const [current, original] = [this.value || [], this.originalValue];
600
+ if (current.length !== original.length) return true;
601
+ if (strictOrder) return original.some((e, idx) => current[idx] !== copyMap.get(e));
602
+ else return !original.every((e) => current.includes(copyMap.get(e)));
603
+ },
604
+ get rows() {
605
+ if (_tick.value < 0) fail();
606
+ return (parentInstance[key] || []).map((child) => getOrCreateChildState(child, { skipSet: true }));
607
+ },
608
+ get touched() {
609
+ return this.rows.some((r) => r.touched) || this.hasChanged();
610
+ },
611
+ set touched(touched) {
612
+ this.rows.forEach((r) => r.touched = touched);
613
+ },
614
+ rules,
615
+ get valid() {
616
+ const opts = {
617
+ value: this.rows,
618
+ key,
619
+ originalValue: this.originalValue,
620
+ object: parentState()
621
+ };
622
+ const collectionValid = this.rules.every((r) => r(opts) === void 0);
623
+ const entriesValid = this.rows.filter((r) => !r._considerDeleted()).every((r) => r.valid);
624
+ return collectionValid && entriesValid;
625
+ },
626
+ get errors() {
627
+ if (_tick.value < 0) fail();
628
+ const opts = {
629
+ value: this.rows,
630
+ key,
631
+ originalValue: this.originalValue,
632
+ object: parentState()
633
+ };
634
+ return this.rules.map((r) => r(opts)).filter(isNotUndefined);
635
+ },
636
+ get changedValue() {
637
+ const result = [];
638
+ const hasOpKey = Object.keys(listConfig.config).includes("op");
639
+ const hasLegacyOpKey = Object.keys(listConfig.config).some((key) => key === "delete" || key === "remove");
640
+ const incremental = listConfig.update === "incremental" || listConfig.update === void 0 && (hasOpKey || hasLegacyOpKey);
641
+ const exhaustive = !incremental;
642
+ this.rows.forEach((r) => {
643
+ if (exhaustive || r.dirty || r.isNewEntity) {
644
+ const changed = r.changedValue;
645
+ if (incremental && hasOpKey) changed.op ??= "include";
646
+ result.push(changed);
647
+ }
648
+ });
649
+ return result;
650
+ },
651
+ focus() {
652
+ this._focused = true;
653
+ },
654
+ blur() {
655
+ this._focused = false;
656
+ this.maybeAutoSave();
657
+ },
658
+ maybeAutoSave() {
659
+ this.touched = true;
660
+ maybeAutoSave();
661
+ },
662
+ set(values, opts = {}) {
663
+ if (this.readOnly && !opts.resetting && !opts.refreshing) throw new Error(`${String(key)} is currently readOnly`);
664
+ if ((this.dirty || this.hasNewEntity) && opts.refreshing) {
665
+ const currentItems = this.value || [];
666
+ const incomingItems = values || [];
667
+ const mergedItems = [];
668
+ const idKey = this.rows.length > 0 && this.rows[0].idKey;
669
+ const contentKeys = Object.entries(config.config).filter(([key, cfg]) => key !== idKey && cfg.type === "value").map(([key]) => key);
670
+ const hashByContent = (item) => hashValue(normalizeHashValue(Object.fromEntries(Object.entries(item).filter(([key]) => contentKeys.includes(key)))));
671
+ const hashById = (item) => {
672
+ const idValue = idKey ? item[idKey] : void 0;
673
+ return idValue === void 0 ? hashByContent(item) : idValue;
674
+ };
675
+ const currentById = groupBy(currentItems, hashById);
676
+ const incomingById = groupBy(incomingItems, hashById);
677
+ const incomingByContent = idKey && currentItems.some((item) => !item[idKey]) && groupBy(incomingItems, hashByContent);
678
+ const hasOpKey = Object.keys(listConfig.config).includes("op");
679
+ for (const currentItem of currentItems) {
680
+ const childState = rowMap.get(currentItem);
681
+ const hash = hashById(currentItem);
682
+ const match = incomingById.get(hash)?.[0] ?? (!!idKey && !currentItem[idKey] && !!incomingByContent && incomingByContent?.get(hash)?.[0]);
683
+ if (match) {
684
+ childState.set(match, opts);
685
+ mergedItems.push(childState.value);
686
+ addedRows.delete(currentItem);
687
+ } else if (!childState.dirty && !childState.isNewEntity && !addedRows.has(currentItem)) {} else if (hasOpKey && currentItem.op === "delete") {} else if (currentItems.length === incomingItems.length && !!idKey && currentItem[idKey] === void 0) {} else mergedItems.push(currentItem);
688
+ }
689
+ for (const incomingItem of incomingItems) if (!(currentById.get(hashById(incomingItem))?.[0] || currentById.get(hashByContent(incomingItem))?.[0])) {
690
+ const childState = getOrCreateChildState(incomingItem, opts);
691
+ mergedItems.push(childState.value);
692
+ }
693
+ parentInstance[key] = mergedItems;
694
+ _tick.value++;
695
+ this.setOriginalValue(incomingItems);
696
+ } else {
697
+ parentInstance[key] = (values ?? []).map((child) => getOrCreateChildState(child, opts).value);
698
+ _tick.value++;
699
+ if (opts.refreshing) this.setOriginalValue();
700
+ }
701
+ },
702
+ add(value, spliceIndex) {
703
+ const childState = getOrCreateChildState(value);
704
+ rowMap.set(value, childState);
705
+ addedRows.add(value);
706
+ this.ensureSet();
707
+ this.value.splice(typeof spliceIndex === "number" ? spliceIndex : this.value.length, 0, childState.value);
708
+ _tick.value++;
709
+ maybeAutoSave();
710
+ },
711
+ remove(indexOrValue) {
712
+ this.ensureSet();
713
+ if (typeof indexOrValue === "number") this.value.splice(indexOrValue, 1);
714
+ else {
715
+ const index = this.value.findIndex((v) => v === indexOrValue);
716
+ if (index > -1) this.value.splice(index, 1);
717
+ }
718
+ _tick.value++;
719
+ maybeAutoSave();
720
+ },
721
+ revertChanges() {
722
+ this.set(this.originalValue, { resetting: true });
723
+ this.rows.forEach((r) => r.revertChanges());
724
+ this.touched = false;
725
+ },
726
+ commitChanges() {
727
+ this.rows.forEach((r) => r.commitChanges());
728
+ this.setOriginalValue();
729
+ this.touched = false;
730
+ _tick.value++;
731
+ },
732
+ get originalValue() {
733
+ return (_originalValueTick.value > -1 ? parentCopy[key] : parentCopy[key]) ?? [];
734
+ },
735
+ setOriginalValue(incomingItems) {
736
+ parentCopy[key] = incomingItems ?? this.rows.map((r) => r.originalValue);
737
+ parentCopy[key].forEach((copy, i) => {
738
+ copyMap.set(copy, parentInstance[key][i]);
739
+ });
740
+ _originalValueTick.value++;
741
+ },
742
+ ensureSet() {
743
+ if (!parentInstance[key]) parentInstance[key] = [];
744
+ _tick.value++;
745
+ }
746
+ };
747
+ const proxy = makeAutoObservable(list, { value: computed({ equals: () => false }) });
748
+ reaction(() => proxy.rows.map((r) => r.value), () => _childTick.value++);
749
+ return proxy;
750
+ }
751
+ function hashValue(value) {
752
+ return hash(value);
753
+ }
754
+ //#endregion
755
+ //#region src/fields/objectField.ts
756
+ function fragment(value) {
757
+ return value;
758
+ }
759
+ /**
760
+ * Creates a new `ObjectState` for a given form object `T` given config rules in `config`.
761
+ *
762
+ * The returned `ObjectState` can be used in a mobx `useLocalObservable` to drive an
763
+ * interactive form that tracks the current valid/touched/etc. state of both each
764
+ * individual fields as well as the top-level form/object itself.
765
+ */
766
+ function createObjectState(config, instance, opts = {}) {
767
+ const noop = () => {};
768
+ return newObjectState({
769
+ type: "object",
770
+ config
771
+ }, void 0, void 0, void 0, instance, void 0, opts.maybeAutoSave || noop, false);
772
+ }
773
+ /** A more internal version of `createObjectState`. */
774
+ function newObjectState(config, parentState, parentInstance, parentListState, instance, key, maybeAutoSave, deepExhaustive) {
775
+ let proxy = void 0;
776
+ function getObjectState() {
777
+ if (!proxy) throw new Error("Race condition");
778
+ return proxy;
779
+ }
780
+ const originalCopy = deepClone(instance);
781
+ const objectConfig = config.config;
782
+ const fieldStates = Object.entries(objectConfig).map(([_key, _config]) => {
783
+ const key = _key;
784
+ const config = _config;
785
+ let field;
786
+ if (config.type === "value") field = newValueFieldState(originalCopy, instance, getObjectState, key, config.rules || [], config.isIdKey || key === "id" && !("isIdKey" in config) && !Object.entries(objectConfig).some(([other, c]) => other !== key && c.isIdKey), config.isDeleteKey || false, config.isReadOnlyKey || false, config.isLocalOnly || false, config.computed ?? (isObservable(instance) ? !(key in originalCopy) : false), config.readOnly ?? false, config.strictOrder ?? true, maybeAutoSave);
787
+ else if (config.type === "list") field = newListFieldState(originalCopy, instance, getObjectState, key, config.rules || [], config, {
788
+ type: "object",
789
+ config: config.config
790
+ }, config.strictOrder ?? true, maybeAutoSave, deepExhaustive);
791
+ else if (config.type === "object") {
792
+ if (!instance[key]) instance[key] = {};
793
+ field = newObjectState(config, getObjectState, instance, void 0, instance[key], key, maybeAutoSave, deepExhaustive);
794
+ } else if (config.type === "fragment") field = newFragmentField(instance, key);
795
+ else throw new Error(`Invalid type value ${config.type}`);
796
+ return [key, field];
797
+ });
798
+ const _tick = observable({ value: 1 });
799
+ const fieldNames = Object.keys(objectConfig);
800
+ function getFields(proxyThis) {
801
+ return fieldNames.map((name) => proxyThis[name]);
802
+ }
803
+ const obj = {
804
+ ...Object.fromEntries(fieldStates),
805
+ key,
806
+ get value() {
807
+ _tick.value > 0 || fail();
808
+ return instance;
809
+ },
810
+ set value(value) {
811
+ this.set(value);
812
+ },
813
+ _kind: "object",
814
+ _readOnly: false,
815
+ _loading: false,
816
+ _isAutoSaving: false,
817
+ _considerDeleted() {
818
+ return !!getFields(this).find((f) => f._isDeleteKey)?.value;
819
+ },
820
+ _considerReadOnly() {
821
+ return !!getFields(this).find((f) => f._isReadOnlyKey)?.value;
822
+ },
823
+ get focused() {
824
+ return getFields(this).some((f) => f.focused);
825
+ },
826
+ get touched() {
827
+ return getFields(this).some((f) => f.touched);
828
+ },
829
+ set touched(touched) {
830
+ getFields(this).forEach((f) => f.touched = touched);
831
+ },
832
+ get readOnly() {
833
+ return this._readOnly || this._considerReadOnly() || parentState && parentState().readOnly || !!parentListState?.readOnly;
834
+ },
835
+ set readOnly(readOnly) {
836
+ this._readOnly = readOnly;
837
+ },
838
+ get loading() {
839
+ return this._loading || parentState && parentState().loading || !!parentListState?.loading;
840
+ },
841
+ set loading(loading) {
842
+ this._loading = loading;
843
+ },
844
+ get valid() {
845
+ return getFields(this).every((f) => {
846
+ return f.valid;
847
+ });
848
+ },
849
+ get errors() {
850
+ return getFields(this).flatMap((f) => f.errors.map((e) => `${f.key}: ${e}`));
851
+ },
852
+ get dirty() {
853
+ return getFields(this).some((f) => f.dirty && !f._isLocalOnly) || this.isUnset() && !areEqual(this.value, {});
854
+ },
855
+ get isNewEntity() {
856
+ const idField = getFields(this).find((f) => f._isIdKey);
857
+ if (!idField && parentState) return parentState().isNewEntity;
858
+ return idField !== void 0 && (idField.value === null || idField.value === void 0);
859
+ },
860
+ canSave() {
861
+ this.touched = true;
862
+ return this.valid;
863
+ },
864
+ set(value, opts = {}) {
865
+ if (this.readOnly && !opts.resetting && !opts.refreshing) throw new Error(`${String(key) || "formState"} is currently readOnly`);
866
+ if (value && this.isUnset()) parentInstance[key] = instance;
867
+ if (!value && parentInstance) parentInstance[key] = void 0;
868
+ getFields(this).forEach((field) => {
869
+ if (value && typeof value === "object" && field.key in value) field.set(value[field.key], opts);
870
+ });
871
+ },
872
+ revertChanges() {
873
+ getFields(this).forEach((f) => f.revertChanges());
874
+ },
875
+ commitChanges() {
876
+ if (this._isAutoSaving) throw new Error("When using autoSave, you should not manually call commitChanges, instead have init.map/input update the form state");
877
+ getFields(this).forEach((f) => f.commitChanges());
878
+ },
879
+ get changedValue() {
880
+ const result = {};
881
+ if (this.isUnset()) {
882
+ if (getFields(this).find((f) => f._isIdKey) && config.reference) return { id: null };
883
+ return null;
884
+ }
885
+ getFields(this).forEach((f) => {
886
+ if (f._computed) return;
887
+ if (config.reference) return;
888
+ if (f._isLocalOnly) return;
889
+ if (f.dirty || this.isNewEntity && f._kind === "value" && f.value !== void 0 || this.isNewEntity && f._kind === "object" && f.changedValue && Object.entries(f.changedValue).length > 0 || this.isNewEntity && f._kind === "list" && f.value?.length > 0 || deepExhaustive) result[f.key] = f.changedValue;
890
+ });
891
+ const idField = getFields(this).find((f) => f._isIdKey);
892
+ if (idField && idField.value !== void 0) result[idField.key] = idField.value;
893
+ return result;
894
+ },
895
+ get originalValue() {
896
+ getFields(proxy).map((f) => f.originalValue);
897
+ return originalCopy;
898
+ },
899
+ isSameEntity(other) {
900
+ const idField = getFields(this).find((f) => f._isIdKey);
901
+ if (!idField) return false;
902
+ const ourIdValue = idField.value;
903
+ const otherIdValue = other[idField.key];
904
+ return otherIdValue !== void 0 && areEqual(otherIdValue, ourIdValue);
905
+ },
906
+ isUnset() {
907
+ return !!parentInstance && parentInstance[key] === void 0;
908
+ },
909
+ get idKey() {
910
+ return getFields(this).find((f) => f._isIdKey)?.key;
911
+ }
912
+ };
913
+ proxy = makeAutoObservable(obj, {
914
+ value: computed({ equals: () => false }),
915
+ originalValue: computed({ equals: () => false })
916
+ });
917
+ reaction(() => getFields(proxy).map((f) => f.value), () => _tick.value++);
918
+ return proxy;
919
+ }
920
+ //#endregion
921
+ //#region src/configBuilders.ts
922
+ /**
923
+ * Provides a Zod-ish API for building form configs.
924
+ */
925
+ const f = {
926
+ /** Creates the top-level config for a form, like an author or book. */
927
+ config(fields) {
928
+ return new ObjectConfigBuilder(fields).build().config;
929
+ },
930
+ /** Creates the config DSL for an object, like an author or book. */
931
+ object(fields) {
932
+ return new ObjectConfigBuilder(fields);
933
+ },
934
+ /** Creates the config DSL for an list of objects, like an author's list of books. */
935
+ list(fields) {
936
+ return new ListFieldConfigBuilder(fields);
937
+ },
938
+ /** Creates the config DSL for a primitive value, like a first name or phone number. */
939
+ value() {
940
+ return new ValueFieldConfigBuilder();
941
+ },
942
+ /** A shortcut for creating a child object with a single `id` key. */
943
+ reference(fields) {
944
+ return f.object({
945
+ id: f.value(),
946
+ ...fields
947
+ }).ref();
948
+ },
949
+ /** A shorthand for creating a computed value. */
950
+ computed() {
951
+ return this.value().computed();
952
+ }
953
+ };
954
+ /** Provides a fluent DSL for building up an object's config. */
955
+ var ObjectConfigBuilder = class {
956
+ config = {
957
+ type: "object",
958
+ config: {}
959
+ };
960
+ constructor(fields) {
961
+ for (const [key, value] of Object.entries(fields)) this.config.config[key] = value.build();
962
+ }
963
+ ref() {
964
+ this.config.reference = true;
965
+ return this;
966
+ }
967
+ build() {
968
+ return this.config;
969
+ }
970
+ };
971
+ /** Provides a fluent DSL for building up a value's config. */
972
+ var ValueFieldConfigBuilder = class {
973
+ config = {
974
+ type: "value",
975
+ rules: []
976
+ };
977
+ /** Marks the field as required. */
978
+ req() {
979
+ this.config.rules = [required];
980
+ return this;
981
+ }
982
+ /** Marks the field as read only. */
983
+ readOnly() {
984
+ this.config.readOnly = true;
985
+ return this;
986
+ }
987
+ /** Marks the field as local only. */
988
+ localOnly() {
989
+ this.config.isLocalOnly = true;
990
+ return this;
991
+ }
992
+ /** Appends `rules` to the field's validation rules. */
993
+ rules(rules) {
994
+ (this.config.rules ??= []).push(...rules);
995
+ return this;
996
+ }
997
+ /** Marks the field as computed. */
998
+ computed() {
999
+ this.config.computed = true;
1000
+ return this;
1001
+ }
1002
+ build() {
1003
+ return this.config;
1004
+ }
1005
+ };
1006
+ /** Provides a fluent DSL for building up a list's config. */
1007
+ var ListFieldConfigBuilder = class {
1008
+ config;
1009
+ constructor(fields) {
1010
+ const config = {};
1011
+ for (const [key, value] of Object.entries(fields)) config[key] = value.build();
1012
+ this.config = {
1013
+ type: "list",
1014
+ rules: [],
1015
+ config
1016
+ };
1017
+ }
1018
+ /** Appends `rule` to the field's validation rules. */
1019
+ rule(rule) {
1020
+ (this.config.rules ??= []).push(rule);
1021
+ return this;
1022
+ }
1023
+ /** Appends `rules` to the field's validation rules. */
1024
+ rules(rules) {
1025
+ (this.config.rules ??= []).push(...rules);
1026
+ return this;
1027
+ }
1028
+ build() {
1029
+ return this.config;
1030
+ }
1031
+ };
1032
+ //#endregion
1033
+ //#region src/useFormStates.ts
1034
+ /**
1035
+ * A hook to manage many "mini-forms" on a single page, typically one form per row
1036
+ * in a table.
1037
+ *
1038
+ * This hook basically provides the page/table with a cache, so each table row naively ask "what's
1039
+ * the form state for this given row's data?" and get back a new-or-existing `ObjectState` instance
1040
+ * that, if already existing, still has any of the user's WIP changes.
1041
+ *
1042
+ * Each mini-form/row can have its own autoSave calls, independent of the other rows.
1043
+ *
1044
+ * @typeparam T the form type, which is usually as close as possible to your *GraphQL input*
1045
+ * @typeparam I the *form input* type, which is usually the *GraphQL output* type, i.e. the type of the response from your GraphQL query
1046
+ */
1047
+ function useFormStates(opts) {
1048
+ const { config, autoSave, getId, map, addRules, readOnly = false } = opts;
1049
+ const objectStateCache = useMemo(() => ({}), [config]);
1050
+ const pendingAutoSaves = useRef(/* @__PURE__ */ new Set());
1051
+ const autoSaveRef = useRef(autoSave);
1052
+ autoSaveRef.current = autoSave;
1053
+ const readOnlyRef = useRef(readOnly);
1054
+ readOnlyRef.current = readOnly;
1055
+ return { getFormState: useCallback((input, opts = {}) => {
1056
+ const existing = objectStateCache[getId(input)];
1057
+ let form = existing?.[0];
1058
+ async function maybeAutoSave(form) {
1059
+ if (autoSaveRef.current && form.dirty) {
1060
+ if (!form.valid) {
1061
+ console.debug("Skipping auto-save b/c form is invalid: ", form.errors);
1062
+ return;
1063
+ }
1064
+ const { current: pending } = pendingAutoSaves;
1065
+ if (isAutoSaving) {
1066
+ pending.add(form);
1067
+ return;
1068
+ }
1069
+ try {
1070
+ isAutoSaving = true;
1071
+ await new Promise((resolve) => setTimeout(resolve, 0));
1072
+ pending.delete(form);
1073
+ await autoSaveRef.current(form);
1074
+ } catch (e) {
1075
+ throw e;
1076
+ } finally {
1077
+ isAutoSaving = false;
1078
+ if (pending.size > 0) {
1079
+ const first = pending.values().next().value;
1080
+ pending.delete(first);
1081
+ await maybeAutoSave(first);
1082
+ }
1083
+ }
1084
+ }
1085
+ }
1086
+ if (!form) {
1087
+ form = createObjectState(config, initValue(config, {
1088
+ map,
1089
+ input
1090
+ }), { maybeAutoSave: () => maybeAutoSave(form) });
1091
+ if (addRules) addRules(form);
1092
+ objectStateCache[getId(input)] = [form, input];
1093
+ }
1094
+ if (existing && existing[1] !== input) {
1095
+ form.set(initValue(config, {
1096
+ map,
1097
+ input
1098
+ }), { refreshing: true });
1099
+ existing[1] = input;
1100
+ }
1101
+ form.readOnly = readOnlyRef.current || !!opts.readOnly;
1102
+ return form;
1103
+ }, [objectStateCache, config]) };
1104
+ }
1105
+ let isAutoSaving = false;
1106
+ //#endregion
1107
+ export { createObjectState, f, fragment, required, useFormState, useFormStates };
1108
+
1109
+ //# sourceMappingURL=index.js.map