@homebound/form-state 2.26.12 → 2.27.1
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 +5 -1
- package/dist/fields/listField.d.ts +1 -1
- package/dist/fields/listField.js +3 -2
- package/dist/fields/objectField.d.ts +1 -1
- package/dist/fields/objectField.js +7 -5
- package/dist/fields/valueField.js +24 -6
- package/dist/formState.test.js +66 -2
- package/dist/useFormState.test.js +36 -17
- package/package.json +1 -1
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;
|
|
@@ -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
|
});
|
|
@@ -11,6 +11,13 @@ function newValueFieldState(parentCopy, parentInstance, parentState, key, rules,
|
|
|
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 });
|
|
13
13
|
const _originalValueTick = (0, mobx_1.observable)({ value: 1 });
|
|
14
|
+
// Track "this is probably what we put into a mutation to the server" to allow
|
|
15
|
+
// use to better accept server acks/responses that change our initial submission.
|
|
16
|
+
//
|
|
17
|
+
// I.e. we submit `firstName: bob` and the server acks `firstName: Bob`, we should
|
|
18
|
+
// accept it if we can tell we haven't changed `bob` since our initial submission.
|
|
19
|
+
let hasInflightChangedValue = false;
|
|
20
|
+
let lastChangedValue = undefined;
|
|
14
21
|
const field = {
|
|
15
22
|
key: key,
|
|
16
23
|
touched: false,
|
|
@@ -60,6 +67,10 @@ function newValueFieldState(parentCopy, parentInstance, parentState, key, rules,
|
|
|
60
67
|
},
|
|
61
68
|
// For primitive fields, the changed value is just the value.
|
|
62
69
|
get changedValue() {
|
|
70
|
+
// Add some hints to let `set(..., { refreshing: true })` accept server-driven changes to our value,
|
|
71
|
+
// if the user hasn't changed it yet-again while the changedValue was in in-flight.
|
|
72
|
+
hasInflightChangedValue = true;
|
|
73
|
+
lastChangedValue = this.value;
|
|
63
74
|
// Usually if we see a field being unset, we set `parent[key] = null`, but if we're wrapping
|
|
64
75
|
// a mobx observable object, it might have changed to `undefined` without us being able to
|
|
65
76
|
// tell it to be `null` (and potentially break the type contract of `string | undefined`).
|
|
@@ -100,12 +111,18 @@ function newValueFieldState(parentCopy, parentInstance, parentState, key, rules,
|
|
|
100
111
|
if (this.readOnly && !opts.resetting && !opts.refreshing) {
|
|
101
112
|
throw new Error(`${String(key)} is currently readOnly`);
|
|
102
113
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
//
|
|
108
|
-
|
|
114
|
+
if (opts.refreshing && this.dirty) {
|
|
115
|
+
// See if we should ignore the incoming server-side value, to avoid dropping local WIP changes
|
|
116
|
+
const isAckingUnset = this.value === null && (value === null || value === undefined);
|
|
117
|
+
const acceptServerAck = hasInflightChangedValue && this.value === lastChangedValue;
|
|
118
|
+
// Ignore incoming values if we have changes (this.dirty) unless:
|
|
119
|
+
// - our latest change (this.value) matches the incoming value (value), i.e. the server
|
|
120
|
+
// is exactly acking our change, or
|
|
121
|
+
// - the user hasn't made any changes since `.changedValue` was put on the wire, i.e. the
|
|
122
|
+
// server received our value, but wanted to to change it.
|
|
123
|
+
const keepLocalWipChange = this.value !== value && !isAckingUnset && !acceptServerAck;
|
|
124
|
+
if (keepLocalWipChange)
|
|
125
|
+
return;
|
|
109
126
|
}
|
|
110
127
|
else if (computed && (opts.resetting || opts.refreshing)) {
|
|
111
128
|
// Computeds can't be either reset or refreshed
|
|
@@ -117,6 +134,7 @@ function newValueFieldState(parentCopy, parentInstance, parentState, key, rules,
|
|
|
117
134
|
// If a list of primitives was originally undefined, coerce `[]` to `undefined`
|
|
118
135
|
const coerceEmptyList = value && value instanceof Array && value.length === 0 && (0, utils_1.isEmpty)(this.originalValue);
|
|
119
136
|
const newValue = keepNull ? null : (0, utils_1.isEmpty)(value) || coerceEmptyList ? undefined : value;
|
|
137
|
+
hasInflightChangedValue = false;
|
|
120
138
|
// Set the value on our parent object
|
|
121
139
|
const changed = !(0, utils_1.areEqual)(newValue, this.value, strictOrder);
|
|
122
140
|
parentInstance[key] = newValue;
|
package/dist/formState.test.js
CHANGED
|
@@ -1339,7 +1339,7 @@ describe("formState", () => {
|
|
|
1339
1339
|
// And we don't include it in the changedValue
|
|
1340
1340
|
expect(formState.changedValue).toEqual({ firstName: "First" });
|
|
1341
1341
|
});
|
|
1342
|
-
it("can return only changed list fields", () => {
|
|
1342
|
+
it("can return only changed and exhaustive list fields", () => {
|
|
1343
1343
|
// Given an author with some books
|
|
1344
1344
|
const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, {
|
|
1345
1345
|
id: "a:1",
|
|
@@ -1430,6 +1430,40 @@ describe("formState", () => {
|
|
|
1430
1430
|
books: [{ id: "b:1", title: "t1b" }],
|
|
1431
1431
|
});
|
|
1432
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
|
+
});
|
|
1433
1467
|
it("uses the op key as a hint to use incremental list behavior", () => {
|
|
1434
1468
|
// Given an author
|
|
1435
1469
|
const formState = (0, objectField_1.createObjectState)({
|
|
@@ -1527,7 +1561,7 @@ describe("formState", () => {
|
|
|
1527
1561
|
formState.books.add({ title: "b2" });
|
|
1528
1562
|
expect(ticks).toEqual(2);
|
|
1529
1563
|
});
|
|
1530
|
-
it("
|
|
1564
|
+
it("keeps WIP changes for changed field", () => {
|
|
1531
1565
|
const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1532
1566
|
// Given first name is focused
|
|
1533
1567
|
formState.firstName.focus();
|
|
@@ -1542,6 +1576,36 @@ describe("formState", () => {
|
|
|
1542
1576
|
formState.firstName.set("fff");
|
|
1543
1577
|
expect(formState.firstName.value).toEqual("fff");
|
|
1544
1578
|
});
|
|
1579
|
+
it("accepts server result if we have no further changes", () => {
|
|
1580
|
+
const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, { id: "a:1", firstName: "f" });
|
|
1581
|
+
// Given first name has wip edits
|
|
1582
|
+
formState.firstName.set("ff");
|
|
1583
|
+
// When a mutation result sets both firstName and lastName
|
|
1584
|
+
formState.set({ firstName: "f2" }, { refreshing: true });
|
|
1585
|
+
// Then we don't overwrite the user's WIP work
|
|
1586
|
+
expect(formState.firstName.value).toEqual("ff");
|
|
1587
|
+
// But if we call changedValue, insinuating we're sending the value off to the server
|
|
1588
|
+
expect(formState.changedValue).toEqual({ id: "a:1", firstName: "ff" });
|
|
1589
|
+
// And now the server acks something different
|
|
1590
|
+
formState.set({ firstName: "FF" }, { refreshing: true });
|
|
1591
|
+
// Then we accept the value
|
|
1592
|
+
expect(formState.firstName.value).toEqual("FF");
|
|
1593
|
+
expect(formState.firstName.dirty).toEqual(false);
|
|
1594
|
+
});
|
|
1595
|
+
it("rejects server result if we have further changes", () => {
|
|
1596
|
+
const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, { id: "a:1", firstName: "f" });
|
|
1597
|
+
// Given first name has wip edits
|
|
1598
|
+
formState.firstName.set("ff");
|
|
1599
|
+
// And we call changedValue, insinuating we're sending the value off to the server
|
|
1600
|
+
expect(formState.changedValue).toEqual({ id: "a:1", firstName: "ff" });
|
|
1601
|
+
// And the user changes the name yet again
|
|
1602
|
+
formState.firstName.set("fff");
|
|
1603
|
+
// And now the server acks something different
|
|
1604
|
+
formState.set({ firstName: "FF" }, { refreshing: true });
|
|
1605
|
+
// Then we keep the "post-changedValue" value
|
|
1606
|
+
expect(formState.firstName.value).toEqual("fff");
|
|
1607
|
+
expect(formState.firstName.dirty).toEqual(true);
|
|
1608
|
+
});
|
|
1545
1609
|
it("does update a focused, unchanged field", () => {
|
|
1546
1610
|
const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1547
1611
|
// Given first name is focused, but has not changed
|
|
@@ -175,7 +175,7 @@ describe("useFormState", () => {
|
|
|
175
175
|
form.address.street.value = "local";
|
|
176
176
|
form.books.rows[0].title.value = "local";
|
|
177
177
|
}
|
|
178
|
-
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": "lastName", children: form.lastName.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "street", children: form.address.street.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "city", children: form.address.city.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "title1", children: form.books.rows[0].title.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "title2", children: form.books.rows[1].title.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "booksLength", children: form.books.rows.length }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "makeLocalChanges", onClick: makeLocalChanges }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "refreshData", onClick: () => setData(data2) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "saveData", onClick: () => setData(data3) }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "
|
|
178
|
+
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": "lastName", children: form.lastName.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "street", children: form.address.street.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "city", children: form.address.city.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "title1", children: form.books.rows[0].title.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "title2", children: form.books.rows[1].title.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "booksLength", children: form.books.rows.length }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "makeLocalChanges", onClick: makeLocalChanges }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "refreshData", onClick: () => setData(data2) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "saveData", onClick: () => setData(data3) }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "dirty", children: String(form.dirty) })] })) }));
|
|
179
179
|
}
|
|
180
180
|
// And we start out with the initial query data
|
|
181
181
|
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
@@ -188,12 +188,7 @@ describe("useFormState", () => {
|
|
|
188
188
|
expect(r.firstName.textContent).toEqual("local");
|
|
189
189
|
expect(r.street.textContent).toEqual("local");
|
|
190
190
|
expect(r.title1.textContent).toEqual("local");
|
|
191
|
-
expect(
|
|
192
|
-
id: "a:1",
|
|
193
|
-
address: { id: "address:1", street: "local" },
|
|
194
|
-
books: [{ id: "b:1", title: "local" }, { id: "b:2" }],
|
|
195
|
-
firstName: "local",
|
|
196
|
-
});
|
|
191
|
+
expect(r.dirty.textContent).toEqual("true");
|
|
197
192
|
// And when the new query is ran i.e. due to a cache refresh
|
|
198
193
|
(0, rtl_utils_1.click)(r.refreshData);
|
|
199
194
|
// Then we kept our local changes
|
|
@@ -205,18 +200,41 @@ describe("useFormState", () => {
|
|
|
205
200
|
expect(r.city.textContent).toEqual("c2");
|
|
206
201
|
expect(r.title2.textContent).toEqual("b2");
|
|
207
202
|
expect(r.booksLength.textContent).toEqual("3");
|
|
208
|
-
expect(
|
|
209
|
-
id: "a:1",
|
|
210
|
-
address: { id: "address:1", street: "local" },
|
|
211
|
-
books: [{ id: "b:1", title: "local" }, { id: "b:2" }, { id: "b:3" }],
|
|
212
|
-
firstName: "local",
|
|
213
|
-
});
|
|
203
|
+
expect(r.dirty.textContent).toEqual("true");
|
|
214
204
|
// And then when our mutation results come back
|
|
215
205
|
(0, rtl_utils_1.click)(r.saveData);
|
|
216
|
-
//
|
|
217
|
-
expect(
|
|
218
|
-
|
|
219
|
-
|
|
206
|
+
// And we're not dirty anymore
|
|
207
|
+
expect(r.dirty.textContent).toEqual("false");
|
|
208
|
+
});
|
|
209
|
+
it("accepts server values if client has no further changes", async () => {
|
|
210
|
+
function TestComponent() {
|
|
211
|
+
const config = authorWithAddressAndBooksConfig;
|
|
212
|
+
// The initial resposne
|
|
213
|
+
const data1 = { id: "a:1", firstName: "F1" };
|
|
214
|
+
// The acked response, which uppercases our submission
|
|
215
|
+
const data2 = { id: "a:1", firstName: "F2" };
|
|
216
|
+
// And we start out with data1
|
|
217
|
+
const [data, setData] = (0, react_2.useState)(data1);
|
|
218
|
+
const form = (0, useFormState_1.useFormState)({ config, init: { input: data } });
|
|
219
|
+
function makeLocalChanges() {
|
|
220
|
+
form.firstName.value = "f2";
|
|
221
|
+
}
|
|
222
|
+
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)("button", { "data-testid": "makeLocalChanges", onClick: makeLocalChanges }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "refreshData", onClick: () => {
|
|
223
|
+
noop(form.changedValue); // pretend we put this on the wire
|
|
224
|
+
setData(data2);
|
|
225
|
+
} })] })) }));
|
|
226
|
+
}
|
|
227
|
+
// And we start out with the initial query data
|
|
228
|
+
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
229
|
+
expect(r.firstName.textContent).toEqual("F1");
|
|
230
|
+
// When we make some local changes
|
|
231
|
+
(0, rtl_utils_1.click)(r.makeLocalChanges);
|
|
232
|
+
// Then we see them
|
|
233
|
+
expect(r.firstName.textContent).toEqual("f2");
|
|
234
|
+
// And when the server acks a slightly different result
|
|
235
|
+
(0, rtl_utils_1.click)(r.refreshData);
|
|
236
|
+
// Then we accept it
|
|
237
|
+
expect(r.firstName.textContent).toEqual("F2");
|
|
220
238
|
});
|
|
221
239
|
it("can accept new data while read only", async () => {
|
|
222
240
|
// Given a component
|
|
@@ -694,3 +712,4 @@ function focusAndBlur(state) {
|
|
|
694
712
|
state.focus();
|
|
695
713
|
state.blur();
|
|
696
714
|
}
|
|
715
|
+
function noop(value) { }
|