@homebound/form-state 2.33.0 → 3.0.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.
Files changed (57) hide show
  1. package/dist/index.d.ts +437 -9
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +1109 -14
  4. package/dist/index.js.map +1 -0
  5. package/package.json +32 -35
  6. package/dist/FormStateApp.d.ts +0 -5
  7. package/dist/FormStateApp.js +0 -54
  8. package/dist/FormStateApp.test.d.ts +0 -1
  9. package/dist/FormStateApp.test.js +0 -26
  10. package/dist/config.d.ts +0 -78
  11. package/dist/config.js +0 -2
  12. package/dist/configBuilders.d.ts +0 -67
  13. package/dist/configBuilders.js +0 -115
  14. package/dist/configBuilders.test.d.ts +0 -1
  15. package/dist/configBuilders.test.js +0 -65
  16. package/dist/fields/deepEquals.d.ts +0 -7
  17. package/dist/fields/deepEquals.js +0 -53
  18. package/dist/fields/fragmentField.d.ts +0 -4
  19. package/dist/fields/fragmentField.js +0 -58
  20. package/dist/fields/listField.d.ts +0 -11
  21. package/dist/fields/listField.js +0 -355
  22. package/dist/fields/objectField.d.ts +0 -63
  23. package/dist/fields/objectField.js +0 -277
  24. package/dist/fields/valueField.d.ts +0 -81
  25. package/dist/fields/valueField.js +0 -244
  26. package/dist/formState.stories.d.ts +0 -4
  27. package/dist/formState.stories.js +0 -12
  28. package/dist/formState.test.d.ts +0 -11
  29. package/dist/formState.test.js +0 -2109
  30. package/dist/formStateDomain.d.ts +0 -47
  31. package/dist/formStateDomain.js +0 -28
  32. package/dist/mobx-behavior.test.d.ts +0 -1
  33. package/dist/mobx-behavior.test.js +0 -51
  34. package/dist/proxies.d.ts +0 -5
  35. package/dist/proxies.js +0 -20
  36. package/dist/rules.d.ts +0 -10
  37. package/dist/rules.js +0 -11
  38. package/dist/setupTests.d.ts +0 -1
  39. package/dist/setupTests.js +0 -15
  40. package/dist/stackoverlfow.d.ts +0 -17
  41. package/dist/stackoverlfow.js +0 -17
  42. package/dist/temporal.d.ts +0 -6
  43. package/dist/temporal.js +0 -34
  44. package/dist/temporal.test.d.ts +0 -1
  45. package/dist/temporal.test.js +0 -97
  46. package/dist/useFormState.d.ts +0 -82
  47. package/dist/useFormState.js +0 -158
  48. package/dist/useFormState.test.d.ts +0 -1
  49. package/dist/useFormState.test.js +0 -787
  50. package/dist/useFormStates.d.ts +0 -68
  51. package/dist/useFormStates.js +0 -101
  52. package/dist/useFormStates.test.d.ts +0 -1
  53. package/dist/useFormStates.test.js +0 -223
  54. package/dist/utils.d.ts +0 -52
  55. package/dist/utils.js +0 -198
  56. package/dist/utils.test.d.ts +0 -1
  57. package/dist/utils.test.js +0 -102
@@ -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) { }