@homebound/form-state 2.22.0 → 2.22.2
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 +4 -4
- package/dist/formState.test.js +22 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/useFormState.test.js +12 -5
- package/dist/utils.js +8 -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.
|
|
@@ -188,12 +188,12 @@ function newObjectState(config, parentState, parentListState, instance, key, may
|
|
|
188
188
|
// An internal helper method to see if `other` is for "the same entity" as our current row
|
|
189
189
|
isSameEntity(other) {
|
|
190
190
|
const idField = getFields(this).find((f) => f._isIdKey);
|
|
191
|
-
if (!idField)
|
|
191
|
+
if (!idField)
|
|
192
192
|
return false;
|
|
193
|
-
|
|
194
|
-
// If the otherIdValue is undefined, it's a new entity so can't be the same as us
|
|
193
|
+
const ourIdValue = idField.value;
|
|
195
194
|
const otherIdValue = other[idField.key];
|
|
196
|
-
|
|
195
|
+
// If the otherIdValue is undefined, it's a new entity so can't be the same as us
|
|
196
|
+
return otherIdValue !== undefined && otherIdValue === ourIdValue;
|
|
197
197
|
},
|
|
198
198
|
};
|
|
199
199
|
proxy = (0, mobx_1.makeAutoObservable)(obj, {
|
package/dist/formState.test.js
CHANGED
|
@@ -965,12 +965,14 @@ describe("formState", () => {
|
|
|
965
965
|
const formState = (0, objectField_1.createObjectState)({
|
|
966
966
|
firstName: { type: "value" },
|
|
967
967
|
lastName: { type: "value" },
|
|
968
|
-
fullName: { type: "value"
|
|
968
|
+
fullName: { type: "value" },
|
|
969
969
|
}, new ObservableObject());
|
|
970
970
|
expect(formState.firstName.value).toEqual("first");
|
|
971
971
|
expect(formState.fullName.value).toEqual("first last");
|
|
972
|
+
expect(formState.fullName.dirty).toEqual(false);
|
|
972
973
|
formState.firstName.value = "change";
|
|
973
974
|
expect(formState.fullName.value).toEqual("change last");
|
|
975
|
+
expect(formState.fullName.dirty).toEqual(true);
|
|
974
976
|
formState.revertChanges();
|
|
975
977
|
expect(formState.firstName.value).toEqual("first");
|
|
976
978
|
});
|
|
@@ -1524,6 +1526,25 @@ describe("formState", () => {
|
|
|
1524
1526
|
expect(a.id._isIdKey).toBe(false);
|
|
1525
1527
|
expect(a.otherId._isIdKey).toBe(true);
|
|
1526
1528
|
});
|
|
1529
|
+
it("uses id key to recognize same entities", () => {
|
|
1530
|
+
// With b1 having an id
|
|
1531
|
+
const b1 = (0, objectField_1.createObjectState)({ id: { type: "value" } }, { id: "b:1" });
|
|
1532
|
+
expect(b1.isSameEntity({ id: "b:1" })).toBe(true);
|
|
1533
|
+
expect(b1.isSameEntity({ id: "b:2" })).toBe(false);
|
|
1534
|
+
expect(b1.isSameEntity({ id: undefined })).toBe(false);
|
|
1535
|
+
expect(b1.isSameEntity({})).toBe(false);
|
|
1536
|
+
// With b2 having no id
|
|
1537
|
+
const b2 = (0, objectField_1.createObjectState)({ id: { type: "value" } }, {});
|
|
1538
|
+
expect(b2.isSameEntity({ id: "b:1" })).toBe(false);
|
|
1539
|
+
expect(b2.isSameEntity({ id: undefined })).toBe(false);
|
|
1540
|
+
expect(b2.isSameEntity({})).toBe(false);
|
|
1541
|
+
// With b3 having no id key
|
|
1542
|
+
const b3 = (0, objectField_1.createObjectState)({ title: { type: "value" } }, { title: "b3" });
|
|
1543
|
+
expect(b3.isSameEntity({ title: "b3" })).toBe(false);
|
|
1544
|
+
// With b4 having a different id key
|
|
1545
|
+
const b4 = (0, objectField_1.createObjectState)({ title: { type: "value", isIdKey: true } }, { title: "b4" });
|
|
1546
|
+
expect(b4.isSameEntity({ title: "b4" })).toBe(true);
|
|
1547
|
+
});
|
|
1527
1548
|
});
|
|
1528
1549
|
class ObservableObject {
|
|
1529
1550
|
constructor() {
|
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");
|
|
@@ -244,9 +244,11 @@ describe("useFormState", () => {
|
|
|
244
244
|
// Given a component
|
|
245
245
|
// And it's using a class/mobx proxy as the basis for the data
|
|
246
246
|
class AuthorRow {
|
|
247
|
-
constructor(firstName, lastName) {
|
|
247
|
+
constructor(firstName, lastName, books) {
|
|
248
248
|
this.firstName = firstName;
|
|
249
249
|
this.lastName = lastName;
|
|
250
|
+
this.books = books;
|
|
251
|
+
(0, mobx_1.makeAutoObservable)(this);
|
|
250
252
|
}
|
|
251
253
|
get fullName() {
|
|
252
254
|
return this.firstName + " " + this.lastName;
|
|
@@ -254,27 +256,32 @@ describe("useFormState", () => {
|
|
|
254
256
|
}
|
|
255
257
|
function TestComponent() {
|
|
256
258
|
// And we have two sets of data
|
|
257
|
-
const data1 = { firstName: "f1", lastName: "l1" };
|
|
258
|
-
const data2 = { firstName: "f2", lastName: "l2" };
|
|
259
|
+
const data1 = { firstName: "f1", lastName: "l1", books: [] };
|
|
260
|
+
const data2 = { firstName: "f2", lastName: "l2", books: [{ title: "t1" }] };
|
|
259
261
|
// And we start out with data1
|
|
260
262
|
const [data, setData] = (0, react_2.useState)(data1);
|
|
261
|
-
const author = (0, react_2.useMemo)(() => new AuthorRow(data.firstName, data.lastName), [data]);
|
|
263
|
+
const author = (0, react_2.useMemo)(() => new AuthorRow(data.firstName, data.lastName, data.books), [data]);
|
|
262
264
|
const config = (0, react_2.useMemo)(() => ({
|
|
263
265
|
firstName: { type: "value" },
|
|
264
266
|
lastName: { type: "value" },
|
|
265
267
|
fullName: { type: "value", computed: true },
|
|
268
|
+
books: { type: "list", config: { title: { type: "value" } } },
|
|
266
269
|
}), []);
|
|
267
270
|
const form = (0, useFormState_1.useFormState)({ config, init: { input: author, map: (a) => a } });
|
|
268
|
-
return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: form.firstName.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "fullName", children: form.fullName.value }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "refreshData", onClick: () => setData(data2) })] })) }));
|
|
271
|
+
return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: form.firstName.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "fullName", children: form.fullName.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "booksLength", children: form.books.rows.length }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "booksDirty", children: String(form.books.touched) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "refreshData", onClick: () => setData(data2) })] })) }));
|
|
269
272
|
}
|
|
270
273
|
// And we start out with the initial query data
|
|
271
274
|
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
272
275
|
expect(r.firstName.textContent).toEqual("f1");
|
|
273
276
|
expect(r.fullName.textContent).toEqual("f1 l1");
|
|
277
|
+
expect(r.booksLength.textContent).toEqual("0");
|
|
278
|
+
expect(r.booksDirty.textContent).toEqual("false");
|
|
274
279
|
// When the new query is ran i.e. due to a cache refresh
|
|
275
280
|
(0, rtl_utils_1.click)(r.refreshData);
|
|
276
281
|
expect(r.firstName.textContent).toEqual("f2");
|
|
277
282
|
expect(r.fullName.textContent).toEqual("f2 l2");
|
|
283
|
+
expect(r.booksLength.textContent).toEqual("1");
|
|
284
|
+
expect(r.booksDirty.textContent).toEqual("false");
|
|
278
285
|
});
|
|
279
286
|
it("can trigger auto save for fields in list that were initially undefined", async () => {
|
|
280
287
|
const autoSave = jest.fn();
|
package/dist/utils.js
CHANGED
|
@@ -133,7 +133,7 @@ function deepClone(obj, map = new WeakMap()) {
|
|
|
133
133
|
return map.get(obj);
|
|
134
134
|
const result = Array.isArray(obj) ? [] : {};
|
|
135
135
|
map.set(obj, result);
|
|
136
|
-
Object.assign(result, ...
|
|
136
|
+
Object.assign(result, ...getAllPropertyNames(obj).map((key) => ({ [key]: deepClone(obj[key], map) })));
|
|
137
137
|
return result;
|
|
138
138
|
}
|
|
139
139
|
else {
|
|
@@ -141,3 +141,10 @@ function deepClone(obj, map = new WeakMap()) {
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
exports.deepClone = deepClone;
|
|
144
|
+
/** Returns all property names, including mobx computeds (non-enumerable) & inherited. */
|
|
145
|
+
function getAllPropertyNames(obj) {
|
|
146
|
+
const proto = Object.getPrototypeOf(obj);
|
|
147
|
+
// Don't crawl up into Object.prototype, or into arrays/observable arrays
|
|
148
|
+
const inherited = proto && proto !== Object.prototype && !Array.isArray(obj) ? getAllPropertyNames(proto) : [];
|
|
149
|
+
return [...new Set(Object.getOwnPropertyNames(obj).concat(inherited))];
|
|
150
|
+
}
|