@homebound/form-state 2.32.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 +40 -43
  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 -13
  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,787 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const jsx_runtime_1 = require("react/jsx-runtime");
4
- const rtl_utils_1 = require("@homebound/rtl-utils");
5
- const react_1 = require("@testing-library/react");
6
- const mobx_1 = require("mobx");
7
- const mobx_react_1 = require("mobx-react");
8
- const react_2 = require("react");
9
- const FormStateApp_1 = require("./FormStateApp");
10
- const objectField_1 = require("./fields/objectField");
11
- const rules_1 = require("./rules");
12
- const useFormState_1 = require("./useFormState");
13
- describe("useFormState", () => {
14
- it("calls init.map if init.input is defined", async () => {
15
- // Given a component
16
- function TestComponent() {
17
- const config = { firstName: { type: "value" } };
18
- // And we have query data that may or may not be defined
19
- const data = { firstName: "bob" };
20
- const form = (0, useFormState_1.useFormState)({
21
- config,
22
- // Then the lambda is passed the "de-undefined" data, i.e. `d.firstName` is not a compile error
23
- init: { input: data, map: (d) => ({ firstName: d.firstName }) },
24
- });
25
- return (0, jsx_runtime_1.jsx)("div", { children: form.firstName.value });
26
- }
27
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
28
- expect(r.baseElement).toHaveTextContent("bob");
29
- });
30
- it("memoizes on init.input being an array", async () => {
31
- const config = { firstName: { type: "value" } };
32
- function TestComponent() {
33
- const [, setTick] = (0, react_2.useState)(0);
34
- const form = (0, useFormState_1.useFormState)({
35
- config,
36
- init: { input: ["a", "b"], map: ([a, b]) => ({ firstName: a + b }) },
37
- });
38
- const onClick = () => [form.firstName.set("fred"), setTick(1)];
39
- return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("button", { "data-testid": "change", onClick: onClick }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: form.firstName.value })] }));
40
- }
41
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
42
- expect(r.firstName).toHaveTextContent("ab");
43
- (0, rtl_utils_1.click)(r.change);
44
- expect(r.firstName).toHaveTextContent("fred");
45
- });
46
- it("uses default if init.input is undefined", async () => {
47
- // Given a component
48
- function TestComponent() {
49
- const config = { firstName: { type: "value" } };
50
- // And we have query data that may or may not be defined (but is actually undefined)
51
- const data = Math.random() >= 0 ? undefined : { firstName: "bob" };
52
- const form = (0, useFormState_1.useFormState)({
53
- config,
54
- init: { input: data, map: (d) => ({ firstName: d.firstName }) },
55
- });
56
- return (0, jsx_runtime_1.jsx)("div", { children: form.firstName.value });
57
- }
58
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
59
- // Then we init.map wasn't called, and we used {} instead
60
- expect(r.baseElement.textContent).toEqual("");
61
- });
62
- it("uses custom init.ifUndefined if init.input is undefined", async () => {
63
- // Given a component
64
- function TestComponent() {
65
- const config = {
66
- id: { type: "value" },
67
- firstName: { type: "value" },
68
- };
69
- // And we have query data that may or may not be defined (but is actually undefined)
70
- const data = Math.random() >= 0 ? undefined : { firstName: "bob" };
71
- const form = (0, useFormState_1.useFormState)({
72
- config,
73
- // And we pass `ifUndefined`
74
- init: {
75
- input: data,
76
- map: (d) => ({ firstName: d.firstName }),
77
- ifUndefined: { firstName: "default" },
78
- },
79
- });
80
- return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: form.firstName.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "changedValue", children: JSON.stringify(form.changedValue) })] }));
81
- }
82
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
83
- // Then we use the ifUndefined value
84
- expect(r.firstName.textContent).toEqual("default");
85
- expect(r.changedValue.textContent).toEqual(JSON.stringify({ firstName: "default" }));
86
- });
87
- it("doesn't required an init value", async () => {
88
- function TestComponent() {
89
- const config = { firstName: { type: "value" } };
90
- const form = (0, useFormState_1.useFormState)({ config });
91
- return (0, jsx_runtime_1.jsx)("div", { children: form.firstName.value });
92
- }
93
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
94
- expect(r.baseElement.textContent).toEqual("");
95
- });
96
- it("uses init.onlyOnce to not react to identity changes", async () => {
97
- const config = { firstName: { type: "value" } };
98
- const TestComponent = (0, mobx_react_1.observer)(() => {
99
- // And we have an `init` value that is dynamic, so we can observe whether init reruns
100
- const renderCount = (0, react_2.useRef)(0).current++;
101
- const form = (0, useFormState_1.useFormState)({
102
- config,
103
- init: { input: { firstName: `${renderCount}` }, onlyOnce: true },
104
- });
105
- const [, setTick] = (0, react_2.useState)(0);
106
- return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("button", { "data-testid": "change", onClick: () => setTick(1) }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: form.firstName.value })] }));
107
- });
108
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
109
- expect(r.firstName).toHaveTextContent("0");
110
- (0, rtl_utils_1.click)(r.change);
111
- expect(r.firstName).toHaveTextContent("0");
112
- });
113
- it("can init with fragments", async () => {
114
- // Given a component
115
- function TestComponent() {
116
- var _a, _b, _c, _d;
117
- // And we have query data that may or may not be defined
118
- const form = (0, useFormState_1.useFormState)({
119
- config: authorWithBookFragments,
120
- init: {
121
- input: {},
122
- map: () => ({ firstName: "a1", books: [{ id: "b:1", data: (0, objectField_1.fragment)({ misc: "data" }) }] }),
123
- },
124
- });
125
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "fieldValue", children: form.books.rows[0].data.value.misc }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "rowValue", children: (_b = (_a = form.books.rows[0].value.data) === null || _a === void 0 ? void 0 : _a.misc) !== null && _b !== void 0 ? _b : "unset" }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "listValue", children: (_d = (_c = form.books.value[0].data) === null || _c === void 0 ? void 0 : _c.misc) !== null && _d !== void 0 ? _d : "unset" })] }));
126
- }
127
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
128
- expect(r.fieldValue).toHaveTextContent("data");
129
- expect(r.rowValue).toHaveTextContent("unset");
130
- expect(r.listValue).toHaveTextContent("unset");
131
- });
132
- it("keeps local changed values when a query refreshes", async () => {
133
- // Given a component
134
- function TestComponent() {
135
- const config = authorWithAddressAndBooksConfig;
136
- // And we have two sets of data
137
- const data1 = {
138
- id: "a:1",
139
- firstName: "f1",
140
- lastName: "l1",
141
- address: { id: "address:1", street: "s1", city: "c1" },
142
- books: [
143
- { id: "b:1", title: "a1" },
144
- { id: "b:2", title: "b1" },
145
- ],
146
- };
147
- const data2 = {
148
- id: "a:1",
149
- firstName: "f2", // we'll have a local WIP change
150
- lastName: "l2",
151
- address: {
152
- id: "address:1",
153
- street: "s2", // we'll have a local WIP change
154
- city: "c2",
155
- },
156
- books: [
157
- {
158
- id: "b:1",
159
- title: "a2", // we'll have a local WIP change
160
- },
161
- { id: "b:2", title: "b2" },
162
- { id: "b:3", title: "b3" },
163
- ],
164
- };
165
- // Eventually our local values are saved
166
- const data3 = {
167
- id: "a:1",
168
- firstName: "local",
169
- lastName: "l2",
170
- address: { id: "address:1", street: "local", city: "c2" },
171
- books: [
172
- { id: "b:1", title: "local" },
173
- { id: "b:2", title: "b2" },
174
- { id: "b:3", title: "b3" },
175
- ],
176
- };
177
- // And we start out with data1
178
- const [data, setData] = (0, react_2.useState)(data1);
179
- const form = (0, useFormState_1.useFormState)({ config, init: { input: data } });
180
- function makeLocalChanges() {
181
- form.firstName.value = "local";
182
- form.address.street.value = "local";
183
- form.books.rows[0].title.value = "local";
184
- }
185
- return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: form.firstName.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "lastName", children: form.lastName.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "street", children: form.address.street.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "city", children: form.address.city.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "title1", children: form.books.rows[0].title.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "title2", children: form.books.rows[1].title.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "booksLength", children: form.books.rows.length }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "makeLocalChanges", onClick: makeLocalChanges }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "refreshData", onClick: () => setData(data2) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "saveData", onClick: () => setData(data3) }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "dirty", children: String(form.dirty) })] })) }));
186
- }
187
- // And we start out with the initial query data
188
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
189
- expect(r.firstName.textContent).toEqual("f1");
190
- expect(r.street.textContent).toEqual("s1");
191
- expect(r.title1.textContent).toEqual("a1");
192
- // When we make some local changes
193
- (0, rtl_utils_1.click)(r.makeLocalChanges);
194
- // Then we see them
195
- expect(r.firstName.textContent).toEqual("local");
196
- expect(r.street.textContent).toEqual("local");
197
- expect(r.title1.textContent).toEqual("local");
198
- expect(r.dirty.textContent).toEqual("true");
199
- // And when the new query is ran i.e. due to a cache refresh
200
- (0, rtl_utils_1.click)(r.refreshData);
201
- // Then we kept our local changes
202
- expect(r.firstName.textContent).toEqual("local");
203
- expect(r.street.textContent).toEqual("local");
204
- expect(r.title1.textContent).toEqual("local");
205
- // But we also see the new data for fields we have not changed
206
- expect(r.lastName.textContent).toEqual("l2");
207
- expect(r.city.textContent).toEqual("c2");
208
- expect(r.title2.textContent).toEqual("b2");
209
- expect(r.booksLength.textContent).toEqual("3");
210
- expect(r.dirty.textContent).toEqual("true");
211
- // And then when our mutation results come back
212
- (0, rtl_utils_1.click)(r.saveData);
213
- // And we're not dirty anymore
214
- expect(r.dirty.textContent).toEqual("false");
215
- });
216
- it("accepts server values if client has no further changes", async () => {
217
- function TestComponent() {
218
- const config = authorWithAddressAndBooksConfig;
219
- // The initial resposne
220
- const data1 = { id: "a:1", firstName: "F1" };
221
- // The acked response, which uppercases our submission
222
- const data2 = { id: "a:1", firstName: "F2" };
223
- // And we start out with data1
224
- const [data, setData] = (0, react_2.useState)(data1);
225
- const form = (0, useFormState_1.useFormState)({ config, init: { input: data } });
226
- function makeLocalChanges() {
227
- form.firstName.value = "f2";
228
- }
229
- return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: form.firstName.value }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "makeLocalChanges", onClick: makeLocalChanges }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "refreshData", onClick: () => {
230
- noop(form.changedValue); // pretend we put this on the wire
231
- setData(data2);
232
- } })] })) }));
233
- }
234
- // And we start out with the initial query data
235
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
236
- expect(r.firstName.textContent).toEqual("F1");
237
- // When we make some local changes
238
- (0, rtl_utils_1.click)(r.makeLocalChanges);
239
- // Then we see them
240
- expect(r.firstName.textContent).toEqual("f2");
241
- // And when the server acks a slightly different result
242
- (0, rtl_utils_1.click)(r.refreshData);
243
- // Then we accept it
244
- expect(r.firstName.textContent).toEqual("F2");
245
- });
246
- it("can immediately update from a server ack before init input rerenders", async () => {
247
- const config = {
248
- firstName: { type: "value" },
249
- lastName: { type: "value" },
250
- };
251
- const initialAuthor = { legalFirstName: "First", legalLastName: "Last" };
252
- const savedAuthor = { legalFirstName: "Saved", legalLastName: "Author" };
253
- function TestComponent() {
254
- const form = (0, useFormState_1.useFormState)({
255
- config,
256
- init: { input: initialAuthor, map: mapServerAuthor },
257
- });
258
- function makeLocalChanges() {
259
- form.firstName.set("Saved");
260
- form.lastName.set("Author");
261
- }
262
- function saveWithoutUpdating() {
263
- noop(form.changedValue);
264
- }
265
- function saveAndUpdate() {
266
- noop(form.changedValue);
267
- form.update(savedAuthor);
268
- }
269
- return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: form.firstName.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "lastName", children: form.lastName.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "dirty", children: String(form.dirty) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "makeLocalChanges", onClick: makeLocalChanges }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "saveWithoutUpdating", onClick: saveWithoutUpdating }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "saveAndUpdate", onClick: saveAndUpdate })] })) }));
270
- }
271
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
272
- expect(r.firstName.textContent).toEqual("First");
273
- expect(r.lastName.textContent).toEqual("Last");
274
- expect(r.dirty.textContent).toEqual("false");
275
- (0, rtl_utils_1.click)(r.makeLocalChanges);
276
- expect(r.firstName.textContent).toEqual("Saved");
277
- expect(r.lastName.textContent).toEqual("Author");
278
- expect(r.dirty.textContent).toEqual("true");
279
- (0, rtl_utils_1.click)(r.saveWithoutUpdating);
280
- expect(r.dirty.textContent).toEqual("true");
281
- (0, rtl_utils_1.click)(r.saveAndUpdate);
282
- expect(r.firstName.textContent).toEqual("Saved");
283
- expect(r.lastName.textContent).toEqual("Author");
284
- expect(r.dirty.textContent).toEqual("false");
285
- function mapServerAuthor(input) {
286
- return { firstName: input.legalFirstName, lastName: input.legalLastName };
287
- }
288
- });
289
- it("becomes clean when update acks an array value with the same contents", async () => {
290
- const config = { colors: { type: "value" } };
291
- function TestComponent() {
292
- const form = (0, useFormState_1.useFormState)({ config, init: { input: { colors: ["red", "green", "blue"] } } });
293
- // Note: we deliberately do not read `form.changedValue` (the way an auto-save would), so the only
294
- // thing that can let `update` accept the server value is content equality, not the changedValue hint.
295
- return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => {
296
- var _a;
297
- return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "colors", children: ((_a = form.colors.value) !== null && _a !== void 0 ? _a : []).join(",") }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "dirty", children: String(form.dirty) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "edit", onClick: () => form.colors.set(["red", "green"]) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "saveAndUpdate", onClick: () => form.update({ colors: ["red", "green"] }) })] }));
298
- } }));
299
- }
300
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
301
- expect(r.dirty.textContent).toEqual("false");
302
- // When the user edits the array
303
- (0, rtl_utils_1.click)(r.edit);
304
- expect(r.dirty.textContent).toEqual("true");
305
- // And the server acks the same contents in a brand-new array instance
306
- (0, rtl_utils_1.click)(r.saveAndUpdate);
307
- // Then the form is clean (we don't get stuck dirty on array reference inequality)
308
- expect(r.colors.textContent).toEqual("red,green");
309
- expect(r.dirty.textContent).toEqual("false");
310
- });
311
- it("can accept new data while read only", async () => {
312
- // Given a component
313
- function TestComponent() {
314
- const config = authorWithAddressAndBooksConfig;
315
- // And we have two sets of data
316
- const data1 = {
317
- firstName: "f1",
318
- address: { street: "s1" },
319
- books: [{ title: "a1" }],
320
- };
321
- const data2 = {
322
- firstName: "f2",
323
- address: { street: "s2" },
324
- books: [{ title: "a2" }],
325
- };
326
- // And we start out with data1
327
- const [data, setData] = (0, react_2.useState)(data1);
328
- const form = (0, useFormState_1.useFormState)({
329
- config,
330
- init: { input: data },
331
- // And the form is read only
332
- readOnly: true,
333
- });
334
- return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: form.firstName.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "street", children: form.address.street.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "title1", children: form.books.rows[0].title.value }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "refreshData", onClick: () => setData(data2) })] })) }));
335
- }
336
- // And we start out with the initial query data
337
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
338
- expect(r.firstName.textContent).toEqual("f1");
339
- expect(r.street.textContent).toEqual("s1");
340
- expect(r.title1.textContent).toEqual("a1");
341
- // When the new query is ran i.e. due to a cache refresh
342
- (0, rtl_utils_1.click)(r.refreshData);
343
- // Then we see the latest data
344
- expect(r.firstName.textContent).toEqual("f2");
345
- expect(r.street.textContent).toEqual("s2");
346
- expect(r.title1.textContent).toEqual("a2");
347
- });
348
- it("can accept new data with computed fields", async () => {
349
- // Given a component
350
- // And it's using a class/mobx proxy as the basis for the data
351
- class AuthorRow {
352
- constructor(firstName, lastName, books) {
353
- this.firstName = firstName;
354
- this.lastName = lastName;
355
- this.books = books;
356
- (0, mobx_1.makeAutoObservable)(this);
357
- }
358
- get fullName() {
359
- return this.firstName + " " + this.lastName;
360
- }
361
- }
362
- function TestComponent() {
363
- // And we have two sets of data
364
- const data1 = { firstName: "f1", lastName: "l1", books: [] };
365
- const data2 = { firstName: "f2", lastName: "l2", books: [{ title: "t1" }] };
366
- // And we start out with data1
367
- const [data, setData] = (0, react_2.useState)(data1);
368
- const author = (0, react_2.useMemo)(() => new AuthorRow(data.firstName, data.lastName, data.books), [data]);
369
- const config = (0, react_2.useMemo)(() => ({
370
- firstName: { type: "value" },
371
- lastName: { type: "value" },
372
- fullName: { type: "value", computed: true },
373
- books: { type: "list", config: { title: { type: "value" } } },
374
- }), []);
375
- const form = (0, useFormState_1.useFormState)({ config, init: { input: author, map: (a) => a } });
376
- return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: form.firstName.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "fullName", children: form.fullName.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "booksLength", children: form.books.rows.length }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "booksDirty", children: String(form.books.touched) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "refreshData", onClick: () => setData(data2) })] })) }));
377
- }
378
- // And we start out with the initial query data
379
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
380
- expect(r.firstName.textContent).toEqual("f1");
381
- expect(r.fullName.textContent).toEqual("f1 l1");
382
- expect(r.booksLength.textContent).toEqual("0");
383
- expect(r.booksDirty.textContent).toEqual("false");
384
- // When the new query is ran i.e. due to a cache refresh
385
- (0, rtl_utils_1.click)(r.refreshData);
386
- expect(r.firstName.textContent).toEqual("f2");
387
- expect(r.fullName.textContent).toEqual("f2 l2");
388
- expect(r.booksLength.textContent).toEqual("1");
389
- expect(r.booksDirty.textContent).toEqual("false");
390
- });
391
- it("can trigger auto-save when underlying observable is changed", async () => {
392
- // Given a component
393
- // And it's using a class/mobx proxy as the basis for the data
394
- class AuthorRow {
395
- constructor(firstName) {
396
- this.firstName = firstName;
397
- (0, mobx_1.makeAutoObservable)(this);
398
- }
399
- }
400
- const autoSave = jest.fn();
401
- function TestComponent() {
402
- // And the author starts out with f1
403
- const author = (0, react_2.useMemo)(() => new AuthorRow("f1"), []);
404
- const config = (0, react_2.useMemo)(() => ({ firstName: { type: "value" } }), []);
405
- const form = (0, useFormState_1.useFormState)({ config, init: { input: author, map: (a) => a }, autoSave });
406
- return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("button", { "data-testid": "changeFirstName", onClick: () => (author.firstName = "f2") }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "clearFirstName", onClick: () => (author.firstName = undefined) })] }));
407
- }
408
- // When we change the underlying observable
409
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
410
- expect(autoSave).toBeCalledTimes(0);
411
- await (0, rtl_utils_1.clickAndWait)(r.changeFirstName);
412
- // Then auto save is triggered
413
- expect(autoSave).toBeCalledTimes(1);
414
- await (0, rtl_utils_1.clickAndWait)(r.clearFirstName);
415
- expect(autoSave).toBeCalledTimes(2);
416
- });
417
- it("can trigger auto save for fields in list that were initially undefined", async () => {
418
- const autoSave = jest.fn();
419
- // Given a component
420
- function TestComponent() {
421
- // When the data is initially undefined
422
- const [data, setData] = (0, react_2.useState)();
423
- const data2 = { books: [{ title: "Title 1" }] };
424
- const form = (0, useFormState_1.useFormState)({
425
- config: authorWithBooksConfig,
426
- init: { input: data, ifUndefined: { books: [] } },
427
- autoSave,
428
- });
429
- return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("button", { "data-testid": "refreshData", onClick: () => setData(data2) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "add", onClick: () => form.books.add({ title: "New Book" }) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "blurBookOne", onClick: () => focusAndBlur(form.books.rows[0].title) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "blurBookTwo", onClick: () => focusAndBlur(form.books.rows[1].title) })] })) }));
430
- }
431
- // Given a formState with `onBlur` set
432
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
433
- // When the data/child is now available
434
- (0, rtl_utils_1.click)(r.refreshData);
435
- // And the field is blurred
436
- await (0, rtl_utils_1.clickAndWait)(r.blurBookOne);
437
- // Then we don't auto-save because nothing has changed
438
- expect(autoSave).toBeCalledTimes(0);
439
- // And when adding a new book
440
- await (0, rtl_utils_1.clickAndWait)(r.add);
441
- // We autoSave the new row right away (because we don't have any validation rules
442
- // that say the new row can't be empty)
443
- expect(autoSave).toBeCalledTimes(1);
444
- // And the new book is blurred
445
- await (0, rtl_utils_1.clickAndWait)(r.blurBookTwo);
446
- // Then we auto save again
447
- expect(autoSave).toBeCalledTimes(2);
448
- });
449
- it("can revert to last-saved-state after a failed auto save", async () => {
450
- // Given a component
451
- function TestComponent() {
452
- // And the firstName is initially "f1"
453
- const [data, setData] = (0, react_2.useState)({ firstName: "f1" });
454
- const query = (0, react_2.useMemo)(() => {
455
- return { data, loading: false, error: undefined };
456
- }, [data]);
457
- const form = (0, useFormState_1.useFormState)({
458
- config: authorConfig,
459
- init: { query, map: (d) => d },
460
- async autoSave(fs) {
461
- // If the user types "f2", we'll pretend the backend accepted the update, and acks it in the mutation response
462
- if (fs.firstName.value === "f2") {
463
- await new Promise((resolve) => setTimeout(resolve, 1));
464
- setData(fs.value);
465
- }
466
- else {
467
- // If the user types anything else, pretend the backend rejected it, and revert
468
- await new Promise((resolve) => setTimeout(resolve, 1));
469
- fs.revertChanges();
470
- }
471
- },
472
- });
473
- return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "firstNameOriginal", children: form.firstName.originalValue }), (0, jsx_runtime_1.jsx)(FormStateApp_1.TextField, { field: form.firstName }), ";"] })) }));
474
- }
475
- // So we start out with f1
476
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
477
- expect(r.firstName).toHaveValue("f1");
478
- // When we type f2, it should be accepted & stay
479
- await (0, rtl_utils_1.typeAndWait)(r.firstName, "f2");
480
- expect(r.firstName).toHaveValue("f2");
481
- expect(r.firstNameOriginal).toHaveTextContent("f2");
482
- // But if we type f3, we want it reverted, back to f2 (the last good value)
483
- await (0, rtl_utils_1.typeAndWait)(r.firstName, "f3");
484
- expect(r.firstName).toHaveValue("f2");
485
- expect(r.firstNameOriginal).toHaveTextContent("f2");
486
- });
487
- it("returns empty lists even when inputs are undefined", async () => {
488
- const autoSave = jest.fn();
489
- // Given a component
490
- function TestComponent() {
491
- // And there is no initial data (i.e. we're creating the author)
492
- const form = (0, useFormState_1.useFormState)({
493
- config: authorWithBooksConfig,
494
- init: { input: undefined, map: (d) => d },
495
- autoSave,
496
- });
497
- return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "len", children: form.books.value.length }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "dirty", children: String(form.books.dirty) })] }));
498
- }
499
- // Then we can still read it as empty
500
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
501
- expect(r.len).toHaveTextContent("0");
502
- expect(r.dirty).toHaveTextContent("false");
503
- });
504
- it("does not infinite loop when calling set inside of auto-save", async () => {
505
- // Given a component
506
- let autoSaves = 0;
507
- function TestComponent() {
508
- const config = authorConfig;
509
- const data = { firstName: "f1", lastName: "f1" };
510
- const form = (0, useFormState_1.useFormState)({
511
- config,
512
- init: { input: data },
513
- // And there is reactive business logic in the `autoSave` method
514
- async autoSave(state) {
515
- state.lastName.set("l2");
516
- autoSaves++;
517
- },
518
- });
519
- return ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("button", { "data-testid": "setFirst", onClick: () => form.firstName.set("f2") }) }));
520
- }
521
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
522
- // When we change firstName
523
- await (0, rtl_utils_1.clickAndWait)(r.setFirst);
524
- // When autoSave didn't infinite loop
525
- expect(autoSaves).toEqual(1);
526
- });
527
- it("batches calls to auto-save", async () => {
528
- // Given a component
529
- const autoSave = jest.fn();
530
- function TestComponent() {
531
- const config = authorConfig;
532
- const data = { firstName: "f1", lastName: "f1" };
533
- const form = (0, useFormState_1.useFormState)({
534
- config,
535
- init: { input: data },
536
- autoSave: (form) => autoSave(form.changedValue),
537
- });
538
- return ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("button", { "data-testid": "set", onClick: () => {
539
- // And it sets two fields in a single callback
540
- form.firstName.set("f2");
541
- form.lastName.set("l2");
542
- } }) }));
543
- }
544
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
545
- // When we change both
546
- await (0, rtl_utils_1.clickAndWait)(r.set);
547
- // Then we only call autoSave once
548
- expect(autoSave).toBeCalledTimes(1);
549
- expect(autoSave).toBeCalledWith({ firstName: "f2", lastName: "l2" });
550
- });
551
- it("queues changes for auto save if a save is already in progress", async () => {
552
- const autoSaveStub = jest.fn();
553
- const config = {
554
- id: { type: "value" },
555
- firstName: { type: "value" },
556
- lastName: { type: "value" },
557
- };
558
- // Given a component using `getObjectState` for lazily creating ObjectStates
559
- function TestComponent() {
560
- const [apiData, setApiData] = (0, react_2.useState)({ id: "a:1", firstName: "Brandon", lastName: "Dow" });
561
- const state = (0, useFormState_1.useFormState)({ config, autoSave, init: { input: apiData, map: (v) => v } });
562
- async function autoSave(form) {
563
- autoSaveStub(form.changedValue);
564
- const changed = form.changedValue;
565
- // Pretend to make an API call and update the local state
566
- await Promise.resolve(1);
567
- setApiData((prevState) => ({ ...prevState, ...changed }));
568
- }
569
- return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { "data-testid": "name", children: [apiData.firstName, " ", apiData.lastName] }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "focusSetAndSaveField", onClick: () => {
570
- state.firstName.focus();
571
- state.firstName.set("Foo");
572
- state.firstName.maybeAutoSave();
573
- state.firstName.blur();
574
- } }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "focusSetAndSaveFieldLastName", onClick: () => {
575
- state.lastName.focus();
576
- state.lastName.set("Bar");
577
- state.lastName.maybeAutoSave();
578
- } })] }));
579
- }
580
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
581
- // And triggering the auto save behavior before awaiting the initial promise to
582
- // resolve so we have pending changes.
583
- (0, rtl_utils_1.click)(r.focusSetAndSaveField, { allowAsync: true });
584
- // Let the initial autoSave be called
585
- (0, react_1.act)(() => {
586
- jest.runOnlyPendingTimers();
587
- });
588
- expect(r.name).toHaveTextContent("Brandon Dow");
589
- expect(autoSaveStub).toBeCalledTimes(1);
590
- expect(autoSaveStub).toBeCalledWith({ id: "a:1", firstName: "Foo" });
591
- // And while that is in flight, trigger another user action
592
- (0, rtl_utils_1.click)(r.focusSetAndSaveFieldLastName, { allowAsync: true });
593
- // (Use `wait` so that our timer flushes before the Promise.resolve(1) is ran)
594
- await (0, rtl_utils_1.wait)();
595
- // Then expect the auto save to only have been called twice. Once with each changedValues.
596
- expect(autoSaveStub).toBeCalledTimes(2);
597
- expect(autoSaveStub).toBeCalledWith({ id: "a:1", firstName: "Foo" });
598
- expect(autoSaveStub).toBeCalledWith({ id: "a:1", lastName: "Bar" });
599
- });
600
- it("calls autoSave with results of calculations in addRules", async () => {
601
- const autoSaveStub = jest.fn();
602
- const config = {
603
- id: { type: "value" },
604
- firstName: { type: "value" },
605
- lastName: { type: "value" },
606
- };
607
- // Given a component that is using autoSave
608
- function TestComponent() {
609
- const fs = (0, useFormState_1.useFormState)({
610
- config,
611
- addRules(fs) {
612
- // And also has calculated values
613
- (0, mobx_1.reaction)(() => fs.firstName.value, (curr) => (fs.lastName.value = curr));
614
- },
615
- autoSave: (fs) => autoSaveStub(fs.changedValue),
616
- init: { input: { id: "a:1" } },
617
- });
618
- return (0, jsx_runtime_1.jsx)(FormStateApp_1.TextField, { field: fs.firstName });
619
- }
620
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
621
- // When the user sets one field
622
- await (0, rtl_utils_1.typeAndWait)(r.firstName, "first");
623
- // Then we only called autoSave once
624
- expect(autoSaveStub).toBeCalledTimes(1);
625
- expect(autoSaveStub).toBeCalledWith({ id: "a:1", firstName: "first", lastName: "first" });
626
- });
627
- it("sets loading if opts.loading is true", async () => {
628
- const config = { firstName: { type: "value" } };
629
- function TestComponent({ loading }) {
630
- const form = (0, useFormState_1.useFormState)({ config, loading });
631
- return (0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => (0, jsx_runtime_1.jsx)("div", { "data-testid": "loading", children: String(form.loading) }) });
632
- }
633
- // And we initially pass in `init.query.loading: true`
634
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, { loading: true }));
635
- // Then the form is marked as loading
636
- expect(r.loading).toHaveTextContent("true");
637
- // And when the query is not loading
638
- await r.rerender((0, jsx_runtime_1.jsx)(TestComponent, { loading: false }));
639
- // Then the form is marked as not loading
640
- expect(r.loading).toHaveTextContent("false");
641
- });
642
- it("sets loading if input.data is undefined", async () => {
643
- const config = { firstName: { type: "value" } };
644
- function TestComponent({ data }) {
645
- const form = (0, useFormState_1.useFormState)({ config, init: { input: data } });
646
- return (0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => (0, jsx_runtime_1.jsx)("div", { "data-testid": "loading", children: String(form.loading) }) });
647
- }
648
- // And we initially pass in `init.input: undefined`
649
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, { data: undefined }));
650
- // Then the form is marked as loading
651
- expect(r.loading).toHaveTextContent("true");
652
- // And when the data is no longer undefined
653
- await r.rerender((0, jsx_runtime_1.jsx)(TestComponent, { data: { firstName: "first" } }));
654
- // Then the form is marked as not loading
655
- expect(r.loading).toHaveTextContent("false");
656
- });
657
- it("sets loading if query.loading is true", async () => {
658
- const config = { firstName: { type: "value" } };
659
- function TestComponent({ loading, data }) {
660
- const form = (0, useFormState_1.useFormState)({ config, init: { query: { data, loading, error: null }, map: (d) => d } });
661
- return (0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => (0, jsx_runtime_1.jsx)("div", { "data-testid": "loading", children: String(form.loading) }) });
662
- }
663
- // And we initially pass in `init.query.loading: true`
664
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, { loading: true, data: undefined }));
665
- // Then the form is marked as loading
666
- expect(r.loading).toHaveTextContent("true");
667
- // And when the query is not loading
668
- await r.rerender((0, jsx_runtime_1.jsx)(TestComponent, { loading: false, data: { firstName: "first" } }));
669
- // Then the form is marked as not loading
670
- expect(r.loading).toHaveTextContent("false");
671
- });
672
- it("treats the id changing as a whole new entity instead of a delete", async () => {
673
- const config = { firstName: { type: "value" } };
674
- function TestComponent() {
675
- // Given an initial author a1
676
- const [author, setAuthor] = (0, react_2.useState)({ id: "a:1", firstName: "a1" });
677
- const form = (0, useFormState_1.useFormState)({ config, init: { input: author, map: (a) => a } });
678
- return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "value", children: String(form.firstName.value) }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "dirty", children: String(form.firstName.dirty) }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "originalValue", children: String(form.firstName.originalValue) }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "objectValue", children: String(form.value.firstName) }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "a1", onClick: () => setAuthor({ id: "a:1", firstName: "a1" }) }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "a2", onClick: () => setAuthor({ id: "a:2", firstName: undefined }) })] })) }));
679
- }
680
- const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
681
- // And the value is initially a1/and not dirty
682
- expect(r.value).toHaveTextContent("a1");
683
- expect(r.dirty).toHaveTextContent("false");
684
- expect(r.originalValue).toHaveTextContent("a1");
685
- expect(r.objectValue).toHaveTextContent("a1");
686
- // When we switch to a completely separate author
687
- (0, rtl_utils_1.click)(r.a2);
688
- // Then it switches to the next author
689
- expect(r.value).toHaveTextContent("undefined");
690
- // And the field is not dirty (which had been the case before this bug fix)
691
- expect(r.dirty).toHaveTextContent("false");
692
- expect(r.originalValue).toHaveTextContent("undefined");
693
- expect(r.objectValue).toHaveTextContent("undefined");
694
- // And when we switch back to the original author
695
- (0, rtl_utils_1.click)(r.a1);
696
- // It again restores the value and does not think an edit was WIP
697
- expect(r.value).toHaveTextContent("a1");
698
- expect(r.dirty).toHaveTextContent("false");
699
- expect(r.originalValue).toHaveTextContent("a1");
700
- expect(r.objectValue).toHaveTextContent("a1");
701
- });
702
- it("doesn't allow calling commitChanges", async () => {
703
- // Given a component
704
- const autoSave = jest.fn();
705
- function TestComponent() {
706
- const config = authorConfig;
707
- const form = (0, useFormState_1.useFormState)({
708
- config,
709
- init: { input: { firstName: "f1", lastName: "f1" } },
710
- autoSave: async (form) => {
711
- autoSave(form.changedValue);
712
- // And the autoSave functions erroneously calls commitChanges
713
- form.commitChanges();
714
- },
715
- });
716
- return (0, jsx_runtime_1.jsx)("button", { "data-testid": "set", onClick: () => form.firstName.set("f2") });
717
- }
718
- await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
719
- // When we change a field, and try to commit, it blows up
720
- // await expect(clickAndWait(r.set)).rejects.toThrow(
721
- // "When using autoSave, you should not manually call commitChanges",
722
- // );
723
- //
724
- // ...for some reason, I think maybe due to rtl-utils calling runOnlyPendingTimers
725
- // instead of runOnlyPendingTimers async, the `clickAndWait` actually resolves
726
- // successfully (it's not a rejected promise), and our test technically keeps running,
727
- // except then somewhere in the guts of Jest, the fake timer infra realizes that a tick
728
- // failed (our autosave is actually fired from a setTimout tick), and so the Jest test
729
- // fails anyway, but without anyway for us to catch and do a `.toThrow` assertion.
730
- });
731
- });
732
- const authorConfig = {
733
- id: { type: "value" },
734
- firstName: { type: "value" },
735
- lastName: { type: "value" },
736
- };
737
- const authorWithBooksConfig = {
738
- id: { type: "value" },
739
- firstName: { type: "value" },
740
- lastName: { type: "value" },
741
- books: {
742
- type: "list",
743
- config: {
744
- id: { type: "value" },
745
- title: { type: "value", rules: [rules_1.required] },
746
- classification: { type: "value" },
747
- },
748
- },
749
- };
750
- const authorWithAddressAndBooksConfig = {
751
- id: { type: "value" },
752
- firstName: { type: "value" },
753
- lastName: { type: "value" },
754
- address: {
755
- type: "object",
756
- config: {
757
- id: { type: "value" },
758
- street: { type: "value", rules: [rules_1.required] },
759
- city: { type: "value" },
760
- },
761
- },
762
- books: {
763
- type: "list",
764
- config: {
765
- id: { type: "value" },
766
- title: { type: "value", rules: [rules_1.required] },
767
- classification: { type: "value" },
768
- },
769
- },
770
- };
771
- const authorWithBookFragments = {
772
- id: { type: "value" },
773
- firstName: { type: "value" },
774
- books: {
775
- type: "list",
776
- config: {
777
- id: { type: "value" },
778
- data: { type: "fragment" },
779
- },
780
- },
781
- };
782
- /** Emulates a user focusing and then blurring a field. */
783
- function focusAndBlur(state) {
784
- state.focus();
785
- state.blur();
786
- }
787
- function noop(value) { }