@homebound/form-state 2.33.0 → 3.1.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/README.md +6 -6
- package/dist/index.d.ts +450 -9
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1118 -14
- package/dist/index.js.map +1 -0
- package/package.json +42 -50
- package/skills/form-state/SKILL.md +9 -9
- package/dist/FormStateApp.d.ts +0 -5
- package/dist/FormStateApp.js +0 -54
- package/dist/FormStateApp.test.d.ts +0 -1
- package/dist/FormStateApp.test.js +0 -26
- package/dist/config.d.ts +0 -78
- package/dist/config.js +0 -2
- package/dist/configBuilders.d.ts +0 -67
- package/dist/configBuilders.js +0 -115
- package/dist/configBuilders.test.d.ts +0 -1
- package/dist/configBuilders.test.js +0 -65
- package/dist/fields/deepEquals.d.ts +0 -7
- package/dist/fields/deepEquals.js +0 -53
- package/dist/fields/fragmentField.d.ts +0 -4
- package/dist/fields/fragmentField.js +0 -58
- package/dist/fields/listField.d.ts +0 -11
- package/dist/fields/listField.js +0 -355
- package/dist/fields/objectField.d.ts +0 -63
- package/dist/fields/objectField.js +0 -277
- package/dist/fields/valueField.d.ts +0 -81
- package/dist/fields/valueField.js +0 -244
- package/dist/formState.stories.d.ts +0 -4
- package/dist/formState.stories.js +0 -12
- package/dist/formState.test.d.ts +0 -11
- package/dist/formState.test.js +0 -2109
- package/dist/formStateDomain.d.ts +0 -47
- package/dist/formStateDomain.js +0 -28
- package/dist/mobx-behavior.test.d.ts +0 -1
- package/dist/mobx-behavior.test.js +0 -51
- package/dist/proxies.d.ts +0 -5
- package/dist/proxies.js +0 -20
- package/dist/rules.d.ts +0 -10
- package/dist/rules.js +0 -11
- package/dist/setupTests.d.ts +0 -1
- package/dist/setupTests.js +0 -15
- package/dist/stackoverlfow.d.ts +0 -17
- package/dist/stackoverlfow.js +0 -17
- package/dist/temporal.d.ts +0 -6
- package/dist/temporal.js +0 -34
- package/dist/temporal.test.d.ts +0 -1
- package/dist/temporal.test.js +0 -97
- package/dist/useFormState.d.ts +0 -82
- package/dist/useFormState.js +0 -158
- package/dist/useFormState.test.d.ts +0 -1
- package/dist/useFormState.test.js +0 -787
- package/dist/useFormStates.d.ts +0 -68
- package/dist/useFormStates.js +0 -101
- package/dist/useFormStates.test.d.ts +0 -1
- package/dist/useFormStates.test.js +0 -223
- package/dist/utils.d.ts +0 -52
- package/dist/utils.js +0 -198
- package/dist/utils.test.d.ts +0 -1
- package/dist/utils.test.js +0 -102
package/dist/formState.test.js
DELETED
|
@@ -1,2109 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ObservableObject = void 0;
|
|
4
|
-
const mobx_1 = require("mobx");
|
|
5
|
-
const configBuilders_1 = require("./configBuilders");
|
|
6
|
-
const objectField_1 = require("./fields/objectField");
|
|
7
|
-
const formStateDomain_1 = require("./formStateDomain");
|
|
8
|
-
const rules_1 = require("./rules");
|
|
9
|
-
describe("formState", () => {
|
|
10
|
-
it("can create a simple object", () => {
|
|
11
|
-
const a = (0, objectField_1.createObjectState)({ title: { type: "value", rules: [rules_1.required] } }, {});
|
|
12
|
-
expect(a.valid).toBeFalsy();
|
|
13
|
-
});
|
|
14
|
-
it("can validate a simple input", () => {
|
|
15
|
-
const a = (0, objectField_1.createObjectState)({ title: { type: "value", rules: [rules_1.required] } }, { title: "b1" });
|
|
16
|
-
let numErrors = 0;
|
|
17
|
-
(0, mobx_1.autorun)(() => {
|
|
18
|
-
numErrors = a.title.errors.length;
|
|
19
|
-
});
|
|
20
|
-
expect(a.valid).toBeTruthy();
|
|
21
|
-
expect(numErrors).toEqual(0);
|
|
22
|
-
a.title.value = null;
|
|
23
|
-
expect(a.title.valid).toBeFalsy();
|
|
24
|
-
expect(numErrors).toEqual(1);
|
|
25
|
-
expect(a.title.errors).toEqual(["Required"]);
|
|
26
|
-
expect(a.errors).toEqual(["title: Required"]);
|
|
27
|
-
});
|
|
28
|
-
it("can tell what is required", () => {
|
|
29
|
-
const a = (0, objectField_1.createObjectState)({
|
|
30
|
-
title: { type: "value", rules: [rules_1.required] },
|
|
31
|
-
isPublished: { type: "value" },
|
|
32
|
-
}, {});
|
|
33
|
-
expect(a.title.required).toBeTruthy();
|
|
34
|
-
expect(a.isPublished.required).toBeFalsy();
|
|
35
|
-
});
|
|
36
|
-
it("trims whitespace within the required rule", () => {
|
|
37
|
-
const a = (0, objectField_1.createObjectState)({ title: { type: "value", rules: [rules_1.required] } }, { title: "initial valid title" });
|
|
38
|
-
let numErrors = 0;
|
|
39
|
-
(0, mobx_1.autorun)(() => {
|
|
40
|
-
numErrors = a.title.errors.length;
|
|
41
|
-
});
|
|
42
|
-
expect(a.valid).toBeTruthy();
|
|
43
|
-
expect(numErrors).toEqual(0);
|
|
44
|
-
// When a value contains only whitespace
|
|
45
|
-
a.title.value = " ";
|
|
46
|
-
expect(a.title.valid).toBeFalsy();
|
|
47
|
-
expect(numErrors).toEqual(1);
|
|
48
|
-
expect(a.title.errors).toEqual(["Required"]);
|
|
49
|
-
expect(a.errors).toEqual(["title: Required"]);
|
|
50
|
-
});
|
|
51
|
-
it("can set values", () => {
|
|
52
|
-
const a = (0, objectField_1.createObjectState)({ title: { type: "value" } }, { title: "b1" });
|
|
53
|
-
expect(a.title.value).toEqual("b1");
|
|
54
|
-
});
|
|
55
|
-
it("can read values", () => {
|
|
56
|
-
const a = (0, objectField_1.createObjectState)({ title: { type: "value" } }, { title: "b1" });
|
|
57
|
-
expect(a.value.title).toEqual("b1");
|
|
58
|
-
expect(a.title.value).toEqual("b1");
|
|
59
|
-
});
|
|
60
|
-
it("can set dates", () => {
|
|
61
|
-
const a = (0, objectField_1.createObjectState)({
|
|
62
|
-
birthday: {
|
|
63
|
-
type: "value",
|
|
64
|
-
rules: [({ value }) => ((value === null || value === void 0 ? void 0 : value.getTime()) === formStateDomain_1.jan2.getTime() ? "cannot be born on jan2" : undefined)],
|
|
65
|
-
},
|
|
66
|
-
}, { birthday: formStateDomain_1.jan1 });
|
|
67
|
-
expect(a.birthday.value).toEqual(formStateDomain_1.jan1);
|
|
68
|
-
a.birthday.set(formStateDomain_1.jan2);
|
|
69
|
-
expect(a.birthday.errors).toEqual(["cannot be born on jan2"]);
|
|
70
|
-
});
|
|
71
|
-
it("can set nested values", () => {
|
|
72
|
-
const a1 = createAuthorInputState({
|
|
73
|
-
firstName: "a1",
|
|
74
|
-
books: [{ title: "b1" }],
|
|
75
|
-
});
|
|
76
|
-
expect(a1.books.rows[0].title.value).toEqual("b1");
|
|
77
|
-
});
|
|
78
|
-
it("can set nested values 2", () => {
|
|
79
|
-
const a = createAuthorWithAddressInputState({ address: { city: "b1" } });
|
|
80
|
-
expect(a.address.city.value).toEqual("b1");
|
|
81
|
-
});
|
|
82
|
-
it("can adapt values", () => {
|
|
83
|
-
// Given an author where `delete` is normally a boolean
|
|
84
|
-
const a = (0, objectField_1.createObjectState)({
|
|
85
|
-
delete: { type: "value" },
|
|
86
|
-
}, { delete: true });
|
|
87
|
-
const boolField = a.delete;
|
|
88
|
-
// But we adapt it to a string
|
|
89
|
-
const stringField = a.delete.adapt({
|
|
90
|
-
toValue: (b) => String(b),
|
|
91
|
-
fromValue: (s) => Boolean(s),
|
|
92
|
-
});
|
|
93
|
-
// Then we can read it as a string
|
|
94
|
-
expect(stringField.value).toEqual("true");
|
|
95
|
-
// And we can set it as a string
|
|
96
|
-
stringField.value = "";
|
|
97
|
-
expect(boolField.value).toBe(false);
|
|
98
|
-
// And the originalValue is maintained
|
|
99
|
-
expect(boolField.originalValue).toBe(true);
|
|
100
|
-
expect(stringField.originalValue).toBe("true");
|
|
101
|
-
// As well as the dirty.
|
|
102
|
-
expect(boolField.dirty).toBe(true);
|
|
103
|
-
expect(stringField.dirty).toBe(true);
|
|
104
|
-
// And reverting works
|
|
105
|
-
stringField.revertChanges();
|
|
106
|
-
expect(boolField.dirty).toBe(false);
|
|
107
|
-
expect(stringField.dirty).toBe(false);
|
|
108
|
-
expect(boolField.value).toBe(true);
|
|
109
|
-
expect(stringField.value).toBe("true");
|
|
110
|
-
});
|
|
111
|
-
it("maintains object identity", () => {
|
|
112
|
-
const a1 = { firstName: "a1" };
|
|
113
|
-
const state = createAuthorInputState(a1);
|
|
114
|
-
state.firstName.set("a2");
|
|
115
|
-
expect(state.value === a1).toEqual(true);
|
|
116
|
-
expect(state.originalValue !== a1).toEqual(true);
|
|
117
|
-
expect(a1.firstName).toEqual("a2");
|
|
118
|
-
});
|
|
119
|
-
it("maintains object identity of lists", () => {
|
|
120
|
-
var _a;
|
|
121
|
-
const b1 = { title: "t1" };
|
|
122
|
-
const a1 = { firstName: "a1", books: [b1] };
|
|
123
|
-
const state = createAuthorInputState(a1);
|
|
124
|
-
const b2 = { title: "t2" };
|
|
125
|
-
state.books.add(b2);
|
|
126
|
-
expect(state.value.books === a1.books).toEqual(true);
|
|
127
|
-
expect(state.books.value.length).toEqual(2);
|
|
128
|
-
expect((_a = a1.books) === null || _a === void 0 ? void 0 : _a.length).toEqual(2);
|
|
129
|
-
expect(state.books.rows[0].value === b1).toEqual(true);
|
|
130
|
-
expect(state.books.rows[1].value === b2).toEqual(true);
|
|
131
|
-
expect(a1.books[1] === b2).toEqual(true);
|
|
132
|
-
});
|
|
133
|
-
it("can add items anywhere within a list", () => {
|
|
134
|
-
const b1 = { title: "t1" };
|
|
135
|
-
const a1 = { firstName: "a1", books: [b1] };
|
|
136
|
-
const state = createAuthorInputState(a1);
|
|
137
|
-
let numBooks;
|
|
138
|
-
let ticks = 0;
|
|
139
|
-
(0, mobx_1.autorun)(() => {
|
|
140
|
-
numBooks = state.books.rows.length;
|
|
141
|
-
ticks++;
|
|
142
|
-
});
|
|
143
|
-
const b2 = { title: "t2" };
|
|
144
|
-
state.books.add(b2, 0);
|
|
145
|
-
expect(state.books.rows[0].value === b2).toEqual(true);
|
|
146
|
-
expect(state.books.rows[1].value === b1).toEqual(true);
|
|
147
|
-
expect(ticks).toEqual(2);
|
|
148
|
-
const b3 = { title: "t3" };
|
|
149
|
-
state.books.add(b3, 1);
|
|
150
|
-
expect(state.books.rows[0].value === b2).toEqual(true);
|
|
151
|
-
expect(state.books.rows[1].value === b3).toEqual(true);
|
|
152
|
-
expect(state.books.rows[2].value === b1).toEqual(true);
|
|
153
|
-
expect(ticks).toEqual(3);
|
|
154
|
-
});
|
|
155
|
-
it("list value can refresh on undefined", () => {
|
|
156
|
-
// Given a list field that is currently []
|
|
157
|
-
const a1 = { firstName: "a1", books: [] };
|
|
158
|
-
const state = createAuthorInputState(a1);
|
|
159
|
-
// When we refresh it with undefined
|
|
160
|
-
state.set({ books: undefined }, { refreshing: true });
|
|
161
|
-
// Then it doesn't blow up, and stays []
|
|
162
|
-
expect(state.books.value).toEqual([]);
|
|
163
|
-
expect(state.value).toEqual({ firstName: "a1", books: [] });
|
|
164
|
-
// And it's not considered changed
|
|
165
|
-
expect(state.books.dirty).toBe(false);
|
|
166
|
-
expect(state.books.originalValue).toEqual([]);
|
|
167
|
-
});
|
|
168
|
-
it("list value can refresh and not drop new children w/id", () => {
|
|
169
|
-
// Given a list field that is currently []
|
|
170
|
-
const a1 = { firstName: "a1", books: [] };
|
|
171
|
-
const state = createAuthorInputState(a1);
|
|
172
|
-
// And we add a new child on the client-side
|
|
173
|
-
state.books.add({ id: "b:1", title: "b1" });
|
|
174
|
-
// When we refresh it with the original list undefined
|
|
175
|
-
state.set({ books: [] }, { refreshing: true });
|
|
176
|
-
// Then it keeps the new child (and it somehow already has the id set)
|
|
177
|
-
expect(state.books.value).toEqual([{ id: "b:1", title: "b1" }]);
|
|
178
|
-
// And it's still considered changed
|
|
179
|
-
expect(state.books.dirty).toBe(true);
|
|
180
|
-
expect(state.books.originalValue).toEqual([]);
|
|
181
|
-
// And later when we refresh with the value acked from the server
|
|
182
|
-
state.set({ books: [{ id: "b:1", title: "b1", isPublished: true }] }, { refreshing: true });
|
|
183
|
-
// Then we have only 1 value
|
|
184
|
-
expect(state.books.value).toEqual([{ id: "b:1", title: "b1", isPublished: true }]);
|
|
185
|
-
// And we're not longer dirty
|
|
186
|
-
expect(state.books.dirty).toBe(false);
|
|
187
|
-
});
|
|
188
|
-
it("list value can refresh and not drop new children w/o an id", () => {
|
|
189
|
-
// Given a list field that is currently []
|
|
190
|
-
const a1 = { firstName: "a1", books: [] };
|
|
191
|
-
const state = createAuthorInputState(a1);
|
|
192
|
-
// And we add a new child on the client-side (and it doesn't have an id)
|
|
193
|
-
state.books.add({ title: "b1" });
|
|
194
|
-
// When we refresh it with the original list undefined
|
|
195
|
-
state.set({ books: [] }, { refreshing: true });
|
|
196
|
-
// Then it keeps the new child
|
|
197
|
-
expect(state.books.value).toEqual([{ title: "b1" }]);
|
|
198
|
-
// And it's still considered changed
|
|
199
|
-
expect(state.books.dirty).toBe(true);
|
|
200
|
-
expect(state.books.originalValue).toEqual([]);
|
|
201
|
-
// And later when we refresh with the value acked from the server
|
|
202
|
-
state.set({ books: [{ id: "b:1", title: "b1" }] }, { refreshing: true });
|
|
203
|
-
// Then we have only 1 value
|
|
204
|
-
expect(state.books.value).toEqual([{ id: "b:1", title: "b1" }]);
|
|
205
|
-
// And we're not longer dirty
|
|
206
|
-
expect(state.books.dirty).toBe(false);
|
|
207
|
-
});
|
|
208
|
-
it("list value can refresh and not drop new children that are just data", () => {
|
|
209
|
-
const config = configBuilders_1.f.config({
|
|
210
|
-
firstName: configBuilders_1.f.value(),
|
|
211
|
-
tags: configBuilders_1.f.list({ combo: configBuilders_1.f.value() }),
|
|
212
|
-
});
|
|
213
|
-
const state = (0, objectField_1.createObjectState)(config, { firstName: "a1", tags: [] });
|
|
214
|
-
// And we add a new child on the client-side (and it doesn't have an id)
|
|
215
|
-
state.tags.add({ combo: ["b1", "b2"] });
|
|
216
|
-
// When we refresh it with the original list undefined
|
|
217
|
-
state.set({ tags: [] }, { refreshing: true });
|
|
218
|
-
// Then it keeps the new child
|
|
219
|
-
expect(state.tags.value).toEqual([{ combo: ["b1", "b2"] }]);
|
|
220
|
-
// And it's still considered changed
|
|
221
|
-
expect(state.tags.dirty).toBe(true);
|
|
222
|
-
expect(state.tags.originalValue).toEqual([]);
|
|
223
|
-
// And we add an additional child
|
|
224
|
-
state.tags.add({ combo: ["c1", "c2"] });
|
|
225
|
-
// And a refresh brings back the 1st child acked from the server
|
|
226
|
-
state.set({ tags: [{ combo: ["b1", "b2"] }] }, { refreshing: true });
|
|
227
|
-
// Then we still have 2 children
|
|
228
|
-
expect(state.tags.value).toEqual([{ combo: ["b1", "b2"] }, { combo: ["c1", "c2"] }]);
|
|
229
|
-
// And we're still dirty
|
|
230
|
-
expect(state.tags.dirty).toBe(true);
|
|
231
|
-
// When the server finally acks both children
|
|
232
|
-
state.set({ tags: [{ combo: ["b1", "b2"] }, { combo: ["c1", "c2"] }] }, { refreshing: true });
|
|
233
|
-
// Then we're finally clean
|
|
234
|
-
expect(state.tags.dirty).toBe(false);
|
|
235
|
-
});
|
|
236
|
-
it("list value can refresh and drop children that were deleted", () => {
|
|
237
|
-
// Given a list field that is currently populated
|
|
238
|
-
const a1 = {
|
|
239
|
-
id: "a:1",
|
|
240
|
-
firstName: "a1",
|
|
241
|
-
books: [
|
|
242
|
-
{ id: "b:1", title: "b1", op: "include" },
|
|
243
|
-
{ id: "b:2", title: "b2", op: "include" },
|
|
244
|
-
],
|
|
245
|
-
};
|
|
246
|
-
// And incremental deletion is enabled
|
|
247
|
-
const config = configBuilders_1.f.config({
|
|
248
|
-
id: configBuilders_1.f.value(),
|
|
249
|
-
books: configBuilders_1.f.list({ id: configBuilders_1.f.value(), title: configBuilders_1.f.value(), op: configBuilders_1.f.value() }),
|
|
250
|
-
});
|
|
251
|
-
const state = (0, objectField_1.createObjectState)(config, a1);
|
|
252
|
-
// When we delete the 1st book
|
|
253
|
-
state.books.rows[0].op.set("delete");
|
|
254
|
-
// When we refresh it only the 2nd book
|
|
255
|
-
state.set({ books: [{ id: "b:2", title: "b2" }] }, { refreshing: true });
|
|
256
|
-
// Then we only have 1 book
|
|
257
|
-
expect(state.books.value).toEqual([{ id: "b:2", title: "b2", op: "include" }]);
|
|
258
|
-
// And we're not dirty
|
|
259
|
-
expect(state.books.dirty).toBe(false);
|
|
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
|
-
});
|
|
282
|
-
it("list value can refresh and merge grand children that were added", () => {
|
|
283
|
-
// Given a list field that is currently populated with 1 child w/1 grandchild
|
|
284
|
-
const a1 = {
|
|
285
|
-
id: "a:1",
|
|
286
|
-
books: [{ id: "b:1", title: "b1", reviews: [{ id: "r:1", rating: 5 }] }],
|
|
287
|
-
};
|
|
288
|
-
const config = configBuilders_1.f.config({
|
|
289
|
-
id: configBuilders_1.f.value(),
|
|
290
|
-
books: configBuilders_1.f.list({
|
|
291
|
-
id: configBuilders_1.f.value(),
|
|
292
|
-
title: configBuilders_1.f.value().req(),
|
|
293
|
-
reviews: configBuilders_1.f.list({ id: configBuilders_1.f.value(), rating: configBuilders_1.f.value().req() }),
|
|
294
|
-
}),
|
|
295
|
-
});
|
|
296
|
-
const state = (0, objectField_1.createObjectState)(config, a1);
|
|
297
|
-
// When we add a new book with two ratings
|
|
298
|
-
state.books.add({ title: "b2", reviews: [{ rating: 4 }, { rating: 3 }] });
|
|
299
|
-
// When we refresh but only the 1st review grandchild existed when we auto-saved
|
|
300
|
-
state.set({
|
|
301
|
-
books: [
|
|
302
|
-
{ id: "b:1", title: "b1" },
|
|
303
|
-
{
|
|
304
|
-
id: "b:2",
|
|
305
|
-
title: "b2",
|
|
306
|
-
reviews: [{ id: "r:2", rating: 4 }],
|
|
307
|
-
},
|
|
308
|
-
],
|
|
309
|
-
}, { refreshing: true });
|
|
310
|
-
// Then we still have two books
|
|
311
|
-
expect(state.books.value).toEqual([
|
|
312
|
-
{
|
|
313
|
-
id: "b:1",
|
|
314
|
-
title: "b1",
|
|
315
|
-
reviews: [{ id: "r:1", rating: 5 }],
|
|
316
|
-
},
|
|
317
|
-
{
|
|
318
|
-
id: "b:2",
|
|
319
|
-
title: "b2",
|
|
320
|
-
// And we stitched together the ack-d 1st review (now with an id assigned) and the 2nd WIP review
|
|
321
|
-
reviews: [{ id: "r:2", rating: 4 }, { rating: 3 }],
|
|
322
|
-
},
|
|
323
|
-
]);
|
|
324
|
-
// And we're still dirty
|
|
325
|
-
expect(state.books.dirty).toBe(true);
|
|
326
|
-
expect(state.books.rows[0].dirty).toBe(false);
|
|
327
|
-
expect(state.books.rows[1].dirty).toBe(true);
|
|
328
|
-
expect(state.books.rows[1].reviews.rows[0].dirty).toBe(false);
|
|
329
|
-
expect(state.books.rows[1].reviews.rows[1].dirty).toBe(false);
|
|
330
|
-
expect(state.books.rows[1].reviews.dirty).toBe(true);
|
|
331
|
-
// Until the 2nd auto-save completes
|
|
332
|
-
state.set({
|
|
333
|
-
books: [
|
|
334
|
-
{ id: "b:1", title: "b1" },
|
|
335
|
-
{
|
|
336
|
-
id: "b:2",
|
|
337
|
-
title: "b2",
|
|
338
|
-
reviews: [
|
|
339
|
-
{ id: "r:2", rating: 4 },
|
|
340
|
-
{ id: "r:3", rating: 3 },
|
|
341
|
-
],
|
|
342
|
-
},
|
|
343
|
-
],
|
|
344
|
-
}, { refreshing: true });
|
|
345
|
-
// Then we stitch together the 2nd review as well
|
|
346
|
-
expect(state.books.value).toMatchObject([
|
|
347
|
-
{ id: "b:1" },
|
|
348
|
-
{ id: "b:2", reviews: [{ id: "r:2" }, { id: "r:3", rating: 3 }] },
|
|
349
|
-
]);
|
|
350
|
-
// And we're no longer dirty
|
|
351
|
-
expect(state.books.dirty).toBe(false);
|
|
352
|
-
});
|
|
353
|
-
it("list value can observe changes", () => {
|
|
354
|
-
const b1 = { title: "t1" };
|
|
355
|
-
const a1 = { firstName: "a1", books: [b1] };
|
|
356
|
-
const state = createAuthorInputState(a1);
|
|
357
|
-
let books;
|
|
358
|
-
let ticks = 0;
|
|
359
|
-
(0, mobx_1.autorun)(() => {
|
|
360
|
-
books = state.books.value;
|
|
361
|
-
ticks++;
|
|
362
|
-
});
|
|
363
|
-
state.books.add({ title: "t2" }, 0);
|
|
364
|
-
expect(ticks).toEqual(2);
|
|
365
|
-
});
|
|
366
|
-
it("list originalValue can observe changes", () => {
|
|
367
|
-
const b1 = { title: "t1" };
|
|
368
|
-
const a1 = { firstName: "a1", books: [b1] };
|
|
369
|
-
const state = createAuthorInputState(a1);
|
|
370
|
-
let books;
|
|
371
|
-
let ticks = 0;
|
|
372
|
-
(0, mobx_1.autorun)(() => {
|
|
373
|
-
books = state.books.originalValue;
|
|
374
|
-
ticks++;
|
|
375
|
-
});
|
|
376
|
-
state.books.add({ title: "t2" }, 0);
|
|
377
|
-
expect(ticks).toEqual(1);
|
|
378
|
-
state.commitChanges();
|
|
379
|
-
expect(ticks).toEqual(2);
|
|
380
|
-
});
|
|
381
|
-
it("state originalValue can observe changes", () => {
|
|
382
|
-
const a1 = { firstName: "a1", books: [] };
|
|
383
|
-
const state = createAuthorInputState(a1);
|
|
384
|
-
let ticks = 0;
|
|
385
|
-
(0, mobx_1.autorun)(() => {
|
|
386
|
-
noop(state.originalValue);
|
|
387
|
-
ticks++;
|
|
388
|
-
});
|
|
389
|
-
state.books.add({ title: "t2" }, 0);
|
|
390
|
-
expect(ticks).toEqual(1);
|
|
391
|
-
state.commitChanges();
|
|
392
|
-
expect(ticks).toEqual(2);
|
|
393
|
-
});
|
|
394
|
-
it("maintains unknown fields", () => {
|
|
395
|
-
// Given the form is not directly editing id fields
|
|
396
|
-
const config = {
|
|
397
|
-
firstName: { type: "value" },
|
|
398
|
-
books: { type: "list", config: { title: { type: "value" } } },
|
|
399
|
-
};
|
|
400
|
-
// And we initially have ids in the input
|
|
401
|
-
const a1 = { id: "1", firstName: "a1", books: [{ id: "2", title: "t1" }] };
|
|
402
|
-
const state = (0, objectField_1.createObjectState)(config, a1);
|
|
403
|
-
// And we edit a few things
|
|
404
|
-
state.firstName.set("a2");
|
|
405
|
-
state.books.add({ title: "t2" });
|
|
406
|
-
// When we get back the originalValue
|
|
407
|
-
// Then it has the ids and the new values
|
|
408
|
-
expect(state.value).toMatchObject({
|
|
409
|
-
id: "1",
|
|
410
|
-
books: [{ id: "2", title: "t1" }, { title: "t2" }],
|
|
411
|
-
});
|
|
412
|
-
});
|
|
413
|
-
it("list field valid is based on nested fields", () => {
|
|
414
|
-
// Given an author that is initially valid
|
|
415
|
-
const a1 = createAuthorInputState({ firstName: "a1", books: [] });
|
|
416
|
-
expect(a1.valid).toBeTruthy();
|
|
417
|
-
let lastValid = undefined;
|
|
418
|
-
let ticks = 0;
|
|
419
|
-
(0, mobx_1.autorun)(() => {
|
|
420
|
-
lastValid = a1.valid;
|
|
421
|
-
ticks++;
|
|
422
|
-
});
|
|
423
|
-
// When an empty book is added
|
|
424
|
-
a1.set({ firstName: "a1", books: [{}] });
|
|
425
|
-
// Then it's title is invalid
|
|
426
|
-
expect(a1.books.rows.length).toEqual(1);
|
|
427
|
-
expect(a1.books.rows[0].title.valid).toBeFalsy();
|
|
428
|
-
expect(a1.books.rows[0].title.errors).toEqual(["Required"]);
|
|
429
|
-
// And the books collection itself is invalid
|
|
430
|
-
expect(a1.books.valid).toBeFalsy();
|
|
431
|
-
// But we don't currently show the error here b/c it's not an error on the collection itself;
|
|
432
|
-
// we could have like an `a1.books.allErrors` property instead.
|
|
433
|
-
expect(a1.books.errors).toEqual([]);
|
|
434
|
-
// And the author itself is also invalid
|
|
435
|
-
expect(a1.valid).toBeFalsy();
|
|
436
|
-
expect(lastValid).toBeFalsy();
|
|
437
|
-
expect(ticks).toEqual(2);
|
|
438
|
-
// And when it becomes valid
|
|
439
|
-
a1.books.rows[0].title.value = "b1";
|
|
440
|
-
// Then everything is reactively valid
|
|
441
|
-
expect(lastValid).toBeTruthy();
|
|
442
|
-
expect(ticks).toEqual(3);
|
|
443
|
-
});
|
|
444
|
-
it("can add nested values", () => {
|
|
445
|
-
// Given we already have a book
|
|
446
|
-
const a1 = createAuthorInputState({
|
|
447
|
-
firstName: "a1",
|
|
448
|
-
books: [{ title: "b1" }],
|
|
449
|
-
});
|
|
450
|
-
expect(a1.books.rows[0].title.value).toEqual("b1");
|
|
451
|
-
// When another book is added
|
|
452
|
-
a1.books.add({ title: "b2" });
|
|
453
|
-
expect(a1.books.touched).toEqual(true);
|
|
454
|
-
// Then both books are visible
|
|
455
|
-
expect(a1.books.rows[0].title.value).toEqual("b1");
|
|
456
|
-
expect(a1.books.rows[1].title.value).toEqual("b2");
|
|
457
|
-
});
|
|
458
|
-
it("can access nested values", () => {
|
|
459
|
-
// Given we have two books
|
|
460
|
-
const a1 = createAuthorInputState({
|
|
461
|
-
firstName: "a1",
|
|
462
|
-
books: [{ title: "b1" }, { title: "b2" }],
|
|
463
|
-
});
|
|
464
|
-
// We can see what each book looks like
|
|
465
|
-
expect(a1.books.value[0].title).toEqual("b1");
|
|
466
|
-
expect(a1.books.value[1].title).toEqual("b2");
|
|
467
|
-
});
|
|
468
|
-
it("can remove nested values", () => {
|
|
469
|
-
// Given we have two books
|
|
470
|
-
const a1 = createAuthorInputState({
|
|
471
|
-
firstName: "a1",
|
|
472
|
-
books: [{ title: "b1" }, { title: "b2" }],
|
|
473
|
-
});
|
|
474
|
-
expect(a1.books.rows[0].title.value).toEqual("b1");
|
|
475
|
-
// When we remove the 1st book
|
|
476
|
-
a1.books.remove(0);
|
|
477
|
-
// Then only the 2nd book is left
|
|
478
|
-
expect(a1.books.rows.length).toEqual(1);
|
|
479
|
-
expect(a1.books.rows[0].title.value).toEqual("b2");
|
|
480
|
-
});
|
|
481
|
-
it("can remove non-first nested values", () => {
|
|
482
|
-
// Given we have two books
|
|
483
|
-
const a1 = createAuthorInputState({
|
|
484
|
-
firstName: "a1",
|
|
485
|
-
books: [{ title: "b1" }, { title: "b2" }],
|
|
486
|
-
});
|
|
487
|
-
expect(a1.books.rows[0].title.value).toEqual("b1");
|
|
488
|
-
// When we remove the 2nd book
|
|
489
|
-
a1.books.remove(1);
|
|
490
|
-
// Then only the 1st book is left
|
|
491
|
-
expect(a1.books.rows.length).toEqual(1);
|
|
492
|
-
expect(a1.books.rows[0].title.value).toEqual("b1");
|
|
493
|
-
});
|
|
494
|
-
it("can remove added nested values", () => {
|
|
495
|
-
// Given we have a a single book
|
|
496
|
-
const a1 = createAuthorInputState({ books: [{ title: "b1" }] });
|
|
497
|
-
// And we push a new one
|
|
498
|
-
a1.books.add({ title: "b2" });
|
|
499
|
-
// When we remove the 2nd book by the row's reference value
|
|
500
|
-
a1.books.remove(a1.books.rows[1].value);
|
|
501
|
-
// Then only the 1st book is left
|
|
502
|
-
expect(a1.books.rows.length).toEqual(1);
|
|
503
|
-
});
|
|
504
|
-
it("can remove non-first nested values by identity", () => {
|
|
505
|
-
// Given we have two books
|
|
506
|
-
const a1 = createAuthorInputState({
|
|
507
|
-
firstName: "a1",
|
|
508
|
-
books: [{ title: "b1" }, { title: "b2" }],
|
|
509
|
-
});
|
|
510
|
-
expect(a1.books.rows[0].title.value).toEqual("b1");
|
|
511
|
-
// When we remove the 2nd book
|
|
512
|
-
a1.books.remove(a1.books.value[1]);
|
|
513
|
-
// Then only the 1st book is left
|
|
514
|
-
expect(a1.books.rows.length).toEqual(1);
|
|
515
|
-
expect(a1.books.rows[0].title.value).toEqual("b1");
|
|
516
|
-
});
|
|
517
|
-
it("can validate the nested collection directly", () => {
|
|
518
|
-
// Given we already have a book
|
|
519
|
-
const a1 = createAuthorInputState({ firstName: "a1", books: [] });
|
|
520
|
-
let ticks = 0;
|
|
521
|
-
let numErrors = 0;
|
|
522
|
-
(0, mobx_1.autorun)(() => {
|
|
523
|
-
numErrors = a1.books.errors.length;
|
|
524
|
-
ticks++;
|
|
525
|
-
});
|
|
526
|
-
expect(ticks).toEqual(1);
|
|
527
|
-
expect(numErrors).toEqual(0);
|
|
528
|
-
a1.books.rules.push(({ value: b }) => (b.length === 0 ? "Empty" : undefined));
|
|
529
|
-
expect(a1.books.valid).toBeFalsy();
|
|
530
|
-
expect(a1.books.errors).toEqual(["Empty"]);
|
|
531
|
-
expect(ticks).toEqual(2);
|
|
532
|
-
expect(numErrors).toEqual(1);
|
|
533
|
-
a1.books.add({});
|
|
534
|
-
expect(ticks).toEqual(3);
|
|
535
|
-
expect(numErrors).toEqual(0);
|
|
536
|
-
});
|
|
537
|
-
it("can validate across fields", () => {
|
|
538
|
-
const a = (0, objectField_1.createObjectState)({
|
|
539
|
-
firstName: { type: "value", rules: [] },
|
|
540
|
-
lastName: { type: "value" },
|
|
541
|
-
}, {});
|
|
542
|
-
a.lastName.rules.push(() => {
|
|
543
|
-
return a.firstName.value === a.lastName.value ? "Last name cannot be first name" : undefined;
|
|
544
|
-
});
|
|
545
|
-
a.firstName.value = "b1";
|
|
546
|
-
expect(a.firstName.valid).toBeTruthy();
|
|
547
|
-
expect(a.lastName.valid).toBeTruthy();
|
|
548
|
-
a.lastName.value = "b1";
|
|
549
|
-
expect(a.firstName.valid).toBeTruthy();
|
|
550
|
-
expect(a.lastName.errors).toEqual(["Last name cannot be first name"]);
|
|
551
|
-
});
|
|
552
|
-
it("passes sanitized values to validation rules", () => {
|
|
553
|
-
// Given a field with a validation rule
|
|
554
|
-
const rule = jest.fn().mockReturnValue("Required");
|
|
555
|
-
const a = (0, objectField_1.createObjectState)({ firstName: { type: "value", rules: [rule] } }, {});
|
|
556
|
-
expect(rule).toHaveBeenCalledTimes(0);
|
|
557
|
-
// When the field is set to empty string
|
|
558
|
-
a.firstName.value = "";
|
|
559
|
-
expect(a.firstName.errors).toEqual(["Required"]);
|
|
560
|
-
expect(rule).toHaveBeenCalledTimes(1);
|
|
561
|
-
// Then the validation rule was passed undefined
|
|
562
|
-
expect(rule).toHaveBeenCalledWith({
|
|
563
|
-
value: undefined,
|
|
564
|
-
originalValue: undefined,
|
|
565
|
-
key: "firstName",
|
|
566
|
-
object: expect.anything(),
|
|
567
|
-
});
|
|
568
|
-
});
|
|
569
|
-
it("passes trimmed values to validation rules on maybeAutoSave", () => {
|
|
570
|
-
// Given we have a required rule
|
|
571
|
-
const rule = jest.fn().mockReturnValue("Required");
|
|
572
|
-
const a = (0, objectField_1.createObjectState)({ firstName: { type: "value", rules: [rule] } }, {});
|
|
573
|
-
expect(rule).toHaveBeenCalledTimes(0);
|
|
574
|
-
// When the user initially sets an empty string value
|
|
575
|
-
a.firstName.focus();
|
|
576
|
-
a.firstName.value = " ";
|
|
577
|
-
expect(a.firstName.errors).toEqual(["Required"]);
|
|
578
|
-
// Then initially the rule is called with the empty string
|
|
579
|
-
expect(rule).toHaveBeenCalledTimes(1);
|
|
580
|
-
expect(rule).toHaveBeenCalledWith({
|
|
581
|
-
value: " ",
|
|
582
|
-
originalValue: undefined,
|
|
583
|
-
key: "firstName",
|
|
584
|
-
object: expect.anything(),
|
|
585
|
-
});
|
|
586
|
-
// But then when the field maybeAutoSaves (i.e. via blur)
|
|
587
|
-
a.firstName.maybeAutoSave();
|
|
588
|
-
expect(a.firstName.errors).toEqual(["Required"]);
|
|
589
|
-
// Then the rule is called with the undefined
|
|
590
|
-
expect(rule).toHaveBeenCalledTimes(2);
|
|
591
|
-
expect(rule).toHaveBeenCalledWith({
|
|
592
|
-
value: undefined,
|
|
593
|
-
originalValue: undefined,
|
|
594
|
-
key: "firstName",
|
|
595
|
-
object: expect.anything(),
|
|
596
|
-
});
|
|
597
|
-
});
|
|
598
|
-
it("simple value changes trigger observers", () => {
|
|
599
|
-
const a = (0, objectField_1.createObjectState)({
|
|
600
|
-
title: { type: "value", rules: [rules_1.required] },
|
|
601
|
-
}, { title: "t2" });
|
|
602
|
-
let lastTitle = undefined;
|
|
603
|
-
let ticks = 0;
|
|
604
|
-
(0, mobx_1.autorun)(() => {
|
|
605
|
-
lastTitle = a.title.value;
|
|
606
|
-
ticks++;
|
|
607
|
-
});
|
|
608
|
-
expect(ticks).toEqual(1);
|
|
609
|
-
expect(lastTitle).toEqual("t2");
|
|
610
|
-
});
|
|
611
|
-
it("knows value fields are dirty", () => {
|
|
612
|
-
const a1 = createAuthorInputState({ firstName: "a1" });
|
|
613
|
-
expect(a1.firstName.dirty).toBeFalsy();
|
|
614
|
-
a1.firstName.set("a2");
|
|
615
|
-
expect(a1.firstName.dirty).toBeTruthy();
|
|
616
|
-
a1.firstName.set("a1");
|
|
617
|
-
expect(a1.firstName.dirty).toBeFalsy();
|
|
618
|
-
});
|
|
619
|
-
it("knows value fields are dirty even if rendered before the initial set", () => {
|
|
620
|
-
const a1 = createAuthorInputState({ firstName: "a1" });
|
|
621
|
-
expect(a1.firstName.dirty).toBeFalsy();
|
|
622
|
-
a1.firstName.set("a2");
|
|
623
|
-
expect(a1.firstName.dirty).toBeTruthy();
|
|
624
|
-
a1.firstName.set("a1");
|
|
625
|
-
expect(a1.firstName.dirty).toBeFalsy();
|
|
626
|
-
});
|
|
627
|
-
it("knows nested value fields are dirty", () => {
|
|
628
|
-
const a1 = createAuthorInputState({ books: [{ title: "t1" }] });
|
|
629
|
-
expect(a1.books.rows[0].title.dirty).toBeFalsy();
|
|
630
|
-
a1.books.rows[0].title.set("t2");
|
|
631
|
-
expect(a1.books.rows[0].title.dirty).toBeTruthy();
|
|
632
|
-
a1.books.rows[0].title.set("t1");
|
|
633
|
-
expect(a1.books.rows[0].title.dirty).toBeFalsy();
|
|
634
|
-
});
|
|
635
|
-
it("knows list fields are dirty", () => {
|
|
636
|
-
const a1 = createAuthorInputState({ books: [] });
|
|
637
|
-
expect(a1.books.dirty).toBeFalsy();
|
|
638
|
-
a1.books.add({ title: "t2" });
|
|
639
|
-
expect(a1.books.dirty).toBeTruthy();
|
|
640
|
-
a1.books.remove(0);
|
|
641
|
-
expect(a1.dirty).toBeFalsy();
|
|
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
|
-
});
|
|
670
|
-
it("handles list fields with duplicate ids", () => {
|
|
671
|
-
// Given an author that incorrectly has duplicate books
|
|
672
|
-
const a1 = createAuthorInputState({
|
|
673
|
-
books: [
|
|
674
|
-
{ id: "b:1", title: "b1" },
|
|
675
|
-
{ id: "b:1", title: "b2" },
|
|
676
|
-
],
|
|
677
|
-
});
|
|
678
|
-
// Then it didn't blow up
|
|
679
|
-
expect(a1.books.dirty).toBeFalsy();
|
|
680
|
-
// And the 1st book won
|
|
681
|
-
expect(a1.books.rows[0].title.value).toBe("b1");
|
|
682
|
-
expect(a1.books.rows[1].title.value).toBe("b1");
|
|
683
|
-
});
|
|
684
|
-
it("calls maybeAutoSave when adding or removing to a list", () => {
|
|
685
|
-
const maybeAutoSave = jest.fn();
|
|
686
|
-
const a1 = createAuthorInputState({ books: [] }, maybeAutoSave);
|
|
687
|
-
expect(a1.books.dirty).toBeFalsy();
|
|
688
|
-
a1.books.add({ title: "t2" });
|
|
689
|
-
expect(a1.books.dirty).toBeTruthy();
|
|
690
|
-
a1.books.remove(0);
|
|
691
|
-
expect(a1.dirty).toBeFalsy();
|
|
692
|
-
expect(maybeAutoSave).toBeCalledTimes(2);
|
|
693
|
-
});
|
|
694
|
-
it("calls maybeAutoSave when programmatically setting a value", () => {
|
|
695
|
-
const maybeAutoSave = jest.fn();
|
|
696
|
-
// Given an author listening for blur
|
|
697
|
-
const a1 = createAuthorInputState({ books: [{}] }, maybeAutoSave);
|
|
698
|
-
// When we programmatically set a field that isn't focused
|
|
699
|
-
a1.firstName.value = "first";
|
|
700
|
-
// Then we call maybeAutoSave
|
|
701
|
-
expect(maybeAutoSave).toBeCalledTimes(1);
|
|
702
|
-
// And when we set a nested value
|
|
703
|
-
a1.books.rows[0].title.value = "title";
|
|
704
|
-
// Then we called maybeAutoSave again
|
|
705
|
-
expect(maybeAutoSave).toBeCalledTimes(2);
|
|
706
|
-
});
|
|
707
|
-
it("can skip maybeAutoSave when programmatically setting a value", () => {
|
|
708
|
-
const maybeAutoSave = jest.fn();
|
|
709
|
-
// Given an author listening for blur
|
|
710
|
-
const a1 = createAuthorInputState({ books: [{}] }, maybeAutoSave);
|
|
711
|
-
// When we programmatically set a field that isn't focused
|
|
712
|
-
a1.set({ firstName: "first" }, { autoSave: false });
|
|
713
|
-
// Then we don't call maybeAutoSave
|
|
714
|
-
expect(maybeAutoSave).toBeCalledTimes(0);
|
|
715
|
-
});
|
|
716
|
-
it("defers calling maybeAutoSave when setting a focused value", () => {
|
|
717
|
-
const maybeAutoSave = jest.fn();
|
|
718
|
-
// Given an author listening for blur
|
|
719
|
-
const a1 = createAuthorInputState({ books: [{}] }, maybeAutoSave);
|
|
720
|
-
// And the field is focused
|
|
721
|
-
a1.firstName.focus();
|
|
722
|
-
// When we we set the field
|
|
723
|
-
a1.firstName.value = "first";
|
|
724
|
-
// Then we don't call maybeAutoSave
|
|
725
|
-
expect(maybeAutoSave).toBeCalledTimes(0);
|
|
726
|
-
});
|
|
727
|
-
it("skips maybeAutoSave when refreshing", () => {
|
|
728
|
-
const maybeAutoSave = jest.fn();
|
|
729
|
-
// Given an author listening for blur
|
|
730
|
-
const a1 = createAuthorInputState({ books: [{}] }, maybeAutoSave);
|
|
731
|
-
// When we programmatically set a field that isn't focused
|
|
732
|
-
a1.set({ firstName: "first" }, { refreshing: true });
|
|
733
|
-
// Then we don't call maybeAutoSave
|
|
734
|
-
expect(maybeAutoSave).toBeCalledTimes(0);
|
|
735
|
-
});
|
|
736
|
-
it("skips maybeAutoSave when resetting", () => {
|
|
737
|
-
const maybeAutoSave = jest.fn();
|
|
738
|
-
// Given an author listening for blur
|
|
739
|
-
const a1 = createAuthorInputState({ books: [{}] }, maybeAutoSave);
|
|
740
|
-
// And we called maybeAutoSave once
|
|
741
|
-
a1.set({ firstName: "first" });
|
|
742
|
-
expect(maybeAutoSave).toBeCalledTimes(1);
|
|
743
|
-
// When we reset
|
|
744
|
-
a1.revertChanges();
|
|
745
|
-
// We don't call blur again
|
|
746
|
-
expect(maybeAutoSave).toBeCalledTimes(1);
|
|
747
|
-
});
|
|
748
|
-
it("skips maybeAutoSave when not dirty", () => {
|
|
749
|
-
const maybeAutoSave = jest.fn();
|
|
750
|
-
// Given an author listening for blur
|
|
751
|
-
const a1 = createAuthorInputState({ firstName: "first", books: [{}] }, maybeAutoSave);
|
|
752
|
-
// When we programmatically set a field to it's existing valued
|
|
753
|
-
a1.firstName.value = "first";
|
|
754
|
-
// Then we don't call maybeAutoSave
|
|
755
|
-
expect(maybeAutoSave).toBeCalledTimes(0);
|
|
756
|
-
});
|
|
757
|
-
it("knows list of primitives are dirty", () => {
|
|
758
|
-
const a1 = (0, objectField_1.createObjectState)({ favoriteColors: { type: "value" } }, {});
|
|
759
|
-
expect(a1.favoriteColors.dirty).toBeFalsy();
|
|
760
|
-
a1.favoriteColors.set([formStateDomain_1.Color.Blue]);
|
|
761
|
-
expect(a1.dirty).toBeTruthy();
|
|
762
|
-
a1.favoriteColors.set(undefined);
|
|
763
|
-
expect(a1.dirty).toBeFalsy();
|
|
764
|
-
// Because we were originally undefined, setting as `[]` coerces to `undefined`
|
|
765
|
-
a1.favoriteColors.set([]);
|
|
766
|
-
expect(a1.dirty).toBeFalsy();
|
|
767
|
-
});
|
|
768
|
-
it("knows list of primitives are dirty with initialized as empty list", () => {
|
|
769
|
-
const a1 = (0, objectField_1.createObjectState)({ favoriteColors: { type: "value" } }, { favoriteColors: [] });
|
|
770
|
-
expect(a1.favoriteColors.dirty).toBeFalsy();
|
|
771
|
-
a1.favoriteColors.set([formStateDomain_1.Color.Blue]);
|
|
772
|
-
expect(a1.dirty).toBeTruthy();
|
|
773
|
-
a1.favoriteColors.set([]);
|
|
774
|
-
expect(a1.dirty).toBeFalsy();
|
|
775
|
-
// Because we were originally undefined, setting as `[]` coerces to `undefined`
|
|
776
|
-
a1.favoriteColors.set([]);
|
|
777
|
-
expect(a1.dirty).toBeFalsy();
|
|
778
|
-
});
|
|
779
|
-
it("can evaluate arrays to be equal even if order of options are different from original when strictOrder = false", () => {
|
|
780
|
-
// Given an array field value where we define `strictOrder` as false
|
|
781
|
-
const a1 = (0, objectField_1.createObjectState)({ favoriteColors: { type: "value", strictOrder: false } }, { favoriteColors: [formStateDomain_1.Color.Red, formStateDomain_1.Color.Blue] });
|
|
782
|
-
expect(a1.favoriteColors.dirty).toBeFalsy();
|
|
783
|
-
// When changing the order of the values
|
|
784
|
-
a1.favoriteColors.set([formStateDomain_1.Color.Blue, formStateDomain_1.Color.Red]);
|
|
785
|
-
// Then expect the field to not be dirty
|
|
786
|
-
expect(a1.dirty).toBeFalsy();
|
|
787
|
-
});
|
|
788
|
-
it("evaluates arrays to be equal based on order by default", () => {
|
|
789
|
-
// Given an array field value
|
|
790
|
-
const a1 = (0, objectField_1.createObjectState)({ favoriteColors: { type: "value" } }, { favoriteColors: [formStateDomain_1.Color.Red, formStateDomain_1.Color.Blue] });
|
|
791
|
-
expect(a1.favoriteColors.dirty).toBeFalsy();
|
|
792
|
-
// When we change the order of the values
|
|
793
|
-
a1.favoriteColors.set([formStateDomain_1.Color.Blue, formStateDomain_1.Color.Red]);
|
|
794
|
-
// Then the field should be dirty
|
|
795
|
-
expect(a1.dirty).toBeTruthy();
|
|
796
|
-
});
|
|
797
|
-
it("knows originally unset fields are dirty", () => {
|
|
798
|
-
// Given firstName is purposefully not set when originally initialized
|
|
799
|
-
const a1 = createAuthorInputState({});
|
|
800
|
-
expect(a1.firstName.dirty).toBeFalsy();
|
|
801
|
-
// When it is set
|
|
802
|
-
a1.firstName.value = "a1";
|
|
803
|
-
// Then it's dirty
|
|
804
|
-
expect(a1.firstName.dirty).toBeTruthy();
|
|
805
|
-
// And when it's set back to empty
|
|
806
|
-
a1.firstName.value = undefined;
|
|
807
|
-
// Then it's no longer dirty
|
|
808
|
-
expect(a1.firstName.dirty).toBeFalsy();
|
|
809
|
-
});
|
|
810
|
-
it("knows strings set to empty string should be undefined", () => {
|
|
811
|
-
const a1 = createAuthorInputState({ firstName: undefined });
|
|
812
|
-
a1.firstName.value = "";
|
|
813
|
-
expect(a1.firstName.value).toBeUndefined();
|
|
814
|
-
});
|
|
815
|
-
it("knows object fields are dirty", () => {
|
|
816
|
-
const a1 = createAuthorInputState({ firstName: "a1" });
|
|
817
|
-
expect(a1.dirty).toBeFalsy();
|
|
818
|
-
a1.firstName.set("a2");
|
|
819
|
-
expect(a1.dirty).toBeTruthy();
|
|
820
|
-
a1.firstName.set("a1");
|
|
821
|
-
expect(a1.dirty).toBeFalsy();
|
|
822
|
-
});
|
|
823
|
-
it("realizes object fields set to null aren't actually dirty", () => {
|
|
824
|
-
// Given we started off with a `object: undefined`
|
|
825
|
-
const a1 = createAuthorWithAddressInputState({ address: undefined });
|
|
826
|
-
expect(a1.address.dirty).toBe(false);
|
|
827
|
-
expect(a1.address.value).toEqual({});
|
|
828
|
-
// And it's later set to null, i.e. from a mutation returning `{ address: null }`
|
|
829
|
-
a1.set({ firstName: "a1", address: null });
|
|
830
|
-
// Then we convert it to `{}` to potentially hold data
|
|
831
|
-
expect(a1.address.value).toEqual({});
|
|
832
|
-
// But we don't consider the address dirty
|
|
833
|
-
expect(a1.address.dirty).toBe(false);
|
|
834
|
-
expect(a1.changedValue).toEqual({ firstName: "a1" });
|
|
835
|
-
});
|
|
836
|
-
it("knows an object's field of type object is dirty", () => {
|
|
837
|
-
const a1 = createAuthorInputState({
|
|
838
|
-
books: [{ title: "b1", classification: formStateDomain_1.dd100 }],
|
|
839
|
-
});
|
|
840
|
-
expect(a1.dirty).toBeFalsy();
|
|
841
|
-
a1.books.rows[0].set({ classification: formStateDomain_1.dd200 });
|
|
842
|
-
expect(a1.dirty).toBeTruthy();
|
|
843
|
-
a1.books.rows[0].set({ classification: formStateDomain_1.dd100 });
|
|
844
|
-
expect(a1.dirty).toBeFalsy();
|
|
845
|
-
});
|
|
846
|
-
it("resets values", () => {
|
|
847
|
-
const a1 = createAuthorInputState({
|
|
848
|
-
firstName: "a1",
|
|
849
|
-
lastName: "aL1",
|
|
850
|
-
books: [
|
|
851
|
-
{ title: "b1", classification: formStateDomain_1.dd100 },
|
|
852
|
-
{ title: "b2", classification: formStateDomain_1.dd100 },
|
|
853
|
-
],
|
|
854
|
-
});
|
|
855
|
-
expect(a1.dirty).toBeFalsy();
|
|
856
|
-
a1.firstName.set("a2");
|
|
857
|
-
a1.firstName.touched = true;
|
|
858
|
-
a1.lastName.set("aL2");
|
|
859
|
-
a1.books.rows[0].set({ title: "b2" });
|
|
860
|
-
a1.books.rows[1].set({ title: "bb2" });
|
|
861
|
-
a1.books.add({ title: "b3" });
|
|
862
|
-
expect(a1.books.touched).toEqual(true);
|
|
863
|
-
expect(a1.dirty).toBeTruthy();
|
|
864
|
-
a1.revertChanges();
|
|
865
|
-
expect(a1.firstName.value).toBe("a1");
|
|
866
|
-
expect(a1.firstName.touched).toBeFalsy();
|
|
867
|
-
expect(a1.lastName.value).toBe("aL1");
|
|
868
|
-
expect(a1.books.rows.length).toBe(2);
|
|
869
|
-
expect(a1.books.touched).toBe(false);
|
|
870
|
-
expect(a1.books.rows[0].title.value).toBe("b1");
|
|
871
|
-
expect(a1.books.rows[0].title.dirty).toBe(false);
|
|
872
|
-
expect(a1.books.rows[0].title.touched).toBe(false);
|
|
873
|
-
expect(a1.books.rows[1].title.value).toBe("b2");
|
|
874
|
-
expect(a1.dirty).toBeFalsy();
|
|
875
|
-
expect(a1.touched).toBeFalsy();
|
|
876
|
-
});
|
|
877
|
-
it("resets values even if read only", () => {
|
|
878
|
-
// Given a form state
|
|
879
|
-
const a1 = createAuthorInputState({
|
|
880
|
-
firstName: "a1",
|
|
881
|
-
lastName: "aL1",
|
|
882
|
-
books: [
|
|
883
|
-
{ id: "b:1", title: "b1", classification: formStateDomain_1.dd100 },
|
|
884
|
-
{ id: "b:2", title: "b2", classification: formStateDomain_1.dd100 },
|
|
885
|
-
],
|
|
886
|
-
});
|
|
887
|
-
// And some values have been changed
|
|
888
|
-
expect(a1.dirty).toBeFalsy();
|
|
889
|
-
a1.firstName.set("a2");
|
|
890
|
-
a1.firstName.touched = true;
|
|
891
|
-
a1.lastName.set("aL2");
|
|
892
|
-
a1.books.rows[0].set({ title: "b2" });
|
|
893
|
-
a1.books.rows[1].set({ title: "bb2" });
|
|
894
|
-
a1.books.add({ title: "b3" });
|
|
895
|
-
expect(a1.books.touched).toEqual(true);
|
|
896
|
-
expect(a1.dirty).toBeTruthy();
|
|
897
|
-
// And the "readOnly=true" operation has "beat" the reset operation
|
|
898
|
-
a1.readOnly = true;
|
|
899
|
-
// When we reset
|
|
900
|
-
a1.revertChanges();
|
|
901
|
-
// Then it still works
|
|
902
|
-
expect(a1.firstName.value).toBe("a1");
|
|
903
|
-
expect(a1.firstName.touched).toBeFalsy();
|
|
904
|
-
expect(a1.lastName.value).toBe("aL1");
|
|
905
|
-
expect(a1.books.rows.length).toBe(2);
|
|
906
|
-
expect(a1.books.touched).toBe(false);
|
|
907
|
-
expect(a1.books.rows[0].title.value).toBe("b1");
|
|
908
|
-
expect(a1.books.rows[0].title.dirty).toBe(false);
|
|
909
|
-
expect(a1.books.rows[0].title.touched).toBe(false);
|
|
910
|
-
expect(a1.books.rows[1].title.value).toBe("b2");
|
|
911
|
-
expect(a1.dirty).toBeFalsy();
|
|
912
|
-
expect(a1.touched).toBeFalsy();
|
|
913
|
-
});
|
|
914
|
-
it("saves values into _originalState", () => {
|
|
915
|
-
const a1 = createAuthorInputState({
|
|
916
|
-
firstName: "a1",
|
|
917
|
-
lastName: "aL1",
|
|
918
|
-
books: [{ title: "b1", classification: formStateDomain_1.dd100 }],
|
|
919
|
-
});
|
|
920
|
-
expect(a1.dirty).toBeFalsy();
|
|
921
|
-
// Now dirty things up.
|
|
922
|
-
a1.firstName.set("a2");
|
|
923
|
-
a1.lastName.set("aL2");
|
|
924
|
-
a1.books.rows[0].set({ title: "b2" });
|
|
925
|
-
a1.books.add({ title: "bb2" });
|
|
926
|
-
// Set book 2 to an different value. Ensures our save can traverse all rows
|
|
927
|
-
a1.books.rows[1].set({ title: "bb3" });
|
|
928
|
-
// verify ValueFieldState is dirty, then save, then no longer dirty.
|
|
929
|
-
expect(a1.firstName.dirty).toBeTruthy();
|
|
930
|
-
a1.firstName.commitChanges();
|
|
931
|
-
expect(a1.firstName.dirty).toBeFalsy();
|
|
932
|
-
// verify ListFieldState is dirty, then save, then no longer dirty.
|
|
933
|
-
expect(a1.books.dirty).toBeTruthy();
|
|
934
|
-
a1.books.commitChanges();
|
|
935
|
-
expect(a1.books.dirty).toBeFalsy();
|
|
936
|
-
// Verify the remaining form is still dirty
|
|
937
|
-
expect(a1.dirty).toBeTruthy();
|
|
938
|
-
a1.commitChanges();
|
|
939
|
-
// Verify after save the whole form is no longer dirty.
|
|
940
|
-
expect(a1.dirty).toBeFalsy();
|
|
941
|
-
});
|
|
942
|
-
it("can touch everything at once", () => {
|
|
943
|
-
const a1 = createAuthorInputState({ firstName: "a1", books: [{ title: "b1" }] });
|
|
944
|
-
expect(a1.firstName.touched).toBeFalsy();
|
|
945
|
-
expect(a1.books.touched).toBeFalsy();
|
|
946
|
-
expect(a1.books.rows[0].title.touched).toBeFalsy();
|
|
947
|
-
expect(a1.touched).toBeFalsy();
|
|
948
|
-
a1.touched = true;
|
|
949
|
-
expect(a1.firstName.touched).toBeTruthy();
|
|
950
|
-
expect(a1.books.touched).toBeTruthy();
|
|
951
|
-
expect(a1.books.rows[0].title.touched).toBeTruthy();
|
|
952
|
-
expect(a1.touched).toBeTruthy();
|
|
953
|
-
});
|
|
954
|
-
it("remembers deleted values as null", () => {
|
|
955
|
-
// Given a property that is initially set
|
|
956
|
-
const a1 = createAuthorInputState({ firstName: "asdf" });
|
|
957
|
-
// When it's set to an empty/undefined value
|
|
958
|
-
a1.firstName.value = "";
|
|
959
|
-
// Then we keep it as null
|
|
960
|
-
expect(a1.firstName.value).toBeNull();
|
|
961
|
-
expect(a1.originalValue.firstName).toBe("asdf");
|
|
962
|
-
expect(a1.firstName.dirty).toBeTruthy();
|
|
963
|
-
// And when the server acks as null
|
|
964
|
-
a1.set({ firstName: undefined }, { refreshing: true });
|
|
965
|
-
// Then it's no longer dirty
|
|
966
|
-
expect(a1.firstName.dirty).toBe(false);
|
|
967
|
-
expect(a1.firstName.originalValue).toBe(undefined);
|
|
968
|
-
expect(a1.firstName.value).toBe(undefined);
|
|
969
|
-
expect(a1.changedValue).toEqual({});
|
|
970
|
-
});
|
|
971
|
-
it("initializes null values to be undefined", () => {
|
|
972
|
-
// Given a property that is initially set to null
|
|
973
|
-
const a1 = createAuthorInputState({ firstName: null });
|
|
974
|
-
// Then expect it to be set to undefined
|
|
975
|
-
expect(a1.firstName.value).toBeUndefined();
|
|
976
|
-
});
|
|
977
|
-
it("can keep initially undefined values as null", () => {
|
|
978
|
-
// Given a property that is initially set
|
|
979
|
-
const a1 = createAuthorInputState({ firstName: "foo" });
|
|
980
|
-
// When we set it to null
|
|
981
|
-
a1.firstName.value = null;
|
|
982
|
-
// Then that isn't lost
|
|
983
|
-
expect(a1.firstName.value).toBeNull();
|
|
984
|
-
expect(a1.value.firstName).toBeNull();
|
|
985
|
-
});
|
|
986
|
-
it("can map properties to other types", () => {
|
|
987
|
-
const a1 = (0, objectField_1.createObjectState)({
|
|
988
|
-
person: { type: "value" },
|
|
989
|
-
}, { person: { firstName: "a1", lastName: "b1" } });
|
|
990
|
-
a1.person.set({ firstName: "a2", lastName: "b2" });
|
|
991
|
-
const inputWithPerson = a1.value;
|
|
992
|
-
const { firstName, lastName } = inputWithPerson.person;
|
|
993
|
-
const input = { ...inputWithPerson, firstName, lastName };
|
|
994
|
-
expect(input.firstName).toEqual("a2");
|
|
995
|
-
expect(input.lastName).toEqual("b2");
|
|
996
|
-
});
|
|
997
|
-
it("has readonly", () => {
|
|
998
|
-
const a1 = createAuthorInputState({
|
|
999
|
-
firstName: "a1",
|
|
1000
|
-
lastName: "aL1",
|
|
1001
|
-
books: [{ title: "b1", classification: formStateDomain_1.dd100 }],
|
|
1002
|
-
});
|
|
1003
|
-
const fields = [a1, a1.firstName, a1.books, a1.books.rows[0].title, a1.books.rows[0].classification];
|
|
1004
|
-
fields.forEach((f) => expect(f.readOnly).toBeFalsy());
|
|
1005
|
-
a1.readOnly = true;
|
|
1006
|
-
fields.forEach((f) => expect(f.readOnly).toBeTruthy());
|
|
1007
|
-
fields.forEach((f) => {
|
|
1008
|
-
expect(() => f.set(null)).toThrow("is currently readOnly");
|
|
1009
|
-
});
|
|
1010
|
-
});
|
|
1011
|
-
it("maintain field readOnly state when form is readOnly", () => {
|
|
1012
|
-
// Given a formState
|
|
1013
|
-
const formState = (0, objectField_1.createObjectState)({ title: { type: "value", rules: [rules_1.required], readOnly: true } }, {});
|
|
1014
|
-
// Then expect form
|
|
1015
|
-
expect(formState.title.readOnly).toBeTruthy();
|
|
1016
|
-
});
|
|
1017
|
-
it("has an object-level readOnly=true override field-level readOnly=false", () => {
|
|
1018
|
-
// Given a top-level object
|
|
1019
|
-
const a1 = createAuthorInputState({
|
|
1020
|
-
firstName: "a1",
|
|
1021
|
-
lastName: "aL1",
|
|
1022
|
-
books: [{ title: "b1", classification: formStateDomain_1.dd100 }],
|
|
1023
|
-
});
|
|
1024
|
-
// And it is read-only
|
|
1025
|
-
a1.readOnly = true;
|
|
1026
|
-
// Then its fields are read-only
|
|
1027
|
-
const fields = [a1, a1.firstName, a1.books, a1.books.rows[0].title, a1.books.rows[0].classification];
|
|
1028
|
-
fields.forEach((f) => expect(f.readOnly).toBeTruthy());
|
|
1029
|
-
// And even if a specific field tries to _not_ be read-only,
|
|
1030
|
-
// i.e. due to a more granular business rule that happens to
|
|
1031
|
-
// be allowed right now
|
|
1032
|
-
a1.firstName.readOnly = false;
|
|
1033
|
-
// Then the field-level rule is ignored, and it's still treated as read-only
|
|
1034
|
-
expect(a1.firstName.readOnly).toBeTruthy();
|
|
1035
|
-
});
|
|
1036
|
-
it("has an field-level readOnly=true override object-level readOnly=false", () => {
|
|
1037
|
-
// Given a top-level object
|
|
1038
|
-
const a1 = createAuthorInputState({
|
|
1039
|
-
firstName: "a1",
|
|
1040
|
-
lastName: "aL1",
|
|
1041
|
-
books: [{ title: "b1", classification: formStateDomain_1.dd100 }],
|
|
1042
|
-
});
|
|
1043
|
-
// And the top-level form is explicitly set to read-only=false
|
|
1044
|
-
a1.readOnly = false;
|
|
1045
|
-
// Then its fields are not read-only
|
|
1046
|
-
const fields = [a1, a1.firstName, a1.books, a1.books.rows[0].title, a1.books.rows[0].classification];
|
|
1047
|
-
fields.forEach((f) => expect(f.readOnly).toBeFalsy());
|
|
1048
|
-
// But when one of the fields opts in to readOnly
|
|
1049
|
-
a1.firstName.readOnly = true;
|
|
1050
|
-
// Then the field-level rule is respected
|
|
1051
|
-
expect(a1.firstName.readOnly).toBeTruthy();
|
|
1052
|
-
});
|
|
1053
|
-
it("has an object-level loading=true override field-level loading=false", () => {
|
|
1054
|
-
// Given a top-level object
|
|
1055
|
-
const a1 = createAuthorInputState({
|
|
1056
|
-
firstName: "a1",
|
|
1057
|
-
lastName: "aL1",
|
|
1058
|
-
books: [{ title: "b1", classification: formStateDomain_1.dd100 }],
|
|
1059
|
-
});
|
|
1060
|
-
// And it is loading
|
|
1061
|
-
a1.loading = true;
|
|
1062
|
-
// Then its fields are read-only
|
|
1063
|
-
const fields = [a1, a1.firstName, a1.books, a1.books.rows[0].title, a1.books.rows[0].classification];
|
|
1064
|
-
fields.forEach((f) => expect(f.loading).toBeTruthy());
|
|
1065
|
-
// And even if a specific field tries to _not_ be loading
|
|
1066
|
-
a1.firstName.loading = false;
|
|
1067
|
-
// Then the field-level value is ignored, and it's still treated as loading
|
|
1068
|
-
expect(a1.firstName.loading).toBeTruthy();
|
|
1069
|
-
});
|
|
1070
|
-
it("has an field-level loading=true override object-level loading=false", () => {
|
|
1071
|
-
// Given a top-level object
|
|
1072
|
-
const a1 = createAuthorInputState({
|
|
1073
|
-
firstName: "a1",
|
|
1074
|
-
lastName: "aL1",
|
|
1075
|
-
books: [{ title: "b1", classification: formStateDomain_1.dd100 }],
|
|
1076
|
-
});
|
|
1077
|
-
// And the top-level form is explicitly set to loading=false
|
|
1078
|
-
a1.loading = false;
|
|
1079
|
-
// Then its fields are not loading
|
|
1080
|
-
const fields = [a1, a1.firstName, a1.books, a1.books.rows[0].title, a1.books.rows[0].classification];
|
|
1081
|
-
fields.forEach((f) => expect(f.loading).toBeFalsy());
|
|
1082
|
-
// But when one of the fields opts in to loading
|
|
1083
|
-
a1.firstName.loading = true;
|
|
1084
|
-
// Then the field-level loading is respected
|
|
1085
|
-
expect(a1.firstName.loading).toBeTruthy();
|
|
1086
|
-
});
|
|
1087
|
-
it("canSave returns dirty and touches", () => {
|
|
1088
|
-
const a1 = (0, objectField_1.createObjectState)({
|
|
1089
|
-
firstName: { type: "value", rules: [rules_1.required] },
|
|
1090
|
-
}, {});
|
|
1091
|
-
expect(a1.firstName.touched).toBeFalsy();
|
|
1092
|
-
expect(a1.canSave()).toBeFalsy();
|
|
1093
|
-
expect(a1.firstName.touched).toBeTruthy();
|
|
1094
|
-
});
|
|
1095
|
-
it("uses toJSON if available for dirty checks", () => {
|
|
1096
|
-
const a1 = (0, objectField_1.createObjectState)({
|
|
1097
|
-
birthday: { type: "value" },
|
|
1098
|
-
}, { birthday: new formStateDomain_1.DateOnly(formStateDomain_1.jan1) });
|
|
1099
|
-
a1.birthday.set(new formStateDomain_1.DateOnly(formStateDomain_1.jan1));
|
|
1100
|
-
expect(a1.birthday.dirty).toBeFalsy();
|
|
1101
|
-
});
|
|
1102
|
-
it("can create a nested object", () => {
|
|
1103
|
-
const a = createAuthorWithAddressInputState({});
|
|
1104
|
-
expect(a.valid).toBeFalsy();
|
|
1105
|
-
});
|
|
1106
|
-
it("can validate a nested input", () => {
|
|
1107
|
-
const a = createAuthorWithAddressInputState({ address: { street: "b1" } });
|
|
1108
|
-
expect(a.valid).toBeTruthy();
|
|
1109
|
-
});
|
|
1110
|
-
it("can validate a list input", () => {
|
|
1111
|
-
const a = (0, objectField_1.createObjectState)({
|
|
1112
|
-
books: {
|
|
1113
|
-
type: "list",
|
|
1114
|
-
config: { title: { type: "value", rules: [rules_1.required] } },
|
|
1115
|
-
rules: [({ value }) => (value.find((b) => b.title.value === "t1") ? "Cannot have t1" : undefined)],
|
|
1116
|
-
},
|
|
1117
|
-
}, {});
|
|
1118
|
-
let lastErrors = "";
|
|
1119
|
-
(0, mobx_1.autorun)(() => {
|
|
1120
|
-
lastErrors = a.books.errors.join(", ");
|
|
1121
|
-
});
|
|
1122
|
-
a.books.add({ title: "t1" });
|
|
1123
|
-
expect(lastErrors).toEqual("Cannot have t1");
|
|
1124
|
-
a.books.rows[0].title.value = "t2";
|
|
1125
|
-
expect(lastErrors).toEqual("");
|
|
1126
|
-
});
|
|
1127
|
-
it("can ignore a deleted list entry", () => {
|
|
1128
|
-
const a = (0, objectField_1.createObjectState)({
|
|
1129
|
-
books: {
|
|
1130
|
-
type: "list",
|
|
1131
|
-
config: {
|
|
1132
|
-
title: { type: "value", rules: [rules_1.required] },
|
|
1133
|
-
delete: { type: "value", isDeleteKey: true },
|
|
1134
|
-
},
|
|
1135
|
-
},
|
|
1136
|
-
}, {});
|
|
1137
|
-
// Given we have a known-bad list entry
|
|
1138
|
-
a.books.add({ title: null, delete: false });
|
|
1139
|
-
// And are initially invalid
|
|
1140
|
-
expect(a.books.valid).toBeFalsy();
|
|
1141
|
-
// When the list entry is deleted
|
|
1142
|
-
a.books.rows[0].delete.value = true;
|
|
1143
|
-
// Then we're valid
|
|
1144
|
-
expect(a.books.valid).toBeTruthy();
|
|
1145
|
-
});
|
|
1146
|
-
it("can treat a list entry as read-only", () => {
|
|
1147
|
-
const a = (0, objectField_1.createObjectState)({
|
|
1148
|
-
books: {
|
|
1149
|
-
type: "list",
|
|
1150
|
-
config: {
|
|
1151
|
-
title: { type: "value", rules: [rules_1.required] },
|
|
1152
|
-
isPublished: { type: "value", isReadOnlyKey: true },
|
|
1153
|
-
},
|
|
1154
|
-
},
|
|
1155
|
-
}, {});
|
|
1156
|
-
// Given we have a published book
|
|
1157
|
-
a.books.add({ title: null, isPublished: true });
|
|
1158
|
-
// And an unpublished book
|
|
1159
|
-
a.books.add({ title: null, isPublished: false });
|
|
1160
|
-
// And we currently depend on the form.readOnly being invoked
|
|
1161
|
-
a.readOnly = false;
|
|
1162
|
-
// Then the 1st one is read only
|
|
1163
|
-
expect(a.books.rows[0].readOnly).toBeTruthy();
|
|
1164
|
-
expect(a.books.rows[0].title.readOnly).toBeTruthy();
|
|
1165
|
-
// And the 2nd one is not
|
|
1166
|
-
expect(a.books.rows[1].readOnly).toBeFalsy();
|
|
1167
|
-
expect(a.books.rows[1].title.readOnly).toBeFalsy();
|
|
1168
|
-
// And reset does not blow up
|
|
1169
|
-
a.revertChanges();
|
|
1170
|
-
});
|
|
1171
|
-
it("can set nested values when original null", () => {
|
|
1172
|
-
const a = createAuthorWithAddressInputState({ address: null });
|
|
1173
|
-
a.address.city.value = "b1";
|
|
1174
|
-
expect(a.address.city.value).toEqual("b1");
|
|
1175
|
-
expect(a.address.value).toMatchInlineSnapshot(`
|
|
1176
|
-
{
|
|
1177
|
-
"city": "b1",
|
|
1178
|
-
}
|
|
1179
|
-
`);
|
|
1180
|
-
expect(a.value).toMatchInlineSnapshot(`
|
|
1181
|
-
{
|
|
1182
|
-
"address": {
|
|
1183
|
-
"city": "b1",
|
|
1184
|
-
},
|
|
1185
|
-
}
|
|
1186
|
-
`);
|
|
1187
|
-
});
|
|
1188
|
-
it("can wrap an existing list observable", () => {
|
|
1189
|
-
// Given an array observable that is already created
|
|
1190
|
-
const b1 = { title: "b1" };
|
|
1191
|
-
const books = (0, mobx_1.observable)([b1]);
|
|
1192
|
-
// When we create a form state around it
|
|
1193
|
-
const formState = createAuthorInputState({ books });
|
|
1194
|
-
// Then the initial book was copied over
|
|
1195
|
-
let lastLength = 0;
|
|
1196
|
-
(0, mobx_1.autorun)(() => {
|
|
1197
|
-
lastLength = formState.books.rows.length;
|
|
1198
|
-
});
|
|
1199
|
-
expect(lastLength).toEqual(1);
|
|
1200
|
-
// And when we add a new book to the original observable
|
|
1201
|
-
books.push({ title: "b2" });
|
|
1202
|
-
// Then the formState saw it
|
|
1203
|
-
expect(lastLength).toEqual(2);
|
|
1204
|
-
});
|
|
1205
|
-
it("can reset observable objects with computeds", () => {
|
|
1206
|
-
const formState = (0, objectField_1.createObjectState)({
|
|
1207
|
-
firstName: { type: "value" },
|
|
1208
|
-
lastName: { type: "value" },
|
|
1209
|
-
fullName: { type: "value" },
|
|
1210
|
-
}, new ObservableObject());
|
|
1211
|
-
expect(formState.firstName.value).toEqual("first");
|
|
1212
|
-
expect(formState.fullName.value).toEqual("first last");
|
|
1213
|
-
expect(formState.fullName.dirty).toEqual(false);
|
|
1214
|
-
formState.firstName.value = "change";
|
|
1215
|
-
expect(formState.fullName.value).toEqual("change last");
|
|
1216
|
-
expect(formState.fullName.dirty).toEqual(true);
|
|
1217
|
-
formState.revertChanges();
|
|
1218
|
-
expect(formState.firstName.value).toEqual("first");
|
|
1219
|
-
});
|
|
1220
|
-
it("can observe observable objects being mutated directly", () => {
|
|
1221
|
-
// Given a mobx class with a computed
|
|
1222
|
-
const instance = new ObservableObject();
|
|
1223
|
-
const formState = (0, objectField_1.createObjectState)(authorWithFullName, instance);
|
|
1224
|
-
expect(formState.firstName.value).toEqual("first");
|
|
1225
|
-
expect(formState.fullName.value).toEqual("first last");
|
|
1226
|
-
// And we hook up reactivity to the computed
|
|
1227
|
-
let numCalcs = 0;
|
|
1228
|
-
(0, mobx_1.autorun)(() => {
|
|
1229
|
-
numCalcs++;
|
|
1230
|
-
noop(formState.fullName.value);
|
|
1231
|
-
});
|
|
1232
|
-
// When the underlying mobx class is directly changed
|
|
1233
|
-
instance.firstName = "change";
|
|
1234
|
-
// Then our form-state computed re-run
|
|
1235
|
-
expect(numCalcs).toBe(2);
|
|
1236
|
-
expect(formState.fullName.value).toEqual("change last");
|
|
1237
|
-
// And when the underlying mobx class has a field unset
|
|
1238
|
-
instance.firstName = undefined;
|
|
1239
|
-
// Then our form-state computed re-runs again
|
|
1240
|
-
expect(numCalcs).toBe(3);
|
|
1241
|
-
expect(formState.fullName.value).toEqual("undefined last");
|
|
1242
|
-
// And knows the field should be sent as null
|
|
1243
|
-
expect(formState.changedValue).toEqual({ firstName: null });
|
|
1244
|
-
});
|
|
1245
|
-
it("can set computeds that have setters", () => {
|
|
1246
|
-
const formState = (0, objectField_1.createObjectState)({
|
|
1247
|
-
firstName: { type: "value" },
|
|
1248
|
-
lastName: { type: "value" },
|
|
1249
|
-
fullName: { type: "value", computed: true },
|
|
1250
|
-
}, new ObservableObject());
|
|
1251
|
-
formState.fullName.value = "Bob Smith";
|
|
1252
|
-
expect(formState.firstName.value).toEqual("Bob");
|
|
1253
|
-
formState.fullName.set("Fred Smith");
|
|
1254
|
-
expect(formState.firstName.value).toEqual("Fred");
|
|
1255
|
-
formState.revertChanges();
|
|
1256
|
-
expect(formState.firstName.value).toEqual("first");
|
|
1257
|
-
});
|
|
1258
|
-
it("can mark fields as read only", () => {
|
|
1259
|
-
const formState = (0, objectField_1.createObjectState)({
|
|
1260
|
-
firstName: { type: "value" },
|
|
1261
|
-
lastName: { type: "value" },
|
|
1262
|
-
fullName: { type: "value", readOnly: true },
|
|
1263
|
-
}, new ObservableObject());
|
|
1264
|
-
expect(formState.fullName.value).toEqual("first last");
|
|
1265
|
-
expect(formState.fullName.readOnly).toEqual(true);
|
|
1266
|
-
formState.firstName.value = "change";
|
|
1267
|
-
expect(formState.fullName.value).toEqual("change last");
|
|
1268
|
-
formState.revertChanges();
|
|
1269
|
-
expect(formState.firstName.value).toEqual("first");
|
|
1270
|
-
});
|
|
1271
|
-
it("can return only changed primitive fields", () => {
|
|
1272
|
-
// Given an author
|
|
1273
|
-
const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, {
|
|
1274
|
-
id: "a:1",
|
|
1275
|
-
firstName: "f",
|
|
1276
|
-
lastName: "l",
|
|
1277
|
-
books: [{ title: "t1" }],
|
|
1278
|
-
});
|
|
1279
|
-
// And initially nothing is changed
|
|
1280
|
-
expect(formState.changedValue).toEqual({ id: "a:1" });
|
|
1281
|
-
// When we change the last name
|
|
1282
|
-
formState.lastName.value = "l2";
|
|
1283
|
-
// Then only the id (for updates) and last name are in changed value
|
|
1284
|
-
expect(formState.changedValue).toEqual({
|
|
1285
|
-
id: "a:1",
|
|
1286
|
-
lastName: "l2",
|
|
1287
|
-
});
|
|
1288
|
-
});
|
|
1289
|
-
it("can return only changed object fields", () => {
|
|
1290
|
-
// Given an author with an address object
|
|
1291
|
-
const formState = (0, objectField_1.createObjectState)(authorWithAddressConfig, {
|
|
1292
|
-
id: "a:1",
|
|
1293
|
-
firstName: "f",
|
|
1294
|
-
address: { city: "c1", street: "s1" },
|
|
1295
|
-
});
|
|
1296
|
-
// And initially nothing is changed
|
|
1297
|
-
expect(formState.changedValue).toEqual({ id: "a:1" });
|
|
1298
|
-
// When we change the sub object
|
|
1299
|
-
formState.address.street.value = "s2";
|
|
1300
|
-
// Then we get the author id (for update) and only what changed in the address
|
|
1301
|
-
expect(formState.changedValue).toEqual({
|
|
1302
|
-
id: "a:1",
|
|
1303
|
-
address: { street: "s2" },
|
|
1304
|
-
});
|
|
1305
|
-
});
|
|
1306
|
-
it("changedValue skips empty nested fields", () => {
|
|
1307
|
-
// Given a new author with an address object
|
|
1308
|
-
const formState = (0, objectField_1.createObjectState)(authorWithAddressConfig, {
|
|
1309
|
-
firstName: "f",
|
|
1310
|
-
// And nothing is in the address
|
|
1311
|
-
address: {},
|
|
1312
|
-
});
|
|
1313
|
-
// Then we don't include an empty address in the output,
|
|
1314
|
-
// because this might trigger creating a throw-away entity
|
|
1315
|
-
expect(formState.changedValue).toEqual({ firstName: "f" });
|
|
1316
|
-
});
|
|
1317
|
-
it("changedValue includes initialized-and-unchanged list fields", () => {
|
|
1318
|
-
// Given a new author that is initialized with a new book
|
|
1319
|
-
const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, {
|
|
1320
|
-
books: [{ title: "t1" }],
|
|
1321
|
-
});
|
|
1322
|
-
// Then changedValue realizes that even though `books` is not changed, it should be included
|
|
1323
|
-
expect(formState.books.dirty).toBe(false);
|
|
1324
|
-
expect(formState.changedValue).toEqual({ books: [{ title: "t1" }] });
|
|
1325
|
-
});
|
|
1326
|
-
it("changedValue skips initialized-and-unchanged list fields that are empty", () => {
|
|
1327
|
-
// Given a new author that is initialized with no books
|
|
1328
|
-
const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, {
|
|
1329
|
-
books: [],
|
|
1330
|
-
});
|
|
1331
|
-
// Then changedValue doesn't include the books
|
|
1332
|
-
expect(formState.changedValue).toEqual({});
|
|
1333
|
-
});
|
|
1334
|
-
it("changedValue includes new entity nested fields", () => {
|
|
1335
|
-
// Given a new author with an address FK
|
|
1336
|
-
const formState = (0, objectField_1.createObjectState)(authorWithAddressFkConfig, {
|
|
1337
|
-
firstName: "f",
|
|
1338
|
-
address: { id: "add:1" },
|
|
1339
|
-
});
|
|
1340
|
-
// Then changedValue includes the reference to the FK
|
|
1341
|
-
expect(formState.changedValue).toEqual({
|
|
1342
|
-
firstName: "f",
|
|
1343
|
-
address: { id: "add:1" },
|
|
1344
|
-
});
|
|
1345
|
-
});
|
|
1346
|
-
it("changedValue includes new entity nested fields with multiple keys", () => {
|
|
1347
|
-
// Given a new author with an address FK
|
|
1348
|
-
const formState = (0, objectField_1.createObjectState)(authorWithAddressFkConfig, { firstName: "f" });
|
|
1349
|
-
formState.address.set({ id: "add:1", street: "Main St" });
|
|
1350
|
-
// Then changedValue includes the reference to the FK
|
|
1351
|
-
expect(formState.changedValue).toEqual({
|
|
1352
|
-
firstName: "f",
|
|
1353
|
-
address: { id: "add:1", street: "Main St" },
|
|
1354
|
-
});
|
|
1355
|
-
});
|
|
1356
|
-
it("changedValue includes deleted nested fields", () => {
|
|
1357
|
-
var _a;
|
|
1358
|
-
// Given a new author with an address FK
|
|
1359
|
-
const formState = (0, objectField_1.createObjectState)(configBuilders_1.f.config({
|
|
1360
|
-
id: configBuilders_1.f.value(),
|
|
1361
|
-
firstName: configBuilders_1.f.value(),
|
|
1362
|
-
// And address is a reference
|
|
1363
|
-
address: configBuilders_1.f.reference(),
|
|
1364
|
-
}), { id: "a:1", firstName: "f", address: { id: "add:1" } });
|
|
1365
|
-
// When the address is unset
|
|
1366
|
-
formState.address.set(undefined);
|
|
1367
|
-
// Then the field is dirty and will be removed in changedValue
|
|
1368
|
-
expect(formState.address.dirty).toBe(true);
|
|
1369
|
-
expect(formState.dirty).toBe(true);
|
|
1370
|
-
expect(formState.changedValue).toEqual({
|
|
1371
|
-
id: "a:1",
|
|
1372
|
-
address: { id: null },
|
|
1373
|
-
});
|
|
1374
|
-
// So that we can do a binding like
|
|
1375
|
-
const addressId = (_a = formState.changedValue.address) === null || _a === void 0 ? void 0 : _a.id;
|
|
1376
|
-
expect(addressId).toBeNull();
|
|
1377
|
-
// And when we're restored to the same value
|
|
1378
|
-
formState.address.set({ id: "add:1" });
|
|
1379
|
-
// Then it's back to not being dirty
|
|
1380
|
-
expect(formState.address.dirty).toBe(false);
|
|
1381
|
-
expect(formState.dirty).toBe(false);
|
|
1382
|
-
// And when we're changed to a different address
|
|
1383
|
-
formState.address.set({ id: "add:2" });
|
|
1384
|
-
// Then we're dirty again
|
|
1385
|
-
expect(formState.changedValue).toEqual({
|
|
1386
|
-
id: "a:1",
|
|
1387
|
-
address: { id: "add:2" },
|
|
1388
|
-
});
|
|
1389
|
-
});
|
|
1390
|
-
it("changedValue includes deleted nested fields with a name", () => {
|
|
1391
|
-
// Given a new author with an address FK
|
|
1392
|
-
const formState = (0, objectField_1.createObjectState)(configBuilders_1.f.config({
|
|
1393
|
-
id: configBuilders_1.f.value(),
|
|
1394
|
-
firstName: configBuilders_1.f.value(),
|
|
1395
|
-
address: configBuilders_1.f.reference({ street: configBuilders_1.f.value() }),
|
|
1396
|
-
}), { id: "a:1", firstName: "f", address: { id: "add:1", street: "Main St" } });
|
|
1397
|
-
// Initially both id and street can be bound to the UI
|
|
1398
|
-
expect(formState.address.id.value).toBe("add:1");
|
|
1399
|
-
expect(formState.address.street.value).toBe("Main St");
|
|
1400
|
-
// When the address is unset
|
|
1401
|
-
formState.address.set(undefined);
|
|
1402
|
-
// Then the field is dirty and will be removed in changedValue
|
|
1403
|
-
expect(formState.address.dirty).toBe(true);
|
|
1404
|
-
// And our changedValue only includes the id
|
|
1405
|
-
expect(formState.changedValue).toEqual({
|
|
1406
|
-
id: "a:1",
|
|
1407
|
-
address: { id: null },
|
|
1408
|
-
});
|
|
1409
|
-
// And when we're changed to a new address
|
|
1410
|
-
formState.address.set({ id: "add:2", street: "Side St" });
|
|
1411
|
-
// Then we see oly the id come back in changedValue
|
|
1412
|
-
expect(formState.changedValue).toEqual({
|
|
1413
|
-
id: "a:1",
|
|
1414
|
-
address: { id: "add:2" },
|
|
1415
|
-
});
|
|
1416
|
-
});
|
|
1417
|
-
it("changedValue skips effectively empty nested fields", () => {
|
|
1418
|
-
// Given a new author with an address object
|
|
1419
|
-
const formState = (0, objectField_1.createObjectState)(authorWithAddressConfig, {
|
|
1420
|
-
firstName: "f",
|
|
1421
|
-
// And only undefined keys are in the address
|
|
1422
|
-
address: { city: undefined },
|
|
1423
|
-
});
|
|
1424
|
-
// Then we don't include an empty address in the output,
|
|
1425
|
-
// because this might trigger creating a throw-away entity
|
|
1426
|
-
expect(formState.changedValue).toEqual({ firstName: "f" });
|
|
1427
|
-
});
|
|
1428
|
-
it("changedValue skips computeds", () => {
|
|
1429
|
-
// Given a new author that calls full name
|
|
1430
|
-
const formState = (0, objectField_1.createObjectState)(authorWithFullName, new ObservableObject());
|
|
1431
|
-
expect(formState.fullName.value).toEqual("first last");
|
|
1432
|
-
// When the firstName changes
|
|
1433
|
-
formState.firstName.value = "First";
|
|
1434
|
-
// Then the fullName changes
|
|
1435
|
-
expect(formState.fullName.value).toBe("First last");
|
|
1436
|
-
expect(formState.fullName.dirty).toBe(true);
|
|
1437
|
-
// And we don't include it in the changedValue
|
|
1438
|
-
expect(formState.changedValue).toEqual({ firstName: "First" });
|
|
1439
|
-
});
|
|
1440
|
-
it("can return only changed and exhaustive list fields", () => {
|
|
1441
|
-
// Given an author with some books
|
|
1442
|
-
const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, {
|
|
1443
|
-
id: "a:1",
|
|
1444
|
-
firstName: "f",
|
|
1445
|
-
books: [
|
|
1446
|
-
{ id: "b:1", title: "t1" },
|
|
1447
|
-
{ id: "b:2", title: "t2" },
|
|
1448
|
-
],
|
|
1449
|
-
});
|
|
1450
|
-
// And initially nothing is changed
|
|
1451
|
-
expect(formState.changedValue).toEqual({ id: "a:1" });
|
|
1452
|
-
// When we change the 1st book
|
|
1453
|
-
formState.books.rows[0].title.value = "t1b";
|
|
1454
|
-
// Then we get the author id (for updates) and both books b/c `author.books = [...]` is
|
|
1455
|
-
// assumed to be an exhaustive set and we don't want to orphan the 2nd book.
|
|
1456
|
-
expect(formState.changedValue).toEqual({
|
|
1457
|
-
id: "a:1",
|
|
1458
|
-
books: [{ id: "b:1", title: "t1b" }, { id: "b:2" }],
|
|
1459
|
-
});
|
|
1460
|
-
// And add a 3rd new book
|
|
1461
|
-
formState.books.add({ title: "t3" });
|
|
1462
|
-
// Then we get the author id (for updates) and all 3 books
|
|
1463
|
-
expect(formState.changedValue).toEqual({
|
|
1464
|
-
id: "a:1",
|
|
1465
|
-
books: [{ id: "b:1", title: "t1b" }, { id: "b:2" }, { title: "t3" }],
|
|
1466
|
-
});
|
|
1467
|
-
// And we can still get the original value
|
|
1468
|
-
expect(formState.originalValue).toEqual({
|
|
1469
|
-
id: "a:1",
|
|
1470
|
-
firstName: "f",
|
|
1471
|
-
books: [
|
|
1472
|
-
{ id: "b:1", title: "t1" },
|
|
1473
|
-
{ id: "b:2", title: "t2" },
|
|
1474
|
-
],
|
|
1475
|
-
});
|
|
1476
|
-
// And the books.originalValue as well
|
|
1477
|
-
expect(formState.books.originalValue).toEqual([
|
|
1478
|
-
{ id: "b:1", title: "t1" },
|
|
1479
|
-
{ id: "b:2", title: "t2" },
|
|
1480
|
-
]);
|
|
1481
|
-
// And when we commit changes
|
|
1482
|
-
formState.commitChanges();
|
|
1483
|
-
// Then our originalValue reflects the commit
|
|
1484
|
-
expect(formState.originalValue).toEqual({
|
|
1485
|
-
id: "a:1",
|
|
1486
|
-
firstName: "f",
|
|
1487
|
-
books: [
|
|
1488
|
-
{ id: "b:1", title: "t1b" },
|
|
1489
|
-
{ id: "b:2", title: "t2" },
|
|
1490
|
-
{ id: undefined, title: "t3" },
|
|
1491
|
-
],
|
|
1492
|
-
});
|
|
1493
|
-
// And the books.originalValue as well
|
|
1494
|
-
expect(formState.books.originalValue).toEqual([
|
|
1495
|
-
{ id: "b:1", title: "t1b" },
|
|
1496
|
-
{ id: "b:2", title: "t2" },
|
|
1497
|
-
{ title: "t3" },
|
|
1498
|
-
]);
|
|
1499
|
-
});
|
|
1500
|
-
it("can return only changed but incremental list fields", () => {
|
|
1501
|
-
// Given an author
|
|
1502
|
-
const formState = (0, objectField_1.createObjectState)({
|
|
1503
|
-
id: { type: "value" },
|
|
1504
|
-
// And the books collection is marked as incremental (i.e. line items)
|
|
1505
|
-
books: {
|
|
1506
|
-
type: "list",
|
|
1507
|
-
update: "incremental",
|
|
1508
|
-
config: {
|
|
1509
|
-
id: { type: "value" },
|
|
1510
|
-
title: { type: "value", rules: [rules_1.required] },
|
|
1511
|
-
},
|
|
1512
|
-
},
|
|
1513
|
-
}, {
|
|
1514
|
-
id: "a:1",
|
|
1515
|
-
firstName: "f",
|
|
1516
|
-
books: [
|
|
1517
|
-
{ id: "b:1", title: "t1" },
|
|
1518
|
-
{ id: "b:2", title: "t2" },
|
|
1519
|
-
],
|
|
1520
|
-
});
|
|
1521
|
-
// And initially nothing is changed
|
|
1522
|
-
expect(formState.changedValue).toEqual({ id: "a:1" });
|
|
1523
|
-
// When we change the 1st book
|
|
1524
|
-
formState.books.rows[0].title.value = "t1b";
|
|
1525
|
-
// Then only the 1st book is included
|
|
1526
|
-
expect(formState.changedValue).toEqual({
|
|
1527
|
-
id: "a:1",
|
|
1528
|
-
books: [{ id: "b:1", title: "t1b" }],
|
|
1529
|
-
});
|
|
1530
|
-
});
|
|
1531
|
-
it("can return only changed but deep exhaustive list fields", () => {
|
|
1532
|
-
// Given an author
|
|
1533
|
-
const formState = (0, objectField_1.createObjectState)({
|
|
1534
|
-
id: { type: "value" },
|
|
1535
|
-
// And the books collection is marked as deep-exhaustive
|
|
1536
|
-
books: {
|
|
1537
|
-
type: "list",
|
|
1538
|
-
update: "deep-exhaustive",
|
|
1539
|
-
config: {
|
|
1540
|
-
id: { type: "value" },
|
|
1541
|
-
title: { type: "value", rules: [rules_1.required] },
|
|
1542
|
-
},
|
|
1543
|
-
},
|
|
1544
|
-
}, {
|
|
1545
|
-
id: "a:1",
|
|
1546
|
-
firstName: "f",
|
|
1547
|
-
books: [
|
|
1548
|
-
{ id: "b:1", title: "t1" },
|
|
1549
|
-
{ id: "b:2", title: "t2" },
|
|
1550
|
-
],
|
|
1551
|
-
});
|
|
1552
|
-
// And initially nothing is changed
|
|
1553
|
-
expect(formState.changedValue).toEqual({ id: "a:1" });
|
|
1554
|
-
// When we change the 1st book
|
|
1555
|
-
formState.books.rows[0].title.value = "t1b";
|
|
1556
|
-
// Then we see all books & all attributes
|
|
1557
|
-
expect(formState.changedValue).toEqual({
|
|
1558
|
-
id: "a:1",
|
|
1559
|
-
books: [
|
|
1560
|
-
{ id: "b:1", title: "t1b" },
|
|
1561
|
-
{ id: "b:2", title: "t2" },
|
|
1562
|
-
],
|
|
1563
|
-
});
|
|
1564
|
-
});
|
|
1565
|
-
it("uses the op key as a hint to use incremental list behavior", () => {
|
|
1566
|
-
// Given an author
|
|
1567
|
-
const formState = (0, objectField_1.createObjectState)({
|
|
1568
|
-
id: { type: "value" },
|
|
1569
|
-
// And the books collection is not explicitly marked as incremental
|
|
1570
|
-
books: {
|
|
1571
|
-
type: "list",
|
|
1572
|
-
config: {
|
|
1573
|
-
id: { type: "value" },
|
|
1574
|
-
title: { type: "value", rules: [rules_1.required] },
|
|
1575
|
-
// But it does have an op key
|
|
1576
|
-
op: { type: "value" },
|
|
1577
|
-
},
|
|
1578
|
-
},
|
|
1579
|
-
}, {
|
|
1580
|
-
id: "a:1",
|
|
1581
|
-
firstName: "f",
|
|
1582
|
-
books: [
|
|
1583
|
-
// And the books start out as included
|
|
1584
|
-
{ id: "b:1", title: "t1", op: "include" },
|
|
1585
|
-
{ id: "b:2", title: "t2", op: "include" },
|
|
1586
|
-
],
|
|
1587
|
-
});
|
|
1588
|
-
// And initially nothing is changed
|
|
1589
|
-
expect(formState.changedValue).toEqual({ id: "a:1" });
|
|
1590
|
-
// When we delete the 1st book
|
|
1591
|
-
formState.books.rows[0].op.value = "delete";
|
|
1592
|
-
// Then only the 1st book is included
|
|
1593
|
-
expect(formState.changedValue).toEqual({
|
|
1594
|
-
id: "a:1",
|
|
1595
|
-
books: [{ id: "b:1", op: "delete" }],
|
|
1596
|
-
});
|
|
1597
|
-
});
|
|
1598
|
-
it("defaults the incremental op key to included when not set", () => {
|
|
1599
|
-
// Given an author
|
|
1600
|
-
const formState = (0, objectField_1.createObjectState)({
|
|
1601
|
-
id: { type: "value" },
|
|
1602
|
-
// And the books collection is not explicitly marked as incremental
|
|
1603
|
-
books: {
|
|
1604
|
-
type: "list",
|
|
1605
|
-
config: {
|
|
1606
|
-
id: { type: "value" },
|
|
1607
|
-
title: { type: "value", rules: [rules_1.required] },
|
|
1608
|
-
// But it does have an op key
|
|
1609
|
-
op: { type: "value" },
|
|
1610
|
-
},
|
|
1611
|
-
},
|
|
1612
|
-
}, {
|
|
1613
|
-
id: "a:1",
|
|
1614
|
-
firstName: "f",
|
|
1615
|
-
books: [
|
|
1616
|
-
// And no op keys are initially set
|
|
1617
|
-
{ id: "b:1", title: "t1" },
|
|
1618
|
-
{ id: "b:2", title: "t2" },
|
|
1619
|
-
],
|
|
1620
|
-
});
|
|
1621
|
-
// And initially nothing is changed
|
|
1622
|
-
expect(formState.changedValue).toEqual({ id: "a:1" });
|
|
1623
|
-
// When we delete the 1st book
|
|
1624
|
-
formState.books.rows[0].op.value = "delete";
|
|
1625
|
-
// And add a 3rd book
|
|
1626
|
-
formState.books.add({ title: "t3" });
|
|
1627
|
-
// Then the 3rd book doesn't have an explicit op yet
|
|
1628
|
-
expect(formState.books.rows[2].op.value).toBeUndefined();
|
|
1629
|
-
// But when we create changedValue
|
|
1630
|
-
expect(formState.changedValue).toEqual({
|
|
1631
|
-
id: "a:1",
|
|
1632
|
-
books: [
|
|
1633
|
-
// Then the deleted book is included
|
|
1634
|
-
{ id: "b:1", op: "delete" },
|
|
1635
|
-
// And the 3rd book is included w/the `op: include` flag added
|
|
1636
|
-
{ title: "t3", op: "include" },
|
|
1637
|
-
],
|
|
1638
|
-
});
|
|
1639
|
-
});
|
|
1640
|
-
it("can observe value changes", () => {
|
|
1641
|
-
const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, { firstName: "f" });
|
|
1642
|
-
let ticks = 0;
|
|
1643
|
-
(0, mobx_1.reaction)(() => formState.value, () => ticks++, { equals: () => false });
|
|
1644
|
-
expect(ticks).toEqual(0);
|
|
1645
|
-
formState.firstName.value = "f";
|
|
1646
|
-
expect(ticks).toEqual(0);
|
|
1647
|
-
formState.firstName.value = "f2";
|
|
1648
|
-
expect(ticks).toEqual(1);
|
|
1649
|
-
});
|
|
1650
|
-
it("can observe value changes on lists", () => {
|
|
1651
|
-
const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, { books: [{ title: "b1" }] });
|
|
1652
|
-
let ticks = 0;
|
|
1653
|
-
(0, mobx_1.reaction)(() => formState.books.value, () => ticks++, { equals: () => false });
|
|
1654
|
-
expect(ticks).toEqual(0);
|
|
1655
|
-
formState.books.rows[0].title.value = "b1";
|
|
1656
|
-
expect(ticks).toEqual(0);
|
|
1657
|
-
formState.books.rows[0].title.value = "b1...";
|
|
1658
|
-
expect(ticks).toEqual(1);
|
|
1659
|
-
formState.books.add({ title: "b2" });
|
|
1660
|
-
expect(ticks).toEqual(2);
|
|
1661
|
-
});
|
|
1662
|
-
it("keeps WIP changes for changed field", () => {
|
|
1663
|
-
const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1664
|
-
// Given first name is focused
|
|
1665
|
-
formState.firstName.focus();
|
|
1666
|
-
// And they have wip edits
|
|
1667
|
-
formState.firstName.set("ff");
|
|
1668
|
-
// When a mutation result sets both firstName and lastName
|
|
1669
|
-
formState.set({ firstName: "f2", lastName: "l2" }, { refreshing: true });
|
|
1670
|
-
// Then we don't overwrite the user's WIP work
|
|
1671
|
-
expect(formState.firstName.value).toEqual("ff");
|
|
1672
|
-
expect(formState.lastName.value).toEqual("l2");
|
|
1673
|
-
// But the user can still actively type a value
|
|
1674
|
-
formState.firstName.set("fff");
|
|
1675
|
-
expect(formState.firstName.value).toEqual("fff");
|
|
1676
|
-
});
|
|
1677
|
-
it("accepts server result if we have no further changes", () => {
|
|
1678
|
-
const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, { id: "a:1", firstName: "f" });
|
|
1679
|
-
// Given first name has wip edits
|
|
1680
|
-
formState.firstName.set("ff");
|
|
1681
|
-
// When a mutation result sets both firstName and lastName
|
|
1682
|
-
formState.set({ firstName: "f2" }, { refreshing: true });
|
|
1683
|
-
// Then we don't overwrite the user's WIP work
|
|
1684
|
-
expect(formState.firstName.value).toEqual("ff");
|
|
1685
|
-
// But if we call changedValue, insinuating we're sending the value off to the server
|
|
1686
|
-
expect(formState.changedValue).toEqual({ id: "a:1", firstName: "ff" });
|
|
1687
|
-
// And now the server acks something different
|
|
1688
|
-
formState.set({ firstName: "FF" }, { refreshing: true });
|
|
1689
|
-
// Then we accept the value
|
|
1690
|
-
expect(formState.firstName.value).toEqual("FF");
|
|
1691
|
-
expect(formState.firstName.dirty).toEqual(false);
|
|
1692
|
-
});
|
|
1693
|
-
it("ignores cache refresh of our original value", () => {
|
|
1694
|
-
const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, { id: "a:1", firstName: "f" });
|
|
1695
|
-
// Given first name has wip edits
|
|
1696
|
-
formState.firstName.set("ff");
|
|
1697
|
-
// And we've called changedValue to put it on the wire
|
|
1698
|
-
expect(formState.changedValue).toEqual({ id: "a:1", firstName: "ff" });
|
|
1699
|
-
// But then a cache refresh fires with our original `f`
|
|
1700
|
-
formState.set({ firstName: "f" }, { refreshing: true });
|
|
1701
|
-
// Then we keep our WIP value
|
|
1702
|
-
expect(formState.firstName.value).toEqual("ff");
|
|
1703
|
-
expect(formState.firstName.dirty).toEqual(true);
|
|
1704
|
-
});
|
|
1705
|
-
it("rejects server result if we have further changes", () => {
|
|
1706
|
-
const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, { id: "a:1", firstName: "f" });
|
|
1707
|
-
// Given first name has wip edits
|
|
1708
|
-
formState.firstName.set("ff");
|
|
1709
|
-
// And we call changedValue, insinuating we're sending the value off to the server
|
|
1710
|
-
expect(formState.changedValue).toEqual({ id: "a:1", firstName: "ff" });
|
|
1711
|
-
// And the user changes the name yet again
|
|
1712
|
-
formState.firstName.set("fff");
|
|
1713
|
-
// And now the server acks something different
|
|
1714
|
-
formState.set({ firstName: "FF" }, { refreshing: true });
|
|
1715
|
-
// Then we keep the "post-changedValue" value
|
|
1716
|
-
expect(formState.firstName.value).toEqual("fff");
|
|
1717
|
-
expect(formState.firstName.dirty).toEqual(true);
|
|
1718
|
-
});
|
|
1719
|
-
it("does update a focused, unchanged field", () => {
|
|
1720
|
-
const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1721
|
-
// Given first name is focused, but has not changed
|
|
1722
|
-
formState.firstName.focus();
|
|
1723
|
-
// When a mutation result sets both firstName and lastName
|
|
1724
|
-
formState.set({ firstName: "f2", lastName: "l2" }, { refreshing: true });
|
|
1725
|
-
// Then we do update the value
|
|
1726
|
-
expect(formState.firstName.value).toEqual("f2");
|
|
1727
|
-
});
|
|
1728
|
-
it("trims string values on blur", () => {
|
|
1729
|
-
const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1730
|
-
// Given the user is typing with spaces
|
|
1731
|
-
formState.firstName.focus();
|
|
1732
|
-
formState.firstName.set("f ");
|
|
1733
|
-
// And we initially keep the space
|
|
1734
|
-
expect(formState.firstName.value).toEqual("f ");
|
|
1735
|
-
// When the field is blurred
|
|
1736
|
-
formState.firstName.blur();
|
|
1737
|
-
// Then we trim it
|
|
1738
|
-
expect(formState.firstName.value).toEqual("f");
|
|
1739
|
-
});
|
|
1740
|
-
it("trims string values on maybeAutoSave", () => {
|
|
1741
|
-
const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1742
|
-
// Given the user is typing with spaces
|
|
1743
|
-
formState.firstName.focus();
|
|
1744
|
-
formState.firstName.set("f ");
|
|
1745
|
-
// And we initially keep the space
|
|
1746
|
-
expect(formState.firstName.value).toEqual("f ");
|
|
1747
|
-
// When the user hits Enter (which will call maybeAutoSave)
|
|
1748
|
-
formState.firstName.maybeAutoSave();
|
|
1749
|
-
// Then we trim it
|
|
1750
|
-
expect(formState.firstName.value).toEqual("f");
|
|
1751
|
-
// And the field is considered touched
|
|
1752
|
-
expect(formState.firstName.touched).toEqual(true);
|
|
1753
|
-
});
|
|
1754
|
-
it("trims empty values to undefined on blur", () => {
|
|
1755
|
-
const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1756
|
-
// Given the user is typing with only spaces
|
|
1757
|
-
formState.firstName.focus();
|
|
1758
|
-
formState.firstName.set(" ");
|
|
1759
|
-
// And we initially keep the space
|
|
1760
|
-
expect(formState.firstName.value).toEqual(" ");
|
|
1761
|
-
// When the field is blurred
|
|
1762
|
-
formState.firstName.blur();
|
|
1763
|
-
// Then we trim it to null (b/c the firstName was originally set)
|
|
1764
|
-
expect(formState.firstName.value).toEqual(null);
|
|
1765
|
-
});
|
|
1766
|
-
it("reproduces the type modifier weirdness", () => {
|
|
1767
|
-
const fn = () => { };
|
|
1768
|
-
// @ts-expect-error
|
|
1769
|
-
const anyCallback = fn;
|
|
1770
|
-
});
|
|
1771
|
-
it("can work with both required inputs and optional fields", () => {
|
|
1772
|
-
// And we drop it in a form.
|
|
1773
|
-
const form = (0, objectField_1.createObjectState)({
|
|
1774
|
-
id: { type: "value" },
|
|
1775
|
-
firstName: { type: "value" },
|
|
1776
|
-
}, { id: "a:1" });
|
|
1777
|
-
// id should not accept null/undefined
|
|
1778
|
-
// @ts-expect-error
|
|
1779
|
-
form.id.set(undefined);
|
|
1780
|
-
// A BoundField typically has `string | undefined | null` so works on firstName
|
|
1781
|
-
let field1;
|
|
1782
|
-
field1 = form.firstName;
|
|
1783
|
-
// But not on id
|
|
1784
|
-
// @ts-expect-error
|
|
1785
|
-
field1 = form.id;
|
|
1786
|
-
// And same thing with any as the object type
|
|
1787
|
-
let field2;
|
|
1788
|
-
field2 = form.firstName;
|
|
1789
|
-
// @ts-expect-error
|
|
1790
|
-
field2 = form.id;
|
|
1791
|
-
});
|
|
1792
|
-
it("can have child object states passed in as field states", () => {
|
|
1793
|
-
// Given an author
|
|
1794
|
-
const a = (0, objectField_1.createObjectState)({
|
|
1795
|
-
id: { type: "value" },
|
|
1796
|
-
// And address is modeled as a value object
|
|
1797
|
-
address: { type: "value" },
|
|
1798
|
-
}, {});
|
|
1799
|
-
// And a bound field that wants a FieldState<any, Address> (even though technically `ObjectState`
|
|
1800
|
-
// turns this into a nested `ObjectState` (instead of "just a `FieldState`"), because it can't
|
|
1801
|
-
// "see" the `{ type: value }`, until we pass the config as a generic to `ObjectState`.
|
|
1802
|
-
let field;
|
|
1803
|
-
// Then we can assign the value
|
|
1804
|
-
field = a.address;
|
|
1805
|
-
// And treat it as a value object
|
|
1806
|
-
a.address.set({ street: "123", city: "nyc" });
|
|
1807
|
-
expect(a.value).toEqual({ address: { street: "123", city: "nyc" } });
|
|
1808
|
-
});
|
|
1809
|
-
it("can have child object states with cycles", () => {
|
|
1810
|
-
// Given an author with an address child
|
|
1811
|
-
// And two addresses that are basically identical and both have cycles
|
|
1812
|
-
const address1 = { city: "city2" };
|
|
1813
|
-
address1.someCycle = address1;
|
|
1814
|
-
const address2 = { city: "city2" };
|
|
1815
|
-
address2.someCycle = address2;
|
|
1816
|
-
const a = (0, objectField_1.createObjectState)({
|
|
1817
|
-
id: { type: "value" },
|
|
1818
|
-
address: { type: "value" },
|
|
1819
|
-
}, { address: address1 });
|
|
1820
|
-
// When we change the address
|
|
1821
|
-
a.address.set(address2);
|
|
1822
|
-
// Then it doesn't error on dirty checks
|
|
1823
|
-
expect(a.address.dirty).toBe(false);
|
|
1824
|
-
});
|
|
1825
|
-
it("provides isNewEntity", () => {
|
|
1826
|
-
// Given an author without an id
|
|
1827
|
-
const formState = (0, objectField_1.createObjectState)(authorWithAddressConfig, {
|
|
1828
|
-
firstName: "f",
|
|
1829
|
-
address: { city: "c1", street: "s1" },
|
|
1830
|
-
});
|
|
1831
|
-
// Then their fields know the entity is new
|
|
1832
|
-
expect(formState.firstName.isNewEntity).toBeTruthy();
|
|
1833
|
-
expect(formState.address.city.isNewEntity).toBeTruthy();
|
|
1834
|
-
// When we have an id
|
|
1835
|
-
formState.id.set("1");
|
|
1836
|
-
// Then their fields know the entity is not new
|
|
1837
|
-
expect(formState.firstName.isNewEntity).toBeFalsy();
|
|
1838
|
-
expect(formState.address.city.isNewEntity).toBeFalsy();
|
|
1839
|
-
});
|
|
1840
|
-
it("isNewEntity is false if there is no id field", () => {
|
|
1841
|
-
// Given an author without an id field defined
|
|
1842
|
-
const formState = (0, objectField_1.createObjectState)({
|
|
1843
|
-
firstName: { type: "value" },
|
|
1844
|
-
address: { type: "object", config: { street: { type: "value" } } },
|
|
1845
|
-
}, {});
|
|
1846
|
-
// Then their fields don't think the entity is new
|
|
1847
|
-
expect(formState.firstName.isNewEntity).toBeFalsy();
|
|
1848
|
-
expect(formState.address.street.isNewEntity).toBeFalsy();
|
|
1849
|
-
});
|
|
1850
|
-
it("provides isNewEntity for lists", () => {
|
|
1851
|
-
// Given an author & books without an id
|
|
1852
|
-
const formState = (0, objectField_1.createObjectState)(authorWithBooksConfig, {
|
|
1853
|
-
books: [{ title: "t1" }, { title: "t2" }],
|
|
1854
|
-
});
|
|
1855
|
-
// Then both books know they are new
|
|
1856
|
-
expect(formState.books.rows[0].title.isNewEntity).toBeTruthy();
|
|
1857
|
-
expect(formState.books.rows[1].title.isNewEntity).toBeTruthy();
|
|
1858
|
-
expect(formState.books.rows[0].isNewEntity).toBeTruthy();
|
|
1859
|
-
expect(formState.books.rows[1].isNewEntity).toBeTruthy();
|
|
1860
|
-
expect(formState.books.isNewEntity).toBeTruthy();
|
|
1861
|
-
// And when the author is not new, only it changes
|
|
1862
|
-
formState.id.set("1");
|
|
1863
|
-
expect(formState.books.rows[0].title.isNewEntity).toBeTruthy();
|
|
1864
|
-
expect(formState.books.rows[1].title.isNewEntity).toBeTruthy();
|
|
1865
|
-
expect(formState.books.rows[0].isNewEntity).toBeTruthy();
|
|
1866
|
-
expect(formState.books.rows[1].isNewEntity).toBeTruthy();
|
|
1867
|
-
expect(formState.books.isNewEntity).toBeFalsy();
|
|
1868
|
-
// And when the 1st book is not new, only it changes
|
|
1869
|
-
formState.books.rows[0].id.set("2");
|
|
1870
|
-
expect(formState.books.rows[0].title.isNewEntity).toBeFalsy();
|
|
1871
|
-
expect(formState.books.rows[1].title.isNewEntity).toBeTruthy();
|
|
1872
|
-
expect(formState.books.rows[0].isNewEntity).toBeFalsy();
|
|
1873
|
-
expect(formState.books.rows[1].isNewEntity).toBeTruthy();
|
|
1874
|
-
expect(formState.books.isNewEntity).toBeFalsy();
|
|
1875
|
-
});
|
|
1876
|
-
it("can properly evaluate dirty lists after calling 'save'", () => {
|
|
1877
|
-
// Given an empty list state
|
|
1878
|
-
const a1 = createAuthorInputState({ books: [] });
|
|
1879
|
-
expect(a1.books.dirty).toBeFalsy();
|
|
1880
|
-
// When adding a book
|
|
1881
|
-
a1.books.add({ title: "t2" });
|
|
1882
|
-
// Then the list state is dirty
|
|
1883
|
-
expect(a1.books.dirty).toBeTruthy();
|
|
1884
|
-
// When saving the form state
|
|
1885
|
-
a1.commitChanges();
|
|
1886
|
-
// Then the list is no longer dirty
|
|
1887
|
-
expect(a1.books.dirty).toBeFalsy();
|
|
1888
|
-
// When adding a new book
|
|
1889
|
-
a1.books.add({ title: "t3" });
|
|
1890
|
-
// The list is again dirty
|
|
1891
|
-
expect(a1.books.dirty).toBeTruthy();
|
|
1892
|
-
});
|
|
1893
|
-
it("can properly evaluate dirty lists when order changes", () => {
|
|
1894
|
-
// Given a list state in a specific order
|
|
1895
|
-
const a = createAuthorInputState({ books: [{ title: "t1" }, { title: "t2" }] });
|
|
1896
|
-
expect(a.books.dirty).toBeFalsy();
|
|
1897
|
-
// When changing that order
|
|
1898
|
-
const book1 = a.books.rows[0].value;
|
|
1899
|
-
a.books.remove(book1);
|
|
1900
|
-
a.books.add(book1);
|
|
1901
|
-
// Then the list state is considered dirty.
|
|
1902
|
-
expect(a.books.dirty).toBeTruthy();
|
|
1903
|
-
// When putting the books back in the original order
|
|
1904
|
-
a.books.remove(book1);
|
|
1905
|
-
a.books.add(book1, 0);
|
|
1906
|
-
// Then the list state is no longer dirty
|
|
1907
|
-
expect(a.books.dirty).toBeFalsy();
|
|
1908
|
-
});
|
|
1909
|
-
it("can optionally ignore the order of list state rows when determining dirty state", () => {
|
|
1910
|
-
// Given a list state in a specific order, and the `strictOrder` option set to `false`
|
|
1911
|
-
const a = (0, objectField_1.createObjectState)({
|
|
1912
|
-
books: {
|
|
1913
|
-
type: "list",
|
|
1914
|
-
strictOrder: false,
|
|
1915
|
-
config: {
|
|
1916
|
-
title: { type: "value", rules: [rules_1.required] },
|
|
1917
|
-
},
|
|
1918
|
-
},
|
|
1919
|
-
}, { books: [{ title: "t1" }, { title: "t2" }] });
|
|
1920
|
-
expect(a.books.dirty).toBeFalsy();
|
|
1921
|
-
// When changing that order
|
|
1922
|
-
const book1 = a.books.rows[0].value;
|
|
1923
|
-
a.books.remove(book1);
|
|
1924
|
-
a.books.add(book1);
|
|
1925
|
-
// Then the list state is still not considered dirty.
|
|
1926
|
-
expect(a.books.dirty).toBeFalsy();
|
|
1927
|
-
});
|
|
1928
|
-
describe("fragments", () => {
|
|
1929
|
-
it("ignores changes within fragments", () => {
|
|
1930
|
-
const a = (0, objectField_1.createObjectState)({ title: { type: "value" }, data: { type: "fragment" } }, { title: "b1", data: (0, objectField_1.fragment)({ foo: "1" }) });
|
|
1931
|
-
let numCalcs = 0;
|
|
1932
|
-
(0, mobx_1.autorun)(() => {
|
|
1933
|
-
numCalcs++;
|
|
1934
|
-
noop(a.value);
|
|
1935
|
-
});
|
|
1936
|
-
expect(a.data.value).toEqual({ foo: "1" });
|
|
1937
|
-
expect((0, mobx_1.isObservable)(a.data.value)).toBe(false);
|
|
1938
|
-
expect(numCalcs).toEqual(1);
|
|
1939
|
-
a.data.value = { foo: "2" };
|
|
1940
|
-
expect(numCalcs).toEqual(2);
|
|
1941
|
-
a.data.value.foo = "3";
|
|
1942
|
-
expect(numCalcs).toEqual(2);
|
|
1943
|
-
});
|
|
1944
|
-
it("ignores fragments in changedValue", () => {
|
|
1945
|
-
const a = (0, objectField_1.createObjectState)({ title: { type: "value" }, data: { type: "fragment" } }, { title: "b1", data: (0, objectField_1.fragment)({ foo: "1" }) });
|
|
1946
|
-
a.title.value = "b2";
|
|
1947
|
-
a.data.value = (0, objectField_1.fragment)({ foo: "2" });
|
|
1948
|
-
expect(a.changedValue).toMatchInlineSnapshot(`
|
|
1949
|
-
{
|
|
1950
|
-
"title": "b2",
|
|
1951
|
-
}
|
|
1952
|
-
`);
|
|
1953
|
-
});
|
|
1954
|
-
it("ignores fragments in value", () => {
|
|
1955
|
-
const a = (0, objectField_1.createObjectState)({ title: { type: "value" }, data: { type: "fragment" } }, { title: "b1", data: (0, objectField_1.fragment)({ foo: "1" }) });
|
|
1956
|
-
expect(a.value).toMatchInlineSnapshot(`
|
|
1957
|
-
{
|
|
1958
|
-
"title": "b1",
|
|
1959
|
-
}
|
|
1960
|
-
`);
|
|
1961
|
-
});
|
|
1962
|
-
it("accepts new values in set", () => {
|
|
1963
|
-
const a = (0, objectField_1.createObjectState)({ title: { type: "value" }, data: { type: "fragment" } }, { title: "b1", data: (0, objectField_1.fragment)({ foo: "1" }) });
|
|
1964
|
-
let numCalcs = 0;
|
|
1965
|
-
(0, mobx_1.autorun)(() => {
|
|
1966
|
-
numCalcs++;
|
|
1967
|
-
noop(a.value);
|
|
1968
|
-
});
|
|
1969
|
-
a.set({ data: (0, objectField_1.fragment)({ foo: "2" }) });
|
|
1970
|
-
expect(numCalcs).toBe(2);
|
|
1971
|
-
});
|
|
1972
|
-
it("are always considered valid", () => {
|
|
1973
|
-
const a = (0, objectField_1.createObjectState)({ data: { type: "fragment" } }, { data: (0, objectField_1.fragment)({ foo: "1" }) });
|
|
1974
|
-
expect(a.valid).toBe(true);
|
|
1975
|
-
});
|
|
1976
|
-
});
|
|
1977
|
-
it("sets id key correctly", () => {
|
|
1978
|
-
// Given an author
|
|
1979
|
-
const a = (0, objectField_1.createObjectState)({
|
|
1980
|
-
id: { type: "value" },
|
|
1981
|
-
otherId: { type: "value", isIdKey: true },
|
|
1982
|
-
}, {});
|
|
1983
|
-
expect(a.id._isIdKey).toBe(false);
|
|
1984
|
-
expect(a.otherId._isIdKey).toBe(true);
|
|
1985
|
-
});
|
|
1986
|
-
it("uses id key to recognize same entities", () => {
|
|
1987
|
-
// With b1 having an id
|
|
1988
|
-
const b1 = (0, objectField_1.createObjectState)({ id: { type: "value" } }, { id: "b:1" });
|
|
1989
|
-
expect(b1.isSameEntity({ id: "b:1" })).toBe(true);
|
|
1990
|
-
expect(b1.isSameEntity({ id: "b:2" })).toBe(false);
|
|
1991
|
-
expect(b1.isSameEntity({ id: undefined })).toBe(false);
|
|
1992
|
-
expect(b1.isSameEntity({})).toBe(false);
|
|
1993
|
-
// With b2 having no id
|
|
1994
|
-
const b2 = (0, objectField_1.createObjectState)({ id: { type: "value" } }, {});
|
|
1995
|
-
expect(b2.isSameEntity({ id: "b:1" })).toBe(false);
|
|
1996
|
-
expect(b2.isSameEntity({ id: undefined })).toBe(false);
|
|
1997
|
-
expect(b2.isSameEntity({})).toBe(false);
|
|
1998
|
-
// With b3 having no id key
|
|
1999
|
-
const b3 = (0, objectField_1.createObjectState)({ title: { type: "value" } }, { title: "b3" });
|
|
2000
|
-
expect(b3.isSameEntity({ title: "b3" })).toBe(false);
|
|
2001
|
-
// With b4 having a different id key
|
|
2002
|
-
const b4 = (0, objectField_1.createObjectState)({ title: { type: "value", isIdKey: true } }, { title: "b4" });
|
|
2003
|
-
expect(b4.isSameEntity({ title: "b4" })).toBe(true);
|
|
2004
|
-
});
|
|
2005
|
-
});
|
|
2006
|
-
class ObservableObject {
|
|
2007
|
-
constructor() {
|
|
2008
|
-
this.firstName = "first";
|
|
2009
|
-
this.lastName = "last";
|
|
2010
|
-
this.age = undefined;
|
|
2011
|
-
(0, mobx_1.makeAutoObservable)(this);
|
|
2012
|
-
}
|
|
2013
|
-
get fullName() {
|
|
2014
|
-
return `${this.firstName} ${this.lastName}`;
|
|
2015
|
-
}
|
|
2016
|
-
set fullName(fullName) {
|
|
2017
|
-
const parts = fullName.split(" ");
|
|
2018
|
-
this.firstName = parts[0];
|
|
2019
|
-
this.lastName = parts[1];
|
|
2020
|
-
}
|
|
2021
|
-
toInput() {
|
|
2022
|
-
return { firstName: this.firstName };
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
exports.ObservableObject = ObservableObject;
|
|
2026
|
-
const authorWithBooksConfig = configBuilders_1.f.config({
|
|
2027
|
-
id: configBuilders_1.f.value(),
|
|
2028
|
-
firstName: configBuilders_1.f.value(),
|
|
2029
|
-
lastName: configBuilders_1.f.value(),
|
|
2030
|
-
books: configBuilders_1.f.list({
|
|
2031
|
-
id: configBuilders_1.f.value(),
|
|
2032
|
-
title: configBuilders_1.f.value().req(),
|
|
2033
|
-
classification: configBuilders_1.f.value(),
|
|
2034
|
-
isPublished: configBuilders_1.f.value(),
|
|
2035
|
-
}),
|
|
2036
|
-
});
|
|
2037
|
-
function createAuthorInputState(input, maybeAutoSave) {
|
|
2038
|
-
return (0, objectField_1.createObjectState)(authorWithBooksConfig, input, { maybeAutoSave });
|
|
2039
|
-
}
|
|
2040
|
-
const authorWithFullName = {
|
|
2041
|
-
firstName: { type: "value" },
|
|
2042
|
-
lastName: { type: "value" },
|
|
2043
|
-
fullName: { type: "value", computed: true },
|
|
2044
|
-
};
|
|
2045
|
-
const authorWithAddressConfig = {
|
|
2046
|
-
id: { type: "value" },
|
|
2047
|
-
firstName: { type: "value" },
|
|
2048
|
-
lastName: { type: "value" },
|
|
2049
|
-
address: {
|
|
2050
|
-
type: "object",
|
|
2051
|
-
config: {
|
|
2052
|
-
street: { type: "value", rules: [rules_1.required] },
|
|
2053
|
-
city: { type: "value" },
|
|
2054
|
-
},
|
|
2055
|
-
},
|
|
2056
|
-
};
|
|
2057
|
-
const authorWithAddressFkConfig = {
|
|
2058
|
-
id: { type: "value" },
|
|
2059
|
-
firstName: { type: "value" },
|
|
2060
|
-
lastName: { type: "value" },
|
|
2061
|
-
address: {
|
|
2062
|
-
type: "object",
|
|
2063
|
-
config: {
|
|
2064
|
-
id: { type: "value" },
|
|
2065
|
-
street: { type: "value", rules: [rules_1.required] },
|
|
2066
|
-
city: { type: "value" },
|
|
2067
|
-
},
|
|
2068
|
-
},
|
|
2069
|
-
};
|
|
2070
|
-
const authorWithAddressAndBooksConfig = configBuilders_1.f.config({
|
|
2071
|
-
id: configBuilders_1.f.value(),
|
|
2072
|
-
firstName: configBuilders_1.f.value(),
|
|
2073
|
-
lastName: configBuilders_1.f.value(),
|
|
2074
|
-
address: configBuilders_1.f.object({
|
|
2075
|
-
id: configBuilders_1.f.value(),
|
|
2076
|
-
street: configBuilders_1.f.value().rules([rules_1.required]),
|
|
2077
|
-
city: configBuilders_1.f.value(),
|
|
2078
|
-
}),
|
|
2079
|
-
books: configBuilders_1.f.list({
|
|
2080
|
-
id: configBuilders_1.f.value(),
|
|
2081
|
-
title: configBuilders_1.f.value(),
|
|
2082
|
-
classification: configBuilders_1.f.value(),
|
|
2083
|
-
}),
|
|
2084
|
-
});
|
|
2085
|
-
// const authorWithAddressAndBooksConfig: ObjectConfig<AuthorInput> = {
|
|
2086
|
-
// id: { type: "value" },
|
|
2087
|
-
// firstName: { type: "value" },
|
|
2088
|
-
// lastName: { type: "value" },
|
|
2089
|
-
// address: {
|
|
2090
|
-
// type: "object",
|
|
2091
|
-
// config: {
|
|
2092
|
-
// id: { type: "value" },
|
|
2093
|
-
// street: { type: "value", rules: [required] },
|
|
2094
|
-
// city: { type: "value" },
|
|
2095
|
-
// },
|
|
2096
|
-
// },
|
|
2097
|
-
// books: {
|
|
2098
|
-
// type: "list",
|
|
2099
|
-
// config: {
|
|
2100
|
-
// id: { type: "value" },
|
|
2101
|
-
// title: { type: "value", rules: [required] },
|
|
2102
|
-
// classification: { type: "value" },
|
|
2103
|
-
// },
|
|
2104
|
-
// },
|
|
2105
|
-
// };
|
|
2106
|
-
function createAuthorWithAddressInputState(input) {
|
|
2107
|
-
return (0, objectField_1.createObjectState)(authorWithAddressConfig, input);
|
|
2108
|
-
}
|
|
2109
|
-
function noop(t) { }
|