@homebound/form-state 2.27.3 → 2.29.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/dist/config.d.ts +2 -0
- package/dist/configBuilders.d.ts +2 -0
- package/dist/configBuilders.js +5 -0
- package/dist/fields/deepEquals.js +12 -0
- package/dist/fields/fragmentField.js +1 -0
- package/dist/fields/listField.js +8 -2
- package/dist/fields/objectField.js +6 -3
- package/dist/fields/valueField.d.ts +2 -1
- package/dist/fields/valueField.js +2 -1
- package/dist/formState.test.js +27 -0
- package/dist/temporal.d.ts +6 -0
- package/dist/temporal.js +34 -0
- package/dist/temporal.test.d.ts +1 -0
- package/dist/temporal.test.js +97 -0
- package/dist/utils.d.ts +4 -1
- package/dist/utils.js +33 -0
- package/package.json +3 -2
package/dist/config.d.ts
CHANGED
|
@@ -28,6 +28,8 @@ export type ValueFieldConfig<V> = {
|
|
|
28
28
|
isDeleteKey?: boolean;
|
|
29
29
|
/** If true, the entity that contains this value will be treated as read only. */
|
|
30
30
|
isReadOnlyKey?: boolean;
|
|
31
|
+
/** If true, we ignore field in dirty checks. */
|
|
32
|
+
isLocalOnly?: boolean;
|
|
31
33
|
/**
|
|
32
34
|
* Marks a field as being backed by a mobx class computed field.
|
|
33
35
|
*
|
package/dist/configBuilders.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export declare class ValueFieldConfigBuilder<V> {
|
|
|
47
47
|
req(): this;
|
|
48
48
|
/** Marks the field as read only. */
|
|
49
49
|
readOnly(): this;
|
|
50
|
+
/** Marks the field as local only. */
|
|
51
|
+
localOnly(): this;
|
|
50
52
|
/** Appends `rules` to the field's validation rules. */
|
|
51
53
|
rules(rules: Rule<V | null | undefined>[]): this;
|
|
52
54
|
/** Marks the field as computed. */
|
package/dist/configBuilders.js
CHANGED
|
@@ -63,6 +63,11 @@ class ValueFieldConfigBuilder {
|
|
|
63
63
|
this.config.readOnly = true;
|
|
64
64
|
return this;
|
|
65
65
|
}
|
|
66
|
+
/** Marks the field as local only. */
|
|
67
|
+
localOnly() {
|
|
68
|
+
this.config.isLocalOnly = true;
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
66
71
|
/** Appends `rules` to the field's validation rules. */
|
|
67
72
|
rules(rules) {
|
|
68
73
|
var _a;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.deepEquals = deepEquals;
|
|
4
|
+
const temporal_1 = require("../temporal");
|
|
4
5
|
/**
|
|
5
6
|
* Our own `deepEquals` because either `fast-deep-equals` or `dequal` or etc actually
|
|
6
7
|
* handle cyclic data structures, despite ChatGTP's assertions/hallucinations.
|
|
@@ -8,6 +9,9 @@ exports.deepEquals = deepEquals;
|
|
|
8
9
|
* Ported from https://github.com/KoryNunn/cyclic-deep-equal which is ISC.
|
|
9
10
|
*/
|
|
10
11
|
function deepEquals(a, b, visited = new Set()) {
|
|
12
|
+
const temporalEquals = (0, temporal_1.areSupportedTemporalValuesEqual)(a, b);
|
|
13
|
+
if (temporalEquals !== undefined)
|
|
14
|
+
return temporalEquals;
|
|
11
15
|
const aType = typeof a;
|
|
12
16
|
if (aType !== typeof b)
|
|
13
17
|
return false;
|
|
@@ -16,6 +20,11 @@ function deepEquals(a, b, visited = new Set()) {
|
|
|
16
20
|
return true;
|
|
17
21
|
return a === b;
|
|
18
22
|
}
|
|
23
|
+
if (hasToJSON(a) || hasToJSON(b)) {
|
|
24
|
+
const a1 = hasToJSON(a) ? a.toJSON() : a;
|
|
25
|
+
const b1 = hasToJSON(b) ? b.toJSON() : b;
|
|
26
|
+
return deepEquals(a1, b1, visited);
|
|
27
|
+
}
|
|
19
28
|
if (Array.isArray(a) !== Array.isArray(b))
|
|
20
29
|
return false;
|
|
21
30
|
const aKeys = Object.keys(a), bKeys = Object.keys(b);
|
|
@@ -39,3 +48,6 @@ function deepEquals(a, b, visited = new Set()) {
|
|
|
39
48
|
}
|
|
40
49
|
return equal;
|
|
41
50
|
}
|
|
51
|
+
function hasToJSON(o) {
|
|
52
|
+
return !!(o && typeof o === "object" && "toJSON" in o);
|
|
53
|
+
}
|
package/dist/fields/listField.js
CHANGED
|
@@ -207,8 +207,11 @@ parentInstance, parentState, key, rules, listConfig, config, strictOrder, maybeA
|
|
|
207
207
|
const contentKeys = Object.entries(config.config)
|
|
208
208
|
.filter(([key, cfg]) => key !== idKey && cfg.type === "value")
|
|
209
209
|
.map(([key]) => key);
|
|
210
|
-
const hashByContent = (item) => (0,
|
|
211
|
-
const hashById = (item) =>
|
|
210
|
+
const hashByContent = (item) => hashValue((0, utils_1.normalizeHashValue)(Object.fromEntries(Object.entries(item).filter(([key]) => contentKeys.includes(key)))));
|
|
211
|
+
const hashById = (item) => {
|
|
212
|
+
const idValue = idKey ? item[idKey] : undefined;
|
|
213
|
+
return idValue === undefined ? hashByContent(item) : idValue;
|
|
214
|
+
};
|
|
212
215
|
const currentById = (0, utils_1.groupBy)(currentItems, hashById);
|
|
213
216
|
const incomingById = (0, utils_1.groupBy)(incomingItems, hashById);
|
|
214
217
|
// If our local instance doesn't assign in id, when we get results back from the server, strip
|
|
@@ -347,3 +350,6 @@ parentInstance, parentState, key, rules, listConfig, config, strictOrder, maybeA
|
|
|
347
350
|
(0, mobx_1.reaction)(() => proxy.rows.map((r) => r.value), () => _childTick.value++);
|
|
348
351
|
return proxy;
|
|
349
352
|
}
|
|
353
|
+
function hashValue(value) {
|
|
354
|
+
return (0, object_hash_1.default)(value);
|
|
355
|
+
}
|
|
@@ -50,7 +50,7 @@ function newObjectState(config, parentState, parentInstance, parentListState, in
|
|
|
50
50
|
// Default the id key to "id" unless some other field has isIdKey set
|
|
51
51
|
(key === "id" &&
|
|
52
52
|
!("isIdKey" in config) && // Allow setting `isIdKey=false`
|
|
53
|
-
!Object.entries(objectConfig).some(([other, c]) => other !== key && c.isIdKey)), config.isDeleteKey || false, config.isReadOnlyKey || false, (_a = config.computed) !== null && _a !== void 0 ? _a :
|
|
53
|
+
!Object.entries(objectConfig).some(([other, c]) => other !== key && c.isIdKey)), config.isDeleteKey || false, config.isReadOnlyKey || false, config.isLocalOnly || false, (_a = config.computed) !== null && _a !== void 0 ? _a :
|
|
54
54
|
// If instance is a mobx class, we can detect computeds as they won't be enumerable
|
|
55
55
|
((0, mobx_1.isObservable)(instance) ? !(key in originalCopy) : false), (_b = config.readOnly) !== null && _b !== void 0 ? _b : false, (_c = config.strictOrder) !== null && _c !== void 0 ? _c : true, maybeAutoSave);
|
|
56
56
|
}
|
|
@@ -138,7 +138,7 @@ function newObjectState(config, parentState, parentInstance, parentListState, in
|
|
|
138
138
|
return getFields(this).flatMap((f) => f.errors.map((e) => `${f.key}: ${e}`));
|
|
139
139
|
},
|
|
140
140
|
get dirty() {
|
|
141
|
-
return (getFields(this).some((f) => f.dirty) ||
|
|
141
|
+
return (getFields(this).some((f) => f.dirty && !f._isLocalOnly) ||
|
|
142
142
|
// `isUnset` checks if our `parent[key] === undefined`, which can mean "surely we're dirty",
|
|
143
143
|
// but as long as we've got some keys actually set
|
|
144
144
|
(this.isUnset() && !(0, utils_1.areEqual)(this.value, {})));
|
|
@@ -212,6 +212,9 @@ function newObjectState(config, parentState, parentInstance, parentListState, in
|
|
|
212
212
|
// References only include the id key below
|
|
213
213
|
if (config.reference)
|
|
214
214
|
return;
|
|
215
|
+
// Ignore local-only fields
|
|
216
|
+
if (f._isLocalOnly)
|
|
217
|
+
return;
|
|
215
218
|
if (f.dirty ||
|
|
216
219
|
// If the caller used useFormState.ifUndefined to provide some default values, then those keys may not
|
|
217
220
|
// look dirty, but if we're new we should include them anyway.
|
|
@@ -248,7 +251,7 @@ function newObjectState(config, parentState, parentInstance, parentListState, in
|
|
|
248
251
|
const ourIdValue = idField.value;
|
|
249
252
|
const otherIdValue = other[idField.key];
|
|
250
253
|
// If the otherIdValue is undefined, it's a new entity so can't be the same as us
|
|
251
|
-
return otherIdValue !== undefined && otherIdValue
|
|
254
|
+
return otherIdValue !== undefined && (0, utils_1.areEqual)(otherIdValue, ourIdValue);
|
|
252
255
|
},
|
|
253
256
|
isUnset() {
|
|
254
257
|
return !!parentInstance && parentInstance[key] === undefined;
|
|
@@ -70,5 +70,6 @@ export interface FieldStateInternal<T, V> extends FieldState<V> {
|
|
|
70
70
|
_isIdKey: boolean;
|
|
71
71
|
_isDeleteKey: boolean;
|
|
72
72
|
_isReadOnlyKey: boolean;
|
|
73
|
+
_isLocalOnly: boolean;
|
|
73
74
|
}
|
|
74
|
-
export declare function newValueFieldState<T, K extends keyof T>(parentCopy: T, parentInstance: T, parentState: () => ObjectState<T>, key: K, rules: Rule<T[K] | null | undefined>[], isIdKey: boolean, isDeleteKey: boolean, isReadOnlyKey: boolean, computed: boolean, readOnly: boolean, strictOrder: boolean, maybeAutoSave: () => void): FieldState<T[K] | null | undefined>;
|
|
75
|
+
export declare function newValueFieldState<T, K extends keyof T>(parentCopy: T, parentInstance: T, parentState: () => ObjectState<T>, key: K, rules: Rule<T[K] | null | undefined>[], isIdKey: boolean, isDeleteKey: boolean, isReadOnlyKey: boolean, isLocalOnly: boolean, computed: boolean, readOnly: boolean, strictOrder: boolean, maybeAutoSave: () => void): FieldState<T[K] | null | undefined>;
|
|
@@ -6,7 +6,7 @@ const mobx_1 = require("mobx");
|
|
|
6
6
|
const proxies_1 = require("../proxies");
|
|
7
7
|
const rules_1 = require("../rules");
|
|
8
8
|
const utils_1 = require("../utils");
|
|
9
|
-
function newValueFieldState(parentCopy, parentInstance, parentState, key, rules, isIdKey, isDeleteKey, isReadOnlyKey, computed, readOnly, strictOrder, maybeAutoSave) {
|
|
9
|
+
function newValueFieldState(parentCopy, parentInstance, parentState, key, rules, isIdKey, isDeleteKey, isReadOnlyKey, isLocalOnly, computed, readOnly, strictOrder, maybeAutoSave) {
|
|
10
10
|
// Because we read/write the value directly back into parentInstance[key],
|
|
11
11
|
// which itself is not a proxy, we use this as our "value changed" trigger.
|
|
12
12
|
const _tick = (0, mobx_1.observable)({ value: 1 });
|
|
@@ -31,6 +31,7 @@ function newValueFieldState(parentCopy, parentInstance, parentState, key, rules,
|
|
|
31
31
|
_isIdKey: isIdKey,
|
|
32
32
|
_isDeleteKey: isDeleteKey,
|
|
33
33
|
_isReadOnlyKey: isReadOnlyKey,
|
|
34
|
+
_isLocalOnly: isLocalOnly,
|
|
34
35
|
rules,
|
|
35
36
|
get value() {
|
|
36
37
|
// If we're wrapping a mobx store, then we'll get reactivity from parentInstance[key]
|
package/dist/formState.test.js
CHANGED
|
@@ -640,6 +640,33 @@ describe("formState", () => {
|
|
|
640
640
|
a1.books.remove(0);
|
|
641
641
|
expect(a1.dirty).toBeFalsy();
|
|
642
642
|
});
|
|
643
|
+
it("can ignore isLocalOnly fields as not dirty", () => {
|
|
644
|
+
// Given we have an `isPublished` field that is marked as local only
|
|
645
|
+
const config = {
|
|
646
|
+
id: { type: "value" },
|
|
647
|
+
books: {
|
|
648
|
+
type: "list",
|
|
649
|
+
config: {
|
|
650
|
+
id: { type: "value" },
|
|
651
|
+
isPublished: { type: "value", isLocalOnly: true },
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
};
|
|
655
|
+
// And it starts out true
|
|
656
|
+
const a1 = (0, objectField_1.createObjectState)(config, {
|
|
657
|
+
id: "a:1",
|
|
658
|
+
books: [{ id: "b:1", isPublished: true }],
|
|
659
|
+
});
|
|
660
|
+
// When we change the value to false
|
|
661
|
+
a1.books.rows[0].isPublished.value = false;
|
|
662
|
+
// Then the field itself is dirty
|
|
663
|
+
expect(a1.books.rows[0].isPublished.dirty).toBe(true);
|
|
664
|
+
// But object is not
|
|
665
|
+
expect(a1.dirty).toBe(false);
|
|
666
|
+
expect(a1.books.dirty).toBe(false);
|
|
667
|
+
// And we don't include it in changedValue
|
|
668
|
+
expect(a1.changedValue).toEqual({ id: "a:1" });
|
|
669
|
+
});
|
|
643
670
|
it("handles list fields with duplicate ids", () => {
|
|
644
671
|
// Given an author that incorrectly has duplicate books
|
|
645
672
|
const a1 = createAuthorInputState({
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type Temporal } from "temporal-polyfill";
|
|
2
|
+
export type SupportedTemporal = Temporal.PlainDate | Temporal.ZonedDateTime;
|
|
3
|
+
/** Detects the Temporal value types that form-state treats as builtins. */
|
|
4
|
+
export declare function isSupportedTemporal(value: unknown): value is SupportedTemporal;
|
|
5
|
+
/** Compares supported Temporal values with Temporal's `.equals(...)` semantics. */
|
|
6
|
+
export declare function areSupportedTemporalValuesEqual(a: unknown, b: unknown): boolean | undefined;
|
package/dist/temporal.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isSupportedTemporal = isSupportedTemporal;
|
|
4
|
+
exports.areSupportedTemporalValuesEqual = areSupportedTemporalValuesEqual;
|
|
5
|
+
// Use Temporal's well-known tags for runtime detection instead of `instanceof` because this
|
|
6
|
+
// library can receive values from native Temporal, a polyfill, another copy of a polyfill,
|
|
7
|
+
// or another realm. Tag checks are stable across those boundaries, while `instanceof` is not.
|
|
8
|
+
const temporalPlainDateTag = "Temporal.PlainDate";
|
|
9
|
+
const temporalZonedDateTimeTag = "Temporal.ZonedDateTime";
|
|
10
|
+
/** Detects the Temporal value types that form-state treats as builtins. */
|
|
11
|
+
function isSupportedTemporal(value) {
|
|
12
|
+
if (!value || typeof value !== "object") {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
const record = value;
|
|
16
|
+
return (isSupportedTemporalTag(record[Symbol.toStringTag]) &&
|
|
17
|
+
typeof record.equals === "function" &&
|
|
18
|
+
typeof record.toJSON === "function");
|
|
19
|
+
}
|
|
20
|
+
/** Compares supported Temporal values with Temporal's `.equals(...)` semantics. */
|
|
21
|
+
function areSupportedTemporalValuesEqual(a, b) {
|
|
22
|
+
const aIsTemporal = isSupportedTemporal(a);
|
|
23
|
+
const bIsTemporal = isSupportedTemporal(b);
|
|
24
|
+
if (!aIsTemporal && !bIsTemporal) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
if (!aIsTemporal || !bIsTemporal || a[Symbol.toStringTag] !== b[Symbol.toStringTag]) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return a.equals(b);
|
|
31
|
+
}
|
|
32
|
+
function isSupportedTemporalTag(value) {
|
|
33
|
+
return value === temporalPlainDateTag || value === temporalZonedDateTimeTag;
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const temporal_polyfill_1 = require("temporal-polyfill");
|
|
4
|
+
const configBuilders_1 = require("./configBuilders");
|
|
5
|
+
const objectField_1 = require("./fields/objectField");
|
|
6
|
+
describe("Temporal support", () => {
|
|
7
|
+
it("treats equal Temporal value fields as unchanged", () => {
|
|
8
|
+
const form = (0, objectField_1.createObjectState)(temporalValueConfig, {
|
|
9
|
+
publishedOn: temporal_polyfill_1.Temporal.PlainDate.from("2026-04-17"),
|
|
10
|
+
scheduledAt: temporal_polyfill_1.Temporal.ZonedDateTime.from("2026-04-17T09:00:00-04:00[America/New_York]"),
|
|
11
|
+
});
|
|
12
|
+
form.publishedOn.set(temporal_polyfill_1.Temporal.PlainDate.from("2026-04-17"));
|
|
13
|
+
form.scheduledAt.set(temporal_polyfill_1.Temporal.ZonedDateTime.from("2026-04-17T09:00:00-04:00[America/New_York]"));
|
|
14
|
+
expect(form.publishedOn.dirty).toBe(false);
|
|
15
|
+
expect(form.scheduledAt.dirty).toBe(false);
|
|
16
|
+
expect(form.dirty).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
it("tracks dirty nested object fields with Temporal values", () => {
|
|
19
|
+
const form = (0, objectField_1.createObjectState)(temporalObjectConfig, {
|
|
20
|
+
details: {
|
|
21
|
+
dueOn: temporal_polyfill_1.Temporal.PlainDate.from("2026-04-17"),
|
|
22
|
+
startsAt: temporal_polyfill_1.Temporal.ZonedDateTime.from("2026-04-17T09:00:00-04:00[America/New_York]"),
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
form.details.dueOn.set(temporal_polyfill_1.Temporal.PlainDate.from("2026-04-17"));
|
|
26
|
+
form.details.startsAt.set(temporal_polyfill_1.Temporal.ZonedDateTime.from("2026-04-17T09:00:00-04:00[America/New_York]"));
|
|
27
|
+
expect(form.details.dirty).toBe(false);
|
|
28
|
+
expect(form.dirty).toBe(false);
|
|
29
|
+
form.details.startsAt.set(temporal_polyfill_1.Temporal.ZonedDateTime.from("2026-04-18T09:00:00-04:00[America/New_York]"));
|
|
30
|
+
expect(form.details.startsAt.dirty).toBe(true);
|
|
31
|
+
expect(form.details.dirty).toBe(true);
|
|
32
|
+
expect(form.dirty).toBe(true);
|
|
33
|
+
form.details.revertChanges();
|
|
34
|
+
expect(form.details.startsAt.dirty).toBe(false);
|
|
35
|
+
expect(form.details.dirty).toBe(false);
|
|
36
|
+
expect(form.dirty).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
it("supports list row hashing for Temporal.ZonedDateTime values", () => {
|
|
39
|
+
var _a;
|
|
40
|
+
const form = (0, objectField_1.createObjectState)(temporalListConfig, { events: [], milestones: [] });
|
|
41
|
+
expect(() => {
|
|
42
|
+
form.events.add({ startsAt: temporal_polyfill_1.Temporal.ZonedDateTime.from("2026-04-17T09:00:00-04:00[America/New_York]") });
|
|
43
|
+
}).not.toThrow();
|
|
44
|
+
expect(form.events.dirty).toBe(true);
|
|
45
|
+
const ackedRow = {
|
|
46
|
+
id: "event:1",
|
|
47
|
+
startsAt: temporal_polyfill_1.Temporal.ZonedDateTime.from("2026-04-17T09:00:00-04:00[America/New_York]"),
|
|
48
|
+
};
|
|
49
|
+
expect(() => form.set({ events: [ackedRow] }, { refreshing: true })).not.toThrow();
|
|
50
|
+
expect(form.events.dirty).toBe(false);
|
|
51
|
+
expect(form.events.value[0].id).toEqual("event:1");
|
|
52
|
+
expect((_a = form.events.rows[0].startsAt.value) === null || _a === void 0 ? void 0 : _a.equals(ackedRow.startsAt)).toBe(true);
|
|
53
|
+
form.events.rows[0].startsAt.set(temporal_polyfill_1.Temporal.ZonedDateTime.from("2026-04-18T09:00:00-04:00[America/New_York]"));
|
|
54
|
+
expect(form.events.rows[0].startsAt.dirty).toBe(true);
|
|
55
|
+
expect(form.events.dirty).toBe(true);
|
|
56
|
+
expect(() => form.events.remove(0)).not.toThrow();
|
|
57
|
+
expect(form.events.value).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
it("supports list row hashing for Temporal.PlainDate values", () => {
|
|
60
|
+
var _a;
|
|
61
|
+
const form = (0, objectField_1.createObjectState)(temporalListConfig, { events: [], milestones: [] });
|
|
62
|
+
expect(() => {
|
|
63
|
+
form.milestones.add({ dueOn: temporal_polyfill_1.Temporal.PlainDate.from("2026-04-17") });
|
|
64
|
+
}).not.toThrow();
|
|
65
|
+
expect(form.milestones.dirty).toBe(true);
|
|
66
|
+
const ackedRow = { id: "milestone:1", dueOn: temporal_polyfill_1.Temporal.PlainDate.from("2026-04-17") };
|
|
67
|
+
expect(() => form.set({ milestones: [ackedRow] }, { refreshing: true })).not.toThrow();
|
|
68
|
+
expect(form.milestones.dirty).toBe(false);
|
|
69
|
+
expect(form.milestones.value[0].id).toEqual("milestone:1");
|
|
70
|
+
expect((_a = form.milestones.rows[0].dueOn.value) === null || _a === void 0 ? void 0 : _a.equals(ackedRow.dueOn)).toBe(true);
|
|
71
|
+
form.milestones.rows[0].dueOn.set(temporal_polyfill_1.Temporal.PlainDate.from("2026-04-18"));
|
|
72
|
+
expect(form.milestones.rows[0].dueOn.dirty).toBe(true);
|
|
73
|
+
expect(form.milestones.dirty).toBe(true);
|
|
74
|
+
expect(() => form.milestones.remove(0)).not.toThrow();
|
|
75
|
+
expect(form.milestones.value).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
const temporalValueConfig = configBuilders_1.f.config({
|
|
79
|
+
publishedOn: configBuilders_1.f.value(),
|
|
80
|
+
scheduledAt: configBuilders_1.f.value(),
|
|
81
|
+
});
|
|
82
|
+
const temporalObjectConfig = configBuilders_1.f.config({
|
|
83
|
+
details: configBuilders_1.f.object({
|
|
84
|
+
dueOn: configBuilders_1.f.value(),
|
|
85
|
+
startsAt: configBuilders_1.f.value(),
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
88
|
+
const temporalListConfig = configBuilders_1.f.config({
|
|
89
|
+
events: configBuilders_1.f.list({
|
|
90
|
+
id: configBuilders_1.f.value(),
|
|
91
|
+
startsAt: configBuilders_1.f.value(),
|
|
92
|
+
}),
|
|
93
|
+
milestones: configBuilders_1.f.list({
|
|
94
|
+
id: configBuilders_1.f.value(),
|
|
95
|
+
dueOn: configBuilders_1.f.value(),
|
|
96
|
+
}),
|
|
97
|
+
});
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ObjectConfig } from "./config";
|
|
2
|
+
import { type SupportedTemporal } from "./temporal";
|
|
2
3
|
import { InputAndMap, QueryAndMap, UseFormStateOpts } from "./useFormState";
|
|
3
|
-
export type Builtin = Date | Function | Uint8Array | string | number | boolean;
|
|
4
|
+
export type Builtin = Date | Function | SupportedTemporal | Uint8Array | string | number | boolean;
|
|
4
5
|
export type Primitive = undefined | null | boolean | string | number | Function | Date | {
|
|
5
6
|
toJSON(): any;
|
|
6
7
|
};
|
|
@@ -44,6 +45,8 @@ export declare function areEqual<T>(a?: T, b?: T, strictOrder?: boolean): boolea
|
|
|
44
45
|
export declare function hasToJSON(o?: unknown): o is {
|
|
45
46
|
toJSON(): void;
|
|
46
47
|
};
|
|
48
|
+
/** Normalizes values into stable object-hash-safe data using our existing `toJSON` semantics. */
|
|
49
|
+
export declare function normalizeHashValue(value: unknown, seen?: WeakMap<object, unknown>): unknown;
|
|
47
50
|
/** Make a clone of `obj`, but only recurse into POJOs and Arrays...and stores. */
|
|
48
51
|
export declare function deepClone<T>(obj: T, map?: WeakMap<WeakKey, any>): T;
|
|
49
52
|
export declare function groupBy<T, Y = T, K = string>(list: readonly T[], fn: (x: T) => K, valueFn?: (x: T) => Y): Map<K, Y[]>;
|
package/dist/utils.js
CHANGED
|
@@ -10,11 +10,13 @@ exports.isNotUndefined = isNotUndefined;
|
|
|
10
10
|
exports.isEmpty = isEmpty;
|
|
11
11
|
exports.areEqual = areEqual;
|
|
12
12
|
exports.hasToJSON = hasToJSON;
|
|
13
|
+
exports.normalizeHashValue = normalizeHashValue;
|
|
13
14
|
exports.deepClone = deepClone;
|
|
14
15
|
exports.groupBy = groupBy;
|
|
15
16
|
const is_plain_object_1 = require("is-plain-object");
|
|
16
17
|
const mobx_1 = require("mobx");
|
|
17
18
|
const deepEquals_1 = require("./fields/deepEquals");
|
|
19
|
+
const temporal_1 = require("./temporal");
|
|
18
20
|
function fail(message) {
|
|
19
21
|
throw new Error(message || "Failed");
|
|
20
22
|
}
|
|
@@ -110,6 +112,10 @@ function isEmpty(value) {
|
|
|
110
112
|
* - arrays
|
|
111
113
|
*/
|
|
112
114
|
function areEqual(a, b, strictOrder) {
|
|
115
|
+
const temporalEquals = (0, temporal_1.areSupportedTemporalValuesEqual)(a, b);
|
|
116
|
+
if (temporalEquals !== undefined) {
|
|
117
|
+
return temporalEquals;
|
|
118
|
+
}
|
|
113
119
|
if ((0, is_plain_object_1.isPlainObject)(a)) {
|
|
114
120
|
return (0, deepEquals_1.deepEquals)((0, mobx_1.toJS)(a), (0, mobx_1.toJS)(b));
|
|
115
121
|
}
|
|
@@ -131,6 +137,33 @@ function areEqual(a, b, strictOrder) {
|
|
|
131
137
|
function hasToJSON(o) {
|
|
132
138
|
return !!(o && typeof o === "object" && "toJSON" in o);
|
|
133
139
|
}
|
|
140
|
+
/** Normalizes values into stable object-hash-safe data using our existing `toJSON` semantics. */
|
|
141
|
+
function normalizeHashValue(value, seen = new WeakMap()) {
|
|
142
|
+
if (hasToJSON(value)) {
|
|
143
|
+
return normalizeHashValue(value.toJSON(), seen);
|
|
144
|
+
}
|
|
145
|
+
if (Array.isArray(value)) {
|
|
146
|
+
if (seen.has(value)) {
|
|
147
|
+
return seen.get(value);
|
|
148
|
+
}
|
|
149
|
+
const result = [];
|
|
150
|
+
seen.set(value, result);
|
|
151
|
+
value.forEach((entry) => result.push(normalizeHashValue(entry, seen)));
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
if (value && typeof value === "object" && (0, is_plain_object_1.isPlainObject)(value)) {
|
|
155
|
+
if (seen.has(value)) {
|
|
156
|
+
return seen.get(value);
|
|
157
|
+
}
|
|
158
|
+
const result = {};
|
|
159
|
+
seen.set(value, result);
|
|
160
|
+
Object.entries(value).forEach(([key, entry]) => {
|
|
161
|
+
result[key] = normalizeHashValue(entry, seen);
|
|
162
|
+
});
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
134
167
|
/** Make a clone of `obj`, but only recurse into POJOs and Arrays...and stores. */
|
|
135
168
|
function deepClone(obj, map = new WeakMap()) {
|
|
136
169
|
if (obj && typeof obj === "object" && ((0, is_plain_object_1.isPlainObject)(obj) || Array.isArray(obj) || (0, mobx_1.isObservable)(obj))) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homebound/form-state",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.29.0",
|
|
4
4
|
"author": "Homebound",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@types/object-hash": "^3.0.6",
|
|
34
34
|
"is-plain-object": "^5.0.0",
|
|
35
|
-
"object-hash": "^3.0.0"
|
|
35
|
+
"object-hash": "^3.0.0",
|
|
36
|
+
"temporal-polyfill": "^0.3.2"
|
|
36
37
|
},
|
|
37
38
|
"peerDependencies": {
|
|
38
39
|
"mobx": ">=6",
|