@homebound/form-state 2.22.1 → 2.22.3
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/fields/listField.js +32 -33
- package/dist/fields/objectField.js +6 -4
- package/dist/fields/valueField.js +8 -0
- package/dist/formState.test.js +40 -6
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/package.json +1 -1
package/dist/fields/listField.js
CHANGED
|
@@ -19,6 +19,31 @@ function newListFieldState(parentCopy, parentInstance, parentState, key, rules,
|
|
|
19
19
|
((_a = parentCopy[key]) !== null && _a !== void 0 ? _a : []).forEach((copy, i) => {
|
|
20
20
|
copyMap.set(copy, parentInstance[key][i]);
|
|
21
21
|
});
|
|
22
|
+
// Given a child POJO (or a copy/clone of a child POJO), return its ObjectState wrapper.
|
|
23
|
+
function getOrCreateChildState(child, opts) {
|
|
24
|
+
let childState = rowMap.get(child);
|
|
25
|
+
// If we're being reverted to our originalValue, i.e. values is actually
|
|
26
|
+
// a list of copies, use the copyMap to recover the non-copy original value
|
|
27
|
+
if (!childState && copyMap.has(child)) {
|
|
28
|
+
childState = rowMap.get(copyMap.get(child));
|
|
29
|
+
}
|
|
30
|
+
// Look for an existing child (requires having an id key configured)
|
|
31
|
+
if (!childState) {
|
|
32
|
+
for (const [, otherState] of rowMap.entries()) {
|
|
33
|
+
if (otherState.isSameEntity(child)) {
|
|
34
|
+
otherState.set(child, opts);
|
|
35
|
+
rowMap.set(child, otherState);
|
|
36
|
+
return otherState;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// If we didn't have an existing child, just make a new object state
|
|
41
|
+
if (!childState) {
|
|
42
|
+
childState = (0, objectField_1.newObjectState)(config, parentState, list, child, undefined, maybeAutoSave);
|
|
43
|
+
rowMap.set(child, childState);
|
|
44
|
+
}
|
|
45
|
+
return childState;
|
|
46
|
+
}
|
|
22
47
|
const list = {
|
|
23
48
|
key: key,
|
|
24
49
|
// Our fundamental state of wrapped Us
|
|
@@ -75,15 +100,7 @@ function newListFieldState(parentCopy, parentInstance, parentState, key, rules,
|
|
|
75
100
|
(0, utils_1.fail)();
|
|
76
101
|
// Avoid using `this.value` to avoid registering `_childTick` as a dependency
|
|
77
102
|
const value = parentInstance[key];
|
|
78
|
-
return (value || []).map((child) =>
|
|
79
|
-
// Because we're reading from this.value, child will be the proxy version
|
|
80
|
-
let childState = rowMap.get(child);
|
|
81
|
-
if (!childState) {
|
|
82
|
-
childState = (0, objectField_1.newObjectState)(config, parentState, list, child, undefined, maybeAutoSave);
|
|
83
|
-
rowMap.set(child, childState);
|
|
84
|
-
}
|
|
85
|
-
return childState;
|
|
86
|
-
});
|
|
103
|
+
return (value || []).map((child) => getOrCreateChildState(child));
|
|
87
104
|
},
|
|
88
105
|
// TODO Should this be true when all rows are touched?
|
|
89
106
|
get touched() {
|
|
@@ -144,30 +161,12 @@ function newListFieldState(parentCopy, parentInstance, parentState, key, rules,
|
|
|
144
161
|
if (this.readOnly && !opts.resetting && !opts.refreshing) {
|
|
145
162
|
throw new Error(`${String(key)} is currently readOnly`);
|
|
146
163
|
}
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
childState = rowMap.get(copyMap.get(value));
|
|
154
|
-
}
|
|
155
|
-
if (!childState) {
|
|
156
|
-
// Look for an existing child (requires having an id key configured)
|
|
157
|
-
for (const [, otherState] of rowMap.entries()) {
|
|
158
|
-
if (otherState.isSameEntity(value)) {
|
|
159
|
-
otherState.set(value, opts);
|
|
160
|
-
rowMap.set(value, otherState);
|
|
161
|
-
return otherState.value;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
// If we didn't have an existing child, just make a new object state
|
|
165
|
-
childState = (0, objectField_1.createObjectState)(config, value, { maybeAutoSave });
|
|
166
|
-
rowMap.set(value, childState);
|
|
167
|
-
}
|
|
168
|
-
// Return the already-observable'd value so that our `parent.value[key] = values` doesn't re-proxy things
|
|
169
|
-
return childState.value;
|
|
170
|
-
});
|
|
164
|
+
// Given the values, see if we've got existing child states, and use their
|
|
165
|
+
// value if so. I.e. this covers revertChanges doing `set(originalValue)` and
|
|
166
|
+
// passing us cloned rows from the parentCopy, but `getOrCreateChildState` will
|
|
167
|
+
// use the `copyMap` to recover the non-cloned rows, to avoid promoting the clone
|
|
168
|
+
// into a real row.
|
|
169
|
+
parentInstance[key] = values.map((child) => getOrCreateChildState(child, opts).value);
|
|
171
170
|
// Make sure to tick first so that `setOriginalValue` sees the latest `rows`
|
|
172
171
|
_tick.value++;
|
|
173
172
|
// Reset originalCopy so that our dirty checks have the right # of rows.
|
|
@@ -163,6 +163,8 @@ function newObjectState(config, parentState, parentListState, instance, key, may
|
|
|
163
163
|
get changedValue() {
|
|
164
164
|
const result = {};
|
|
165
165
|
getFields(this).forEach((f) => {
|
|
166
|
+
if (f._computed)
|
|
167
|
+
return;
|
|
166
168
|
if (f.dirty ||
|
|
167
169
|
// If the caller used useFormState.ifUndefined to provide some default values, then those keys may not
|
|
168
170
|
// look dirty, but if we're new we should include them anyway.
|
|
@@ -188,12 +190,12 @@ function newObjectState(config, parentState, parentListState, instance, key, may
|
|
|
188
190
|
// An internal helper method to see if `other` is for "the same entity" as our current row
|
|
189
191
|
isSameEntity(other) {
|
|
190
192
|
const idField = getFields(this).find((f) => f._isIdKey);
|
|
191
|
-
if (!idField)
|
|
193
|
+
if (!idField)
|
|
192
194
|
return false;
|
|
193
|
-
|
|
194
|
-
// If the otherIdValue is undefined, it's a new entity so can't be the same as us
|
|
195
|
+
const ourIdValue = idField.value;
|
|
195
196
|
const otherIdValue = other[idField.key];
|
|
196
|
-
|
|
197
|
+
// If the otherIdValue is undefined, it's a new entity so can't be the same as us
|
|
198
|
+
return otherIdValue !== undefined && otherIdValue === ourIdValue;
|
|
197
199
|
},
|
|
198
200
|
};
|
|
199
201
|
proxy = (0, mobx_1.makeAutoObservable)(obj, {
|
|
@@ -18,6 +18,8 @@ function newValueFieldState(parentCopy, parentInstance, parentState, key, rules,
|
|
|
18
18
|
_readOnly: readOnly || false,
|
|
19
19
|
_loading: false,
|
|
20
20
|
_focused: false,
|
|
21
|
+
// Expose so computed can be skipped in changedValue
|
|
22
|
+
_computed: computed,
|
|
21
23
|
_isIdKey: isIdKey,
|
|
22
24
|
_isDeleteKey: isDeleteKey,
|
|
23
25
|
_isReadOnlyKey: isReadOnlyKey,
|
|
@@ -54,6 +56,12 @@ function newValueFieldState(parentCopy, parentInstance, parentState, key, rules,
|
|
|
54
56
|
},
|
|
55
57
|
// For primitive fields, the changed value is just the value.
|
|
56
58
|
get changedValue() {
|
|
59
|
+
// Usually if we see a field being unset, we set `parent[key] = null`, but if we're wrapping
|
|
60
|
+
// a mobx observable object, it might have changed to `undefined` without us being able to
|
|
61
|
+
// tell it to be `null` (and potentially break the type contract of `string | undefined`).
|
|
62
|
+
// So detect un-set-ness here and return `null`.
|
|
63
|
+
if (this.value === undefined && this.originalValue !== undefined)
|
|
64
|
+
return null;
|
|
57
65
|
return this.value;
|
|
58
66
|
},
|
|
59
67
|
get valid() {
|
package/dist/formState.test.js
CHANGED
|
@@ -979,11 +979,7 @@ describe("formState", () => {
|
|
|
979
979
|
it("can observe observable objects being mutated directly", () => {
|
|
980
980
|
// Given a mobx class with a computed
|
|
981
981
|
const instance = new ObservableObject();
|
|
982
|
-
const formState = (0, objectField_1.createObjectState)(
|
|
983
|
-
firstName: { type: "value" },
|
|
984
|
-
lastName: { type: "value" },
|
|
985
|
-
fullName: { type: "value", computed: true },
|
|
986
|
-
}, instance);
|
|
982
|
+
const formState = (0, objectField_1.createObjectState)(authorWithFullName, instance);
|
|
987
983
|
expect(formState.firstName.value).toEqual("first");
|
|
988
984
|
expect(formState.fullName.value).toEqual("first last");
|
|
989
985
|
// And we hook up reactivity to the computed
|
|
@@ -999,9 +995,11 @@ describe("formState", () => {
|
|
|
999
995
|
expect(formState.fullName.value).toEqual("change last");
|
|
1000
996
|
// And when the underlying mobx class has a field unset
|
|
1001
997
|
instance.firstName = undefined;
|
|
1002
|
-
// Then our
|
|
998
|
+
// Then our form-state computed re-runs again
|
|
1003
999
|
expect(numCalcs).toBe(3);
|
|
1004
1000
|
expect(formState.fullName.value).toEqual("undefined last");
|
|
1001
|
+
// And knows the field should be sent as null
|
|
1002
|
+
expect(formState.changedValue).toEqual({ firstName: null });
|
|
1005
1003
|
});
|
|
1006
1004
|
it("can set computeds that have setters", () => {
|
|
1007
1005
|
const formState = (0, objectField_1.createObjectState)({
|
|
@@ -1086,6 +1084,18 @@ describe("formState", () => {
|
|
|
1086
1084
|
// because this might trigger creating a throw-away entity
|
|
1087
1085
|
expect(formState.changedValue).toEqual({ firstName: "f" });
|
|
1088
1086
|
});
|
|
1087
|
+
it("changedValue skips computeds", () => {
|
|
1088
|
+
// Given a new author that calls full name
|
|
1089
|
+
const formState = (0, objectField_1.createObjectState)(authorWithFullName, new ObservableObject());
|
|
1090
|
+
expect(formState.fullName.value).toEqual("first last");
|
|
1091
|
+
// When the firstName changes
|
|
1092
|
+
formState.firstName.value = "First";
|
|
1093
|
+
// Then the fullName changes
|
|
1094
|
+
expect(formState.fullName.value).toBe("First last");
|
|
1095
|
+
expect(formState.fullName.dirty).toBe(true);
|
|
1096
|
+
// And we don't include it in the changedValue
|
|
1097
|
+
expect(formState.changedValue).toEqual({ firstName: "First" });
|
|
1098
|
+
});
|
|
1089
1099
|
it("can return only changed list fields", () => {
|
|
1090
1100
|
// Given an author with some books
|
|
1091
1101
|
const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, {
|
|
@@ -1526,6 +1536,25 @@ describe("formState", () => {
|
|
|
1526
1536
|
expect(a.id._isIdKey).toBe(false);
|
|
1527
1537
|
expect(a.otherId._isIdKey).toBe(true);
|
|
1528
1538
|
});
|
|
1539
|
+
it("uses id key to recognize same entities", () => {
|
|
1540
|
+
// With b1 having an id
|
|
1541
|
+
const b1 = (0, objectField_1.createObjectState)({ id: { type: "value" } }, { id: "b:1" });
|
|
1542
|
+
expect(b1.isSameEntity({ id: "b:1" })).toBe(true);
|
|
1543
|
+
expect(b1.isSameEntity({ id: "b:2" })).toBe(false);
|
|
1544
|
+
expect(b1.isSameEntity({ id: undefined })).toBe(false);
|
|
1545
|
+
expect(b1.isSameEntity({})).toBe(false);
|
|
1546
|
+
// With b2 having no id
|
|
1547
|
+
const b2 = (0, objectField_1.createObjectState)({ id: { type: "value" } }, {});
|
|
1548
|
+
expect(b2.isSameEntity({ id: "b:1" })).toBe(false);
|
|
1549
|
+
expect(b2.isSameEntity({ id: undefined })).toBe(false);
|
|
1550
|
+
expect(b2.isSameEntity({})).toBe(false);
|
|
1551
|
+
// With b3 having no id key
|
|
1552
|
+
const b3 = (0, objectField_1.createObjectState)({ title: { type: "value" } }, { title: "b3" });
|
|
1553
|
+
expect(b3.isSameEntity({ title: "b3" })).toBe(false);
|
|
1554
|
+
// With b4 having a different id key
|
|
1555
|
+
const b4 = (0, objectField_1.createObjectState)({ title: { type: "value", isIdKey: true } }, { title: "b4" });
|
|
1556
|
+
expect(b4.isSameEntity({ title: "b4" })).toBe(true);
|
|
1557
|
+
});
|
|
1529
1558
|
});
|
|
1530
1559
|
class ObservableObject {
|
|
1531
1560
|
constructor() {
|
|
@@ -1560,6 +1589,11 @@ const authorWithBooksConfig = configBuilders_1.f.config({
|
|
|
1560
1589
|
function createAuthorInputState(input, maybeAutoSave) {
|
|
1561
1590
|
return (0, objectField_1.createObjectState)(authorWithBooksConfig, input, { maybeAutoSave });
|
|
1562
1591
|
}
|
|
1592
|
+
const authorWithFullName = {
|
|
1593
|
+
firstName: { type: "value" },
|
|
1594
|
+
lastName: { type: "value" },
|
|
1595
|
+
fullName: { type: "value", computed: true },
|
|
1596
|
+
};
|
|
1563
1597
|
const authorWithAddressConfig = {
|
|
1564
1598
|
id: { type: "value" },
|
|
1565
1599
|
firstName: { type: "value" },
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { ObjectConfig } from "./config";
|
|
2
2
|
export { f } from "./configBuilders";
|
|
3
|
+
export { FragmentField } from "./fields/fragmentField";
|
|
3
4
|
export { ListFieldState } from "./fields/listField";
|
|
4
|
-
export { ObjectState, createObjectState } from "./fields/objectField";
|
|
5
|
+
export { Fragment, ObjectState, createObjectState, fragment } from "./fields/objectField";
|
|
5
6
|
export { FieldState, ValueAdapter } from "./fields/valueField";
|
|
6
7
|
export { Rule, required } from "./rules";
|
|
7
8
|
export { useFormState } from "./useFormState";
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.useFormStates = exports.useFormState = exports.required = exports.createObjectState = exports.f = void 0;
|
|
3
|
+
exports.useFormStates = exports.useFormState = exports.required = exports.fragment = exports.createObjectState = exports.f = void 0;
|
|
4
4
|
var configBuilders_1 = require("./configBuilders");
|
|
5
5
|
Object.defineProperty(exports, "f", { enumerable: true, get: function () { return configBuilders_1.f; } });
|
|
6
6
|
var objectField_1 = require("./fields/objectField");
|
|
7
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; } });
|
|
8
9
|
var rules_1 = require("./rules");
|
|
9
10
|
Object.defineProperty(exports, "required", { enumerable: true, get: function () { return rules_1.required; } });
|
|
10
11
|
var useFormState_1 = require("./useFormState");
|