@homebound/form-state 2.2.7 → 2.2.11

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.
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.pickFields = exports.useFormState = void 0;
7
+ const is_plain_object_1 = __importDefault(require("is-plain-object"));
8
+ const mobx_1 = require("mobx");
9
+ const react_1 = require("react");
10
+ const formState_1 = require("./formState");
11
+ const utils_1 = require("./utils");
12
+ let isAutoSaving = false;
13
+ /**
14
+ * Creates a formState instance for editing in a form.
15
+ */
16
+ function useFormState(opts) {
17
+ const { config, init, addRules, readOnly = false, autoSave } = opts;
18
+ // Use a ref so our memo'ized `onBlur` always see the latest value
19
+ const autoSaveRef = react_1.useRef(autoSave);
20
+ autoSaveRef.current = autoSave;
21
+ const firstRunRef = react_1.useRef(true);
22
+ // This is a little weird, but we need to know ahead of time, before the form useMemo, if we're working with classes/mobx proxies
23
+ const [firstInitValue] = react_1.useState(() => initValue(config, init));
24
+ const isWrappingMobxProxy = !is_plain_object_1.default(firstInitValue);
25
+ // If they're using init.input, useMemo on it (and it might be an array), otherwise allow the identity of init be unstable
26
+ const dep = init && "input" in init && "map" in init ? (Array.isArray(init.input) ? init.input : [init.input]) : [];
27
+ const form = react_1.useMemo(() => {
28
+ function onBlur() {
29
+ // Don't use canSave() because we don't want to set touched for all of the field
30
+ if (autoSaveRef.current && form.dirty && form.valid && !isAutoSaving) {
31
+ try {
32
+ isAutoSaving = true;
33
+ autoSaveRef.current(form);
34
+ }
35
+ finally {
36
+ isAutoSaving = false;
37
+ }
38
+ }
39
+ }
40
+ const value = firstRunRef.current ? firstInitValue : initValue(config, init);
41
+ const form = formState_1.createObjectState(config, value, { onBlur });
42
+ form.readOnly = readOnly;
43
+ // The identity of `addRules` is not stable, but assume that it is for better UX.
44
+ // eslint-disable-next-line react-hooks/exhaustive-deps
45
+ (addRules || (() => { }))(form);
46
+ firstRunRef.current = true;
47
+ return form;
48
+ },
49
+ // For this useMemo, we (almost) never re-run so that we can have a stable `form` identity across query refreshes.
50
+ // eslint-disable-next-line react-hooks/exhaustive-deps
51
+ [config, ...(isWrappingMobxProxy ? dep : [])]);
52
+ // We use useEffect so that any mutations to the proxies, which will call `setState`s on any observers to
53
+ // queue their components' render), don't happen during our render, per https://fb.me/setstate-in-render.
54
+ react_1.useEffect(() => {
55
+ // Ignore the 1st run b/c our 1st useMemo already initialized `form` with the current `init` value.
56
+ // Also for mobx proxies, we recreate a new form-state every time init changes, so that our
57
+ // fields fundamentally pointing to the right proxy. So just defer to the ^ useMemo.
58
+ if (firstRunRef.current || isWrappingMobxProxy) {
59
+ firstRunRef.current = false;
60
+ return;
61
+ }
62
+ form.set(initValue(config, init), { refreshing: true });
63
+ }, [form, ...dep]);
64
+ // Use useEffect so that we don't touch the form.init proxy during a render
65
+ react_1.useEffect(() => {
66
+ form.readOnly = readOnly;
67
+ }, [form, readOnly]);
68
+ return form;
69
+ }
70
+ exports.useFormState = useFormState;
71
+ /** Introspects the `init` prop to see if has a `map` function/etc. and returns the form value. */
72
+ function initValue(config, init) {
73
+ const initValue = init && "input" in init && "map" in init
74
+ ? init.input
75
+ ? init.map(init.input)
76
+ : init.ifUndefined || {}
77
+ : init || {};
78
+ return pickFields(config, initValue);
79
+ }
80
+ /**
81
+ * Picks ony fields out of `instance` that are in the `formConfig`.
82
+ *
83
+ * This is useful for creating a form input from a GraphQL query result,
84
+ * but ignoring extra data in the query that we don't want/need in the form.
85
+ *
86
+ * (Especially because the form.value will become our mutation's input type
87
+ * that goes on the wire, we don't want superfluous data in it.)
88
+ */
89
+ function pickFields(formConfig, instance) {
90
+ // If the caller is using classes, i.e. with their own custom observable behavior, then just use as-is
91
+ if (!is_plain_object_1.default(instance)) {
92
+ return instance;
93
+ }
94
+ return Object.fromEntries(Object.entries(formConfig).map(([key, _keyConfig]) => {
95
+ const keyConfig = _keyConfig;
96
+ const value = instance[key];
97
+ if (keyConfig.type === "object") {
98
+ if (value) {
99
+ return [key, pickFields(keyConfig.config, value)];
100
+ }
101
+ else {
102
+ return [key, value];
103
+ }
104
+ }
105
+ else if (keyConfig.type === "list") {
106
+ if (mobx_1.isObservable(value)) {
107
+ // If we hit an observable array, leave it as the existing proxy so the our
108
+ // ListFieldState will react to changes in the original array.
109
+ return [key, value];
110
+ }
111
+ else if (value) {
112
+ return [key, value.map((u) => pickFields(keyConfig.config, u))];
113
+ }
114
+ else {
115
+ return [key, value];
116
+ }
117
+ }
118
+ else if (keyConfig.type === "value") {
119
+ return [key, value];
120
+ }
121
+ else {
122
+ return utils_1.assertNever(keyConfig);
123
+ }
124
+ }));
125
+ }
126
+ exports.pickFields = pickFields;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,474 @@
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 mobx_1 = require("mobx");
6
+ const mobx_react_1 = require("mobx-react");
7
+ const react_1 = require("react");
8
+ const formState_1 = require("./formState");
9
+ const useFormState_1 = require("./useFormState");
10
+ describe("useFormState", () => {
11
+ it("calls init.map if init.input is defined", async () => {
12
+ // Given a component
13
+ function TestComponent() {
14
+ const config = { firstName: { type: "value" } };
15
+ // And we have query data that may or may not be defined
16
+ const data = Math.random() >= 0 ? { firstName: "bob" } : undefined;
17
+ // Then the lambda is passed the "de-undefined" data
18
+ const form = useFormState_1.useFormState({
19
+ config,
20
+ init: { input: data, map: (d) => ({ firstName: d.firstName }) },
21
+ });
22
+ return jsx_runtime_1.jsx("div", { children: form.firstName.value }, void 0);
23
+ }
24
+ const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
25
+ expect(r.baseElement).toHaveTextContent("bob");
26
+ });
27
+ it("memoizes on init.input being an array", async () => {
28
+ const config = { firstName: { type: "value" } };
29
+ function TestComponent() {
30
+ const [, setTick] = react_1.useState(0);
31
+ const form = useFormState_1.useFormState({
32
+ config,
33
+ init: { input: ["a", "b"], map: ([a, b]) => ({ firstName: a + b }) },
34
+ });
35
+ const onClick = () => [form.firstName.set("fred"), setTick(1)];
36
+ return (jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsx("button", { "data-testid": "change", onClick: onClick }, void 0),
37
+ jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "firstName" }, { children: form.firstName.value }), void 0)] }, void 0));
38
+ }
39
+ const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
40
+ expect(r.firstName()).toHaveTextContent("ab");
41
+ rtl_utils_1.click(r.change);
42
+ expect(r.firstName()).toHaveTextContent("fred");
43
+ });
44
+ it("uses default if init.input is undefined", async () => {
45
+ // Given a component
46
+ function TestComponent() {
47
+ const config = { firstName: { type: "value" } };
48
+ // And we have query data that may or may not be defined (but is actually undefined)
49
+ const data = Math.random() >= 0 ? undefined : { firstName: "bob" };
50
+ const form = useFormState_1.useFormState({
51
+ config,
52
+ init: { input: data, map: (d) => ({ firstName: d.firstName }) },
53
+ });
54
+ return jsx_runtime_1.jsx("div", { children: form.firstName.value }, void 0);
55
+ }
56
+ const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
57
+ // Then we init.map wasn't called, and we used {} instead
58
+ expect(r.baseElement.textContent).toEqual("");
59
+ });
60
+ it("uses custom init.ifUndefined if init.input is undefined", async () => {
61
+ // Given a component
62
+ function TestComponent() {
63
+ const config = {
64
+ id: { type: "value" },
65
+ firstName: { type: "value" },
66
+ };
67
+ // And we have query data that may or may not be defined (but is actually undefined)
68
+ const data = Math.random() >= 0 ? undefined : { firstName: "bob" };
69
+ const form = useFormState_1.useFormState({
70
+ config,
71
+ // And we pass `ifUndefined`
72
+ init: {
73
+ input: data,
74
+ map: (d) => ({ firstName: d.firstName }),
75
+ ifUndefined: { firstName: "default" },
76
+ },
77
+ });
78
+ return (jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "firstName" }, { children: form.firstName.value }), void 0),
79
+ jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "changedValue" }, { children: JSON.stringify(form.changedValue) }), void 0)] }, void 0));
80
+ }
81
+ const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
82
+ // Then we use the ifUndefined value
83
+ expect(r.firstName.textContent).toEqual("default");
84
+ expect(r.changedValue.textContent).toEqual(JSON.stringify({ firstName: "default" }));
85
+ });
86
+ it("uses init if set as a value", async () => {
87
+ const config = { firstName: { type: "value" } };
88
+ function TestComponent() {
89
+ const [, setTick] = react_1.useState(0);
90
+ const form = useFormState_1.useFormState({
91
+ config,
92
+ // That's using a raw init value
93
+ init: { firstName: "bob" },
94
+ });
95
+ return (jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsx("button", { "data-testid": "change", onClick: () => {
96
+ // When that value changes
97
+ form.firstName.set("fred");
98
+ // And also we re-render the component
99
+ setTick(1);
100
+ } }, void 0),
101
+ jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "firstName" }, { children: form.firstName.value }), void 0)] }, void 0));
102
+ }
103
+ const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
104
+ expect(r.firstName()).toHaveTextContent("bob");
105
+ rtl_utils_1.click(r.change);
106
+ // Then the change didn't get dropped due to init being unstable
107
+ expect(r.firstName()).toHaveTextContent("fred");
108
+ });
109
+ it("doesn't required an init value", async () => {
110
+ function TestComponent() {
111
+ const config = { firstName: { type: "value" } };
112
+ const form = useFormState_1.useFormState({ config });
113
+ return jsx_runtime_1.jsx("div", { children: form.firstName.value }, void 0);
114
+ }
115
+ const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
116
+ expect(r.baseElement.textContent).toEqual("");
117
+ });
118
+ it("keeps local changed values when a query refreshes", async () => {
119
+ // Given a component
120
+ function TestComponent() {
121
+ const config = authorWithAddressAndBooksConfig;
122
+ // And we have two sets of data
123
+ const data1 = {
124
+ id: "a:1",
125
+ firstName: "f1",
126
+ lastName: "l1",
127
+ address: { id: "address:1", street: "s1", city: "c1" },
128
+ books: [
129
+ { id: "b:1", title: "a1" },
130
+ { id: "b:2", title: "b1" },
131
+ ],
132
+ };
133
+ const data2 = {
134
+ id: "a:1",
135
+ firstName: "f2",
136
+ lastName: "l2",
137
+ address: { id: "address:1", street: "s2", city: "c2" },
138
+ books: [
139
+ { id: "b:1", title: "a2" },
140
+ { id: "b:2", title: "b2" },
141
+ { id: "b:3", title: "b3" },
142
+ ],
143
+ };
144
+ // Eventually our local values are saved
145
+ const data3 = {
146
+ id: "a:1",
147
+ firstName: "local",
148
+ lastName: "l2",
149
+ address: { id: "address:1", street: "local", city: "c2" },
150
+ books: [
151
+ { id: "b:1", title: "local" },
152
+ { id: "b:2", title: "b2" },
153
+ { id: "b:3", title: "b3" },
154
+ ],
155
+ };
156
+ // And we start out with data1
157
+ const [data, setData] = react_1.useState(data1);
158
+ const form = useFormState_1.useFormState({ config, init: { input: data, map: (d) => d } });
159
+ function makeLocalChanges() {
160
+ form.firstName.value = "local";
161
+ form.address.street.value = "local";
162
+ form.books.rows[0].title.value = "local";
163
+ }
164
+ return (jsx_runtime_1.jsx(mobx_react_1.Observer, { children: () => (jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "firstName" }, { children: form.firstName.value }), void 0),
165
+ jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "lastName" }, { children: form.lastName.value }), void 0),
166
+ jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "street" }, { children: form.address.street.value }), void 0),
167
+ jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "city" }, { children: form.address.city.value }), void 0),
168
+ jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "title1" }, { children: form.books.rows[0].title.value }), void 0),
169
+ jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "title2" }, { children: form.books.rows[1].title.value }), void 0),
170
+ jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "booksLength" }, { children: form.books.rows.length }), void 0),
171
+ jsx_runtime_1.jsx("button", { "data-testid": "makeLocalChanges", onClick: makeLocalChanges }, void 0),
172
+ jsx_runtime_1.jsx("button", { "data-testid": "refreshData", onClick: () => setData(data2) }, void 0),
173
+ jsx_runtime_1.jsx("button", { "data-testid": "saveData", onClick: () => setData(data3) }, void 0),
174
+ jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "changedValue" }, { children: JSON.stringify(form.changedValue) }), void 0)] }, void 0)) }, void 0));
175
+ }
176
+ // And we start out with the initial query data
177
+ const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
178
+ expect(r.firstName().textContent).toEqual("f1");
179
+ expect(r.street().textContent).toEqual("s1");
180
+ expect(r.title1().textContent).toEqual("a1");
181
+ // When we make some local changes
182
+ rtl_utils_1.click(r.makeLocalChanges);
183
+ // Then we see them
184
+ expect(r.firstName().textContent).toEqual("local");
185
+ expect(r.street().textContent).toEqual("local");
186
+ expect(r.title1().textContent).toEqual("local");
187
+ expect(JSON.parse(r.changedValue().textContent)).toEqual({
188
+ id: "a:1",
189
+ address: { id: "address:1", street: "local" },
190
+ books: [{ id: "b:1", title: "local" }, { id: "b:2" }],
191
+ firstName: "local",
192
+ });
193
+ // And when the new query is ran i.e. due to a cache refresh
194
+ rtl_utils_1.click(r.refreshData);
195
+ // Then we kept our local changes
196
+ expect(r.firstName().textContent).toEqual("local");
197
+ expect(r.street().textContent).toEqual("local");
198
+ expect(r.title1().textContent).toEqual("local");
199
+ // But we also see the new data for fields we have not changed
200
+ expect(r.lastName().textContent).toEqual("l2");
201
+ expect(r.city().textContent).toEqual("c2");
202
+ expect(r.title2().textContent).toEqual("b2");
203
+ expect(r.booksLength().textContent).toEqual("3");
204
+ expect(JSON.parse(r.changedValue().textContent)).toEqual({
205
+ id: "a:1",
206
+ address: { id: "address:1", street: "local" },
207
+ books: [{ id: "b:1", title: "local" }, { id: "b:2" }, { id: "b:3" }],
208
+ firstName: "local",
209
+ });
210
+ // And then when our mutation results come back
211
+ rtl_utils_1.click(r.saveData);
212
+ // Then changedValue doesn't show our local changes anymore
213
+ expect(JSON.parse(r.changedValue().textContent)).toEqual({
214
+ id: "a:1",
215
+ });
216
+ });
217
+ it("useFormState can accept new data while read only", async () => {
218
+ // Given a component
219
+ function TestComponent() {
220
+ const config = authorWithAddressAndBooksConfig;
221
+ // And we have two sets of data
222
+ const data1 = {
223
+ firstName: "f1",
224
+ address: { street: "s1" },
225
+ books: [{ title: "a1" }],
226
+ };
227
+ const data2 = {
228
+ firstName: "f2",
229
+ address: { street: "s2" },
230
+ books: [{ title: "a2" }],
231
+ };
232
+ // And we start out with data1
233
+ const [data, setData] = react_1.useState(data1);
234
+ const form = useFormState_1.useFormState({
235
+ config,
236
+ init: { input: data, map: (d) => d },
237
+ // And the form is read only
238
+ readOnly: true,
239
+ });
240
+ return (jsx_runtime_1.jsx(mobx_react_1.Observer, { children: () => (jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "firstName" }, { children: form.firstName.value }), void 0),
241
+ jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "street" }, { children: form.address.street.value }), void 0),
242
+ jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "title1" }, { children: form.books.rows[0].title.value }), void 0),
243
+ jsx_runtime_1.jsx("button", { "data-testid": "refreshData", onClick: () => setData(data2) }, void 0)] }, void 0)) }, void 0));
244
+ }
245
+ // And we start out with the initial query data
246
+ const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
247
+ expect(r.firstName().textContent).toEqual("f1");
248
+ expect(r.street().textContent).toEqual("s1");
249
+ expect(r.title1().textContent).toEqual("a1");
250
+ // When the new query is ran i.e. due to a cache refresh
251
+ rtl_utils_1.click(r.refreshData);
252
+ // Then we see the latest data
253
+ expect(r.firstName().textContent).toEqual("f2");
254
+ expect(r.street().textContent).toEqual("s2");
255
+ expect(r.title1().textContent).toEqual("a2");
256
+ });
257
+ it("useFormState can accept new data with computed fields", async () => {
258
+ // Given a component
259
+ function TestComponent() {
260
+ // And it's using a class/mobx proxy as the basis for the data
261
+ class AuthorRow {
262
+ constructor(firstName, lastName) {
263
+ this.firstName = firstName;
264
+ this.lastName = lastName;
265
+ }
266
+ get fullName() {
267
+ return this.firstName + " " + this.lastName;
268
+ }
269
+ }
270
+ // And we have two sets of data
271
+ const data1 = { firstName: "f1", lastName: "l1" };
272
+ const data2 = { firstName: "f2", lastName: "l2" };
273
+ // And we start out with data1
274
+ const [data, setData] = react_1.useState(data1);
275
+ const author = react_1.useMemo(() => new AuthorRow(data.firstName, data.lastName), [data]);
276
+ const config = react_1.useMemo(() => ({
277
+ firstName: { type: "value" },
278
+ lastName: { type: "value" },
279
+ fullName: { type: "value", computed: true },
280
+ }), []);
281
+ const form = useFormState_1.useFormState({ config, init: { input: author, map: (a) => a } });
282
+ return (jsx_runtime_1.jsx(mobx_react_1.Observer, { children: () => (jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "firstName" }, { children: form.firstName.value }), void 0),
283
+ jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "fullName" }, { children: form.fullName.value }), void 0),
284
+ jsx_runtime_1.jsx("button", { "data-testid": "refreshData", onClick: () => setData(data2) }, void 0)] }, void 0)) }, void 0));
285
+ }
286
+ // And we start out with the initial query data
287
+ const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
288
+ expect(r.firstName().textContent).toEqual("f1");
289
+ expect(r.fullName().textContent).toEqual("f1 l1");
290
+ // When the new query is ran i.e. due to a cache refresh
291
+ rtl_utils_1.click(r.refreshData);
292
+ expect(r.firstName().textContent).toEqual("f2");
293
+ expect(r.fullName().textContent).toEqual("f2 l2");
294
+ });
295
+ it("can trigger auto save for fields in list that were initially undefined", async () => {
296
+ const autoSave = jest.fn();
297
+ // Given a component
298
+ function TestComponent() {
299
+ // When the data is initially undefined
300
+ const [data, setData] = react_1.useState();
301
+ const data2 = { books: [{ title: "Title 1" }] };
302
+ const form = useFormState_1.useFormState({
303
+ config: authorWithBooksConfig,
304
+ init: { input: data, map: (d) => d, ifUndefined: { books: [] } },
305
+ autoSave,
306
+ });
307
+ return (jsx_runtime_1.jsx(mobx_react_1.Observer, { children: () => (jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsx("button", { "data-testid": "refreshData", onClick: () => setData(data2) }, void 0),
308
+ jsx_runtime_1.jsx("button", { "data-testid": "add", onClick: () => form.books.add({ title: "New Book" }) }, void 0),
309
+ jsx_runtime_1.jsx("button", { "data-testid": "blurBookOne", onClick: () => form.books.rows[0].title.blur() }, void 0),
310
+ jsx_runtime_1.jsx("button", { "data-testid": "blurBookTwo", onClick: () => form.books.rows[1].title.blur() }, void 0)] }, void 0)) }, void 0));
311
+ }
312
+ // Given a formState with `onBlur` set
313
+ const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
314
+ // When the data/child is now available
315
+ rtl_utils_1.click(r.refreshData);
316
+ // And the field is blurred
317
+ rtl_utils_1.click(r.blurBookOne);
318
+ // Then we don't auto-save because nothing has changed
319
+ expect(autoSave).toBeCalledTimes(0);
320
+ // And when adding a new book
321
+ rtl_utils_1.click(r.add);
322
+ // We autoSave the new row right away (because we don't have any validation rules
323
+ // that say the new row can't be empty)
324
+ expect(autoSave).toBeCalledTimes(1);
325
+ // And the new book is blurred
326
+ rtl_utils_1.click(r.blurBookTwo);
327
+ // Then we auto save again
328
+ expect(autoSave).toBeCalledTimes(2);
329
+ });
330
+ it("does not infinite loop when calling set inside of auto-save", async () => {
331
+ // Given a component
332
+ let autoSaves = 0;
333
+ function TestComponent() {
334
+ const config = authorConfig;
335
+ const data = { firstName: "f1", lastName: "f1" };
336
+ const form = useFormState_1.useFormState({
337
+ config,
338
+ init: data,
339
+ // And there is reactive business logic in the `autoSave` method
340
+ autoSave(state) {
341
+ state.lastName.set("l2");
342
+ autoSaves++;
343
+ },
344
+ });
345
+ return (jsx_runtime_1.jsx("div", { children: jsx_runtime_1.jsx("button", { "data-testid": "setFirst", onClick: () => form.firstName.set("f2") }, void 0) }, void 0));
346
+ }
347
+ const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
348
+ // When we change firstName
349
+ rtl_utils_1.click(r.setFirst);
350
+ // When autoSave didn't infinite loop
351
+ expect(autoSaves).toEqual(1);
352
+ });
353
+ describe("pickFields", () => {
354
+ it("can pick a value field", () => {
355
+ const a = useFormState_1.pickFields(
356
+ //
357
+ { firstName: { type: "value" } }, { firstName: "a", b: "ignored" });
358
+ expect(a).toMatchInlineSnapshot(`
359
+ Object {
360
+ "firstName": "a",
361
+ }
362
+ `);
363
+ });
364
+ it("can pick an unset object fields", () => {
365
+ const a = useFormState_1.pickFields(authorWithAddressConfig, { firstName: "a", b: "ignored" });
366
+ expect(a).toMatchInlineSnapshot(`
367
+ Object {
368
+ "address": undefined,
369
+ "firstName": "a",
370
+ "id": undefined,
371
+ "lastName": undefined,
372
+ }
373
+ `);
374
+ });
375
+ it("can pick a set object field", () => {
376
+ const a = useFormState_1.pickFields(authorWithAddressConfig, { firstName: "a", b: "ignored", address: {} });
377
+ expect(a).toMatchInlineSnapshot(`
378
+ Object {
379
+ "address": Object {
380
+ "city": undefined,
381
+ "street": undefined,
382
+ },
383
+ "firstName": "a",
384
+ "id": undefined,
385
+ "lastName": undefined,
386
+ }
387
+ `);
388
+ });
389
+ it("can pick an unset list field", () => {
390
+ const a = useFormState_1.pickFields(authorWithBooksConfig, { firstName: "a", b: "ignored" });
391
+ expect(a).toMatchInlineSnapshot(`
392
+ Object {
393
+ "books": undefined,
394
+ "firstName": "a",
395
+ "id": undefined,
396
+ "lastName": undefined,
397
+ }
398
+ `);
399
+ });
400
+ it("can pick a set list field", () => {
401
+ const a = useFormState_1.pickFields(authorWithBooksConfig, { firstName: "a", b: "ignored", books: [{}] });
402
+ expect(a).toMatchInlineSnapshot(`
403
+ Object {
404
+ "books": Array [
405
+ Object {
406
+ "classification": undefined,
407
+ "id": undefined,
408
+ "title": undefined,
409
+ },
410
+ ],
411
+ "firstName": "a",
412
+ "id": undefined,
413
+ "lastName": undefined,
414
+ }
415
+ `);
416
+ });
417
+ it("can pick a set observable list field", () => {
418
+ const books = mobx_1.observable([]);
419
+ const a = useFormState_1.pickFields(authorWithBooksConfig, { firstName: "a", b: "ignored", books });
420
+ expect(mobx_1.isObservable(a.books)).toEqual(true);
421
+ });
422
+ });
423
+ });
424
+ const authorConfig = {
425
+ id: { type: "value" },
426
+ firstName: { type: "value" },
427
+ lastName: { type: "value" },
428
+ };
429
+ const authorWithAddressConfig = {
430
+ id: { type: "value" },
431
+ firstName: { type: "value" },
432
+ lastName: { type: "value" },
433
+ address: {
434
+ type: "object",
435
+ config: {
436
+ street: { type: "value", rules: [formState_1.required] },
437
+ city: { type: "value" },
438
+ },
439
+ },
440
+ };
441
+ const authorWithBooksConfig = {
442
+ id: { type: "value" },
443
+ firstName: { type: "value" },
444
+ lastName: { type: "value" },
445
+ books: {
446
+ type: "list",
447
+ config: {
448
+ id: { type: "value" },
449
+ title: { type: "value", rules: [formState_1.required] },
450
+ classification: { type: "value" },
451
+ },
452
+ },
453
+ };
454
+ const authorWithAddressAndBooksConfig = {
455
+ id: { type: "value" },
456
+ firstName: { type: "value" },
457
+ lastName: { type: "value" },
458
+ address: {
459
+ type: "object",
460
+ config: {
461
+ id: { type: "value" },
462
+ street: { type: "value", rules: [formState_1.required] },
463
+ city: { type: "value" },
464
+ },
465
+ },
466
+ books: {
467
+ type: "list",
468
+ config: {
469
+ id: { type: "value" },
470
+ title: { type: "value", rules: [formState_1.required] },
471
+ classification: { type: "value" },
472
+ },
473
+ },
474
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/form-state",
3
- "version": "2.2.7",
3
+ "version": "2.2.11",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",