@homebound/form-state 2.26.11 → 2.27.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
CHANGED
|
@@ -56,10 +56,14 @@ export type ListFieldConfig<U> = {
|
|
|
56
56
|
*
|
|
57
57
|
* When incremental, we only include changed rows in `changedValue`.
|
|
58
58
|
*
|
|
59
|
+
* When deep-exhaustive, we include all rows in `changedValue` and also
|
|
60
|
+
* all fields in the child entities, i.e. not just placeholder `{ id: "b:1" }`
|
|
61
|
+
* values for otherwise-unchanged children.
|
|
62
|
+
*
|
|
59
63
|
* Defaults to `exhaustive` b/c that is the safest and also Joist's
|
|
60
64
|
* default behavior.
|
|
61
65
|
*/
|
|
62
|
-
update?: "exhaustive" | "incremental";
|
|
66
|
+
update?: "exhaustive" | "incremental" | "deep-exhaustive";
|
|
63
67
|
/** Set to not consider the order of the list when evaluating changed/dirty states, defaults true. */
|
|
64
68
|
strictOrder?: false;
|
|
65
69
|
};
|
|
@@ -8,4 +8,4 @@ export interface ListFieldState<U> extends FieldState<U[]> {
|
|
|
8
8
|
add(value: U, index?: number): void;
|
|
9
9
|
remove(indexOrValue: number | U): void;
|
|
10
10
|
}
|
|
11
|
-
export declare function newListFieldState<T, K extends keyof T, U>(parentCopy: T, parentInstance: T, parentState: () => ObjectState<T>, key: K, rules: Rule<readonly ObjectState<U>[]>[], listConfig: ListFieldConfig<U>, config: ObjectFieldConfig<U>, strictOrder: boolean, maybeAutoSave: () => void): ListFieldState<U>;
|
|
11
|
+
export declare function newListFieldState<T, K extends keyof T, U>(parentCopy: T, parentInstance: T, parentState: () => ObjectState<T>, key: K, rules: Rule<readonly ObjectState<U>[]>[], listConfig: ListFieldConfig<U>, config: ObjectFieldConfig<U>, strictOrder: boolean, maybeAutoSave: () => void, deepExhaustive: boolean): ListFieldState<U>;
|
package/dist/fields/listField.js
CHANGED
|
@@ -13,7 +13,7 @@ function newListFieldState(
|
|
|
13
13
|
// parentCopy is objectField's deepClone-d `originalCopy` that it/we use for `dirty` checking
|
|
14
14
|
parentCopy,
|
|
15
15
|
// parentInstance is objectField's `instance`, that we're currently mutating
|
|
16
|
-
parentInstance, parentState, key, rules, listConfig, config, strictOrder, maybeAutoSave) {
|
|
16
|
+
parentInstance, parentState, key, rules, listConfig, config, strictOrder, maybeAutoSave, deepExhaustive) {
|
|
17
17
|
var _a;
|
|
18
18
|
// Keep a map of "item in the parentInstance list" -> "that item's ObjectState"
|
|
19
19
|
const rowMap = new Map();
|
|
@@ -30,6 +30,7 @@ parentInstance, parentState, key, rules, listConfig, config, strictOrder, maybeA
|
|
|
30
30
|
});
|
|
31
31
|
// Given a child POJO (or a copy/clone of a child POJO), return its ObjectState wrapper.
|
|
32
32
|
function getOrCreateChildState(child, opts) {
|
|
33
|
+
var _a;
|
|
33
34
|
let childState = rowMap.get(child);
|
|
34
35
|
// If we're being reverted to our originalValue, i.e. values is actually
|
|
35
36
|
// a list of copies, use the copyMap to recover the non-copy original value
|
|
@@ -50,7 +51,7 @@ parentInstance, parentState, key, rules, listConfig, config, strictOrder, maybeA
|
|
|
50
51
|
}
|
|
51
52
|
// If we didn't have an existing child, just make a new object state
|
|
52
53
|
if (!childState) {
|
|
53
|
-
childState = (0, objectField_1.newObjectState)(config, parentState, undefined, list, child, undefined, maybeAutoSave);
|
|
54
|
+
childState = (0, objectField_1.newObjectState)(config, parentState, undefined, list, child, undefined, maybeAutoSave, (_a = listConfig.update === "deep-exhaustive") !== null && _a !== void 0 ? _a : deepExhaustive);
|
|
54
55
|
rowMap.set(child, childState);
|
|
55
56
|
}
|
|
56
57
|
return childState;
|
|
@@ -223,17 +224,20 @@ parentInstance, parentState, key, rules, listConfig, config, strictOrder, maybeA
|
|
|
223
224
|
// Once matched, we don't this to be an addedRow anymore
|
|
224
225
|
addedRows.delete(currentItem);
|
|
225
226
|
}
|
|
226
|
-
else if (childState.dirty
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
227
|
+
else if (!childState.dirty && !addedRows.has(currentItem)) {
|
|
228
|
+
// Local is not dirty/added, and it's not upstream, so let it get removed
|
|
229
|
+
}
|
|
230
|
+
else if (hasOpKey && currentItem.op === "delete") {
|
|
231
|
+
// We were locally marked as deleted, and not finding a match is the server acking that we're gone
|
|
232
|
+
}
|
|
233
|
+
else if (currentItems.length === incomingItems.length &&
|
|
234
|
+
!!idKey &&
|
|
235
|
+
currentItem[idKey] === undefined) {
|
|
236
|
+
// Assume our newly-assigned id is coming back
|
|
234
237
|
}
|
|
235
238
|
else {
|
|
236
|
-
//
|
|
239
|
+
// If no incoming, but we're dirty (see above) and not deleted (see above), keep the WIP change
|
|
240
|
+
mergedItems.push(currentItem);
|
|
237
241
|
}
|
|
238
242
|
}
|
|
239
243
|
// Then, add any incoming items that don't exist in current
|
|
@@ -59,5 +59,5 @@ export declare function createObjectState<T>(config: ObjectConfig<T>, instance:
|
|
|
59
59
|
maybeAutoSave?: () => void;
|
|
60
60
|
}): ObjectState<T>;
|
|
61
61
|
/** A more internal version of `createObjectState`. */
|
|
62
|
-
export declare function newObjectState<T, P = any>(config: ObjectFieldConfig<T>, parentState: (() => ObjectState<P>) | undefined, parentInstance: P | undefined, parentListState: FieldState<any> | undefined, instance: T, key: keyof T | undefined, maybeAutoSave: () => void): ObjectState<T>;
|
|
62
|
+
export declare function newObjectState<T, P = any>(config: ObjectFieldConfig<T>, parentState: (() => ObjectState<P>) | undefined, parentInstance: P | undefined, parentListState: FieldState<any> | undefined, instance: T, key: keyof T | undefined, maybeAutoSave: () => void, deepExhaustive: boolean): ObjectState<T>;
|
|
63
63
|
export {};
|
|
@@ -21,10 +21,10 @@ function fragment(value) {
|
|
|
21
21
|
*/
|
|
22
22
|
function createObjectState(config, instance, opts = {}) {
|
|
23
23
|
const noop = () => { };
|
|
24
|
-
return newObjectState({ type: "object", config }, undefined, undefined, undefined, instance, undefined, opts.maybeAutoSave || noop);
|
|
24
|
+
return newObjectState({ type: "object", config }, undefined, undefined, undefined, instance, undefined, opts.maybeAutoSave || noop, false);
|
|
25
25
|
}
|
|
26
26
|
/** A more internal version of `createObjectState`. */
|
|
27
|
-
function newObjectState(config, parentState, parentInstance, parentListState, instance, key, maybeAutoSave) {
|
|
27
|
+
function newObjectState(config, parentState, parentInstance, parentListState, instance, key, maybeAutoSave, deepExhaustive) {
|
|
28
28
|
// This is what we return, but we only know it's value until we call `observable`, so
|
|
29
29
|
// we create a mutable variable to capture it so that we can create fields/call their
|
|
30
30
|
// constructors and give them a way to access it later.
|
|
@@ -55,7 +55,7 @@ function newObjectState(config, parentState, parentInstance, parentListState, in
|
|
|
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
|
}
|
|
57
57
|
else if (config.type === "list") {
|
|
58
|
-
field = (0, listField_1.newListFieldState)(originalCopy, instance, getObjectState, key, config.rules || [], config, { type: "object", config: config.config }, (_d = config.strictOrder) !== null && _d !== void 0 ? _d : true, maybeAutoSave);
|
|
58
|
+
field = (0, listField_1.newListFieldState)(originalCopy, instance, getObjectState, key, config.rules || [], config, { type: "object", config: config.config }, (_d = config.strictOrder) !== null && _d !== void 0 ? _d : true, maybeAutoSave, deepExhaustive);
|
|
59
59
|
}
|
|
60
60
|
else if (config.type === "object") {
|
|
61
61
|
// Because our objectField will fundamentally want to do `child.firstName.set(...)` or
|
|
@@ -65,7 +65,7 @@ function newObjectState(config, parentState, parentInstance, parentListState, in
|
|
|
65
65
|
if (!instance[key]) {
|
|
66
66
|
instance[key] = {};
|
|
67
67
|
}
|
|
68
|
-
field = newObjectState(config, getObjectState, instance, undefined, instance[key], key, maybeAutoSave);
|
|
68
|
+
field = newObjectState(config, getObjectState, instance, undefined, instance[key], key, maybeAutoSave, deepExhaustive);
|
|
69
69
|
}
|
|
70
70
|
else if (config.type === "fragment") {
|
|
71
71
|
field = (0, fragmentField_1.newFragmentField)(instance, key);
|
|
@@ -223,7 +223,9 @@ function newObjectState(config, parentState, parentInstance, parentListState, in
|
|
|
223
223
|
f.changedValue &&
|
|
224
224
|
Object.entries(f.changedValue).length > 0) ||
|
|
225
225
|
// ...or they're non-empty sub-lists
|
|
226
|
-
(this.isNewEntity && f._kind === "list" && ((_a = f.value) === null || _a === void 0 ? void 0 : _a.length) > 0)
|
|
226
|
+
(this.isNewEntity && f._kind === "list" && ((_a = f.value) === null || _a === void 0 ? void 0 : _a.length) > 0) ||
|
|
227
|
+
// ...or the parent list is deep-exhaustive
|
|
228
|
+
deepExhaustive) {
|
|
227
229
|
result[f.key] = f.changedValue;
|
|
228
230
|
}
|
|
229
231
|
});
|
package/dist/formState.test.js
CHANGED
|
@@ -258,6 +258,27 @@ describe("formState", () => {
|
|
|
258
258
|
// And we're not dirty
|
|
259
259
|
expect(state.books.dirty).toBe(false);
|
|
260
260
|
});
|
|
261
|
+
it("list value can refresh and merge children that were added", () => {
|
|
262
|
+
// Given a list field that is currently populated with 1 child
|
|
263
|
+
const a1 = { id: "a:1", books: [{ id: "b:1", title: "b1" }] };
|
|
264
|
+
const state = createAuthorInputState(a1);
|
|
265
|
+
// When we add a new book
|
|
266
|
+
state.books.add({ title: "b2" });
|
|
267
|
+
// When we refresh it with both books, and the 2nd book now has its id assigned
|
|
268
|
+
state.set({
|
|
269
|
+
books: [
|
|
270
|
+
{ id: "b:1", title: "b1" },
|
|
271
|
+
{ id: "b:2", title: "b2" },
|
|
272
|
+
],
|
|
273
|
+
}, { refreshing: true });
|
|
274
|
+
// Then wes till have two books
|
|
275
|
+
expect(state.books.value).toEqual([
|
|
276
|
+
{ id: "b:1", title: "b1" },
|
|
277
|
+
{ id: "b:2", title: "b2" },
|
|
278
|
+
]);
|
|
279
|
+
// And we're not dirty
|
|
280
|
+
expect(state.books.dirty).toBe(false);
|
|
281
|
+
});
|
|
261
282
|
it("list value can observe changes", () => {
|
|
262
283
|
const b1 = { title: "t1" };
|
|
263
284
|
const a1 = { firstName: "a1", books: [b1] };
|
|
@@ -1318,7 +1339,7 @@ describe("formState", () => {
|
|
|
1318
1339
|
// And we don't include it in the changedValue
|
|
1319
1340
|
expect(formState.changedValue).toEqual({ firstName: "First" });
|
|
1320
1341
|
});
|
|
1321
|
-
it("can return only changed list fields", () => {
|
|
1342
|
+
it("can return only changed and exhaustive list fields", () => {
|
|
1322
1343
|
// Given an author with some books
|
|
1323
1344
|
const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, {
|
|
1324
1345
|
id: "a:1",
|
|
@@ -1409,6 +1430,40 @@ describe("formState", () => {
|
|
|
1409
1430
|
books: [{ id: "b:1", title: "t1b" }],
|
|
1410
1431
|
});
|
|
1411
1432
|
});
|
|
1433
|
+
it("can return only changed but deep exhaustive list fields", () => {
|
|
1434
|
+
// Given an author
|
|
1435
|
+
const formState = (0, objectField_1.createObjectState)({
|
|
1436
|
+
id: { type: "value" },
|
|
1437
|
+
// And the books collection is marked as deep-exhaustive
|
|
1438
|
+
books: {
|
|
1439
|
+
type: "list",
|
|
1440
|
+
update: "deep-exhaustive",
|
|
1441
|
+
config: {
|
|
1442
|
+
id: { type: "value" },
|
|
1443
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
1444
|
+
},
|
|
1445
|
+
},
|
|
1446
|
+
}, {
|
|
1447
|
+
id: "a:1",
|
|
1448
|
+
firstName: "f",
|
|
1449
|
+
books: [
|
|
1450
|
+
{ id: "b:1", title: "t1" },
|
|
1451
|
+
{ id: "b:2", title: "t2" },
|
|
1452
|
+
],
|
|
1453
|
+
});
|
|
1454
|
+
// And initially nothing is changed
|
|
1455
|
+
expect(formState.changedValue).toEqual({ id: "a:1" });
|
|
1456
|
+
// When we change the 1st book
|
|
1457
|
+
formState.books.rows[0].title.value = "t1b";
|
|
1458
|
+
// Then we see all books & all attributes
|
|
1459
|
+
expect(formState.changedValue).toEqual({
|
|
1460
|
+
id: "a:1",
|
|
1461
|
+
books: [
|
|
1462
|
+
{ id: "b:1", title: "t1b" },
|
|
1463
|
+
{ id: "b:2", title: "t2" },
|
|
1464
|
+
],
|
|
1465
|
+
});
|
|
1466
|
+
});
|
|
1412
1467
|
it("uses the op key as a hint to use incremental list behavior", () => {
|
|
1413
1468
|
// Given an author
|
|
1414
1469
|
const formState = (0, objectField_1.createObjectState)({
|