@homebound/form-state 2.7.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/FormStateApp.d.ts +4 -0
- package/dist/FormStateApp.js +2 -1
- package/dist/useFormState.js +1 -2
- package/dist/useFormState.test.js +29 -0
- package/dist/useFormStates.d.ts +25 -5
- package/dist/useFormStates.js +34 -22
- package/dist/useFormStates.test.js +54 -20
- package/package.json +1 -1
package/dist/FormStateApp.d.ts
CHANGED
package/dist/FormStateApp.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FormStateApp = void 0;
|
|
3
|
+
exports.TextField = exports.FormStateApp = void 0;
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
5
|
const mobx_react_1 = require("mobx-react");
|
|
6
6
|
const index_1 = require("./index");
|
|
@@ -91,3 +91,4 @@ function TextField(props) {
|
|
|
91
91
|
jsx_runtime_1.jsx("td", Object.assign({ "data-testid": `${field.key}_errors` }, { children: field.errors }), void 0),
|
|
92
92
|
jsx_runtime_1.jsx("td", Object.assign({ "data-testid": `${field.key}_original` }, { children: field.originalValue }), void 0)] }, void 0) }, void 0)] }), void 0)] }, void 0)) }, void 0));
|
|
93
93
|
}
|
|
94
|
+
exports.TextField = TextField;
|
package/dist/useFormState.js
CHANGED
|
@@ -12,7 +12,6 @@ let isAutoSaving = false;
|
|
|
12
12
|
// `pendingAutoSave` is a flag for determining if we need to immediately call `maybeAutoSave` again after the initial Promise finishes
|
|
13
13
|
// This could happen if a field triggers auto-save while another field's auto-save is already in progress.
|
|
14
14
|
let pendingAutoSave = false;
|
|
15
|
-
let timerId;
|
|
16
15
|
/**
|
|
17
16
|
* Creates a formState instance for editing in a form.
|
|
18
17
|
*/
|
|
@@ -39,7 +38,7 @@ function useFormState(opts) {
|
|
|
39
38
|
if (autoSaveRef.current && form.dirty && form.valid && !isAutoSaving) {
|
|
40
39
|
isAutoSaving = "queued";
|
|
41
40
|
// We use setTimeout as a cheap way to wait until the end of the current event listener
|
|
42
|
-
|
|
41
|
+
setTimeout(async () => {
|
|
43
42
|
try {
|
|
44
43
|
// We technically don't flip to in-flight until after the call in case the
|
|
45
44
|
// user's autoSave function itself wants to call a .set.
|
|
@@ -3,8 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
4
4
|
const rtl_utils_1 = require("@homebound/rtl-utils");
|
|
5
5
|
const react_1 = require("@testing-library/react");
|
|
6
|
+
const mobx_1 = require("mobx");
|
|
6
7
|
const mobx_react_1 = require("mobx-react");
|
|
7
8
|
const react_2 = require("react");
|
|
9
|
+
const FormStateApp_1 = require("./FormStateApp");
|
|
8
10
|
const formState_1 = require("./formState");
|
|
9
11
|
const useFormState_1 = require("./useFormState");
|
|
10
12
|
describe("useFormState", () => {
|
|
@@ -425,6 +427,33 @@ describe("useFormState", () => {
|
|
|
425
427
|
expect(autoSaveStub).toBeCalledWith({ id: "a:1", firstName: "Foo" });
|
|
426
428
|
expect(autoSaveStub).toBeCalledWith({ id: "a:1", lastName: "Bar" });
|
|
427
429
|
});
|
|
430
|
+
it("calls autoSave with results of calculations in addRules", async () => {
|
|
431
|
+
const autoSaveStub = jest.fn();
|
|
432
|
+
const config = {
|
|
433
|
+
id: { type: "value" },
|
|
434
|
+
firstName: { type: "value" },
|
|
435
|
+
lastName: { type: "value" },
|
|
436
|
+
};
|
|
437
|
+
// Given a component that is using autoSave
|
|
438
|
+
function TestComponent() {
|
|
439
|
+
const fs = useFormState_1.useFormState({
|
|
440
|
+
config,
|
|
441
|
+
addRules(fs) {
|
|
442
|
+
// And also has calculated values
|
|
443
|
+
mobx_1.reaction(() => fs.firstName.value, (curr) => (fs.lastName.value = curr));
|
|
444
|
+
},
|
|
445
|
+
autoSave: (fs) => autoSaveStub(fs.changedValue),
|
|
446
|
+
init: { id: "a:1" },
|
|
447
|
+
});
|
|
448
|
+
return jsx_runtime_1.jsx(FormStateApp_1.TextField, { field: fs.firstName }, void 0);
|
|
449
|
+
}
|
|
450
|
+
const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
|
|
451
|
+
// When the user sets one field
|
|
452
|
+
await rtl_utils_1.typeAndWait(r.firstName, "first");
|
|
453
|
+
// Then we only called autoSave once
|
|
454
|
+
expect(autoSaveStub).toBeCalledTimes(1);
|
|
455
|
+
expect(autoSaveStub).toBeCalledWith({ id: "a:1", firstName: "first", lastName: "first" });
|
|
456
|
+
});
|
|
428
457
|
});
|
|
429
458
|
const authorConfig = {
|
|
430
459
|
id: { type: "value" },
|
package/dist/useFormStates.d.ts
CHANGED
|
@@ -1,21 +1,41 @@
|
|
|
1
1
|
import { ObjectConfig, ObjectState } from "./formState";
|
|
2
2
|
export declare type ObjectStateCache<T, I> = Record<string, [ObjectState<T>, I]>;
|
|
3
3
|
declare type UseFormStatesOpts<T, I> = {
|
|
4
|
+
/**
|
|
5
|
+
* The config to use for each form state.
|
|
6
|
+
*
|
|
7
|
+
* Should be stable/useMemo'd.
|
|
8
|
+
*/
|
|
4
9
|
config: ObjectConfig<T>;
|
|
5
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Fired for each individual `ObjectState` when it's had a value change.
|
|
12
|
+
*
|
|
13
|
+
* Does not need to be stable/useMemo'd.
|
|
14
|
+
*/
|
|
6
15
|
autoSave?: (state: ObjectState<T>) => Promise<void>;
|
|
7
16
|
/**
|
|
8
17
|
* A hook to add custom, cross-field validation rules that can be difficult to setup directly in the config DSL.
|
|
9
18
|
*
|
|
19
|
+
* This will be called once-per `ObjectState` instance, and so is effectively a `useEffect` hook with
|
|
20
|
+
* a `[config, objectState]` dependency.
|
|
21
|
+
*
|
|
10
22
|
* Does not need to be stable/useMemo'd.
|
|
11
23
|
*/
|
|
12
24
|
addRules?: (state: ObjectState<T>) => void;
|
|
13
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Given an input to `getFormState`, returns the identity value that we'll cache that value's form state on.
|
|
27
|
+
*
|
|
28
|
+
* Does not need to be stable/useMemo'd.
|
|
29
|
+
*/
|
|
14
30
|
getId: (v: I) => string;
|
|
15
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Maps an input to `getFormState` to the actual form shape `T`.
|
|
33
|
+
*
|
|
34
|
+
* Does not need to be stable/useMemo'd.
|
|
35
|
+
*/
|
|
16
36
|
map?: (input: Exclude<I, null | undefined>) => T;
|
|
17
37
|
};
|
|
18
|
-
export declare function useFormStates<T, I>(opts: UseFormStatesOpts<T, I>): {
|
|
19
|
-
|
|
38
|
+
export declare function useFormStates<T, I = T>(opts: UseFormStatesOpts<T, I>): {
|
|
39
|
+
getFormState: (input: I) => ObjectState<T>;
|
|
20
40
|
};
|
|
21
41
|
export {};
|
package/dist/useFormStates.js
CHANGED
|
@@ -6,7 +6,10 @@ const formState_1 = require("./formState");
|
|
|
6
6
|
const utils_1 = require("./utils");
|
|
7
7
|
function useFormStates(opts) {
|
|
8
8
|
const { config, autoSave, getId, map, addRules } = opts;
|
|
9
|
-
const objectStateCache = react_1.useMemo(() => ({}),
|
|
9
|
+
const objectStateCache = react_1.useMemo(() => ({}),
|
|
10
|
+
// Force resetting the cache if config changes
|
|
11
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
12
|
+
[config]);
|
|
10
13
|
// Keep track of ObjectStates that triggered auto-save when a save was already in progress.
|
|
11
14
|
const pendingAutoSaves = react_1.useRef([]);
|
|
12
15
|
// Use a ref so our memo'ized `autoSave` always see the latest value
|
|
@@ -20,6 +23,13 @@ function useFormStates(opts) {
|
|
|
20
23
|
if (autoSaveRef.current && form.dirty && form.valid && !isAutoSaving) {
|
|
21
24
|
try {
|
|
22
25
|
isAutoSaving = true;
|
|
26
|
+
// See if we have any reactions that want to run (i.e. added by addRules hooks)
|
|
27
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
28
|
+
// If a reaction re-queued our form during the ^ wait, remove it
|
|
29
|
+
let i = -1;
|
|
30
|
+
while ((i = pendingAutoSaves.current.indexOf(form)) !== -1) {
|
|
31
|
+
pendingAutoSaves.current.splice(i, 1);
|
|
32
|
+
}
|
|
23
33
|
await autoSaveRef.current(form);
|
|
24
34
|
}
|
|
25
35
|
finally {
|
|
@@ -30,28 +40,30 @@ function useFormStates(opts) {
|
|
|
30
40
|
}
|
|
31
41
|
}
|
|
32
42
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
addRules(form);
|
|
44
|
-
}
|
|
45
|
-
objectStateCache[getId(input)] = [form, input];
|
|
46
|
-
}
|
|
47
|
-
// If the source of truth changed, then update the existing state and return it.
|
|
48
|
-
if (existing && existing[1] !== input) {
|
|
49
|
-
form.set(utils_1.initValue(config, map ? { map, input } : input), { refreshing: true });
|
|
50
|
-
existing[1] = input;
|
|
43
|
+
const getFormState = react_1.useCallback((input) => {
|
|
44
|
+
const existing = objectStateCache[getId(input)];
|
|
45
|
+
let form = existing === null || existing === void 0 ? void 0 : existing[0];
|
|
46
|
+
// If it didn't exist, then add to the cache.
|
|
47
|
+
if (!form) {
|
|
48
|
+
form = formState_1.createObjectState(config, utils_1.initValue(config, map ? { map, input } : input), {
|
|
49
|
+
maybeAutoSave: () => maybeAutoSave(form),
|
|
50
|
+
});
|
|
51
|
+
if (addRules) {
|
|
52
|
+
addRules(form);
|
|
51
53
|
}
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
54
|
+
objectStateCache[getId(input)] = [form, input];
|
|
55
|
+
}
|
|
56
|
+
// If the source of truth changed, then update the existing state and return it.
|
|
57
|
+
if (existing && existing[1] !== input) {
|
|
58
|
+
form.set(utils_1.initValue(config, map ? { map, input } : input), { refreshing: true });
|
|
59
|
+
existing[1] = input;
|
|
60
|
+
}
|
|
61
|
+
return form;
|
|
62
|
+
},
|
|
63
|
+
// Allow the user to not stable-ize getId, map, addRules, and autoSave
|
|
64
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
65
|
+
[objectStateCache, config]);
|
|
66
|
+
return { getFormState };
|
|
55
67
|
}
|
|
56
68
|
exports.useFormStates = useFormStates;
|
|
57
69
|
// If the user's autoSave hook makes some last-minute `.set` calls to sneak
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
4
4
|
const rtl_utils_1 = require("@homebound/rtl-utils");
|
|
5
|
+
const mobx_1 = require("mobx");
|
|
5
6
|
const react_1 = require("react");
|
|
6
7
|
const formState_1 = require("./formState");
|
|
8
|
+
const FormStateApp_1 = require("./FormStateApp");
|
|
7
9
|
const useFormStates_1 = require("./useFormStates");
|
|
8
10
|
describe("useFormStates", () => {
|
|
9
11
|
it("can lazily create form states", async () => {
|
|
@@ -14,12 +16,12 @@ describe("useFormStates", () => {
|
|
|
14
16
|
}
|
|
15
17
|
function TestComponent() {
|
|
16
18
|
const config = { id: { type: "value" }, firstName: { type: "value" } };
|
|
17
|
-
const {
|
|
19
|
+
const { getFormState } = useFormStates_1.useFormStates({
|
|
18
20
|
config,
|
|
19
21
|
autoSave,
|
|
20
22
|
getId: (o) => o.id,
|
|
21
23
|
});
|
|
22
|
-
return (jsx_runtime_1.jsx("div", { children: jsx_runtime_1.jsx(ChildComponent, { os:
|
|
24
|
+
return (jsx_runtime_1.jsx("div", { children: jsx_runtime_1.jsx(ChildComponent, { os: getFormState({ id: "a:1", firstName: "Brandon" }) }, void 0) }, void 0));
|
|
23
25
|
}
|
|
24
26
|
const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
|
|
25
27
|
// And the child component has defined state
|
|
@@ -28,13 +30,13 @@ describe("useFormStates", () => {
|
|
|
28
30
|
it("can update existing object state from cache with new values", async () => {
|
|
29
31
|
const autoSave = jest.fn();
|
|
30
32
|
const config = { id: { type: "value" }, firstName: { type: "value" } };
|
|
31
|
-
// Given a component using `
|
|
33
|
+
// Given a component using `getFormState` for lazily creating ObjectStates
|
|
32
34
|
function TestComponent() {
|
|
33
35
|
const [apiData, setApiData] = react_1.useState({ id: "a:1", firstName: "Brandon" });
|
|
34
|
-
const {
|
|
36
|
+
const { getFormState } = useFormStates_1.useFormStates({ config, autoSave, getId: (o) => o.id });
|
|
35
37
|
// Memoize an original for comparing the update against.
|
|
36
|
-
const originalState = react_1.useMemo(() =>
|
|
37
|
-
const state =
|
|
38
|
+
const originalState = react_1.useMemo(() => getFormState(apiData), []);
|
|
39
|
+
const state = getFormState(apiData);
|
|
38
40
|
return (jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "firstName" }, { children: state.firstName.value }), void 0),
|
|
39
41
|
jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "statesEqual" }, { children: JSON.stringify(state === originalState) }), void 0),
|
|
40
42
|
jsx_runtime_1.jsx("button", { "data-testid": "updateApiData", onClick: () => setApiData((prevState) => ({ ...prevState, firstName: "Bob" })) }, void 0)] }, void 0));
|
|
@@ -57,13 +59,13 @@ describe("useFormStates", () => {
|
|
|
57
59
|
firstName: { type: "value" },
|
|
58
60
|
lastName: { type: "value" },
|
|
59
61
|
};
|
|
60
|
-
// Given a component using `
|
|
62
|
+
// Given a component using `getFormState` for lazily creating ObjectStates
|
|
61
63
|
function TestComponent() {
|
|
62
64
|
const [apiData, setApiData] = react_1.useState({ id: "a:1", firstName: "Tony", lastName: "Stark" });
|
|
63
65
|
const [apiData2, setApiData2] = react_1.useState({ id: "a:2", firstName: "Steve", lastName: "Rogers" });
|
|
64
|
-
const {
|
|
65
|
-
const state =
|
|
66
|
-
const state2 =
|
|
66
|
+
const { getFormState } = useFormStates_1.useFormStates({ config, autoSave, getId: (o) => o.id });
|
|
67
|
+
const state = getFormState(apiData);
|
|
68
|
+
const state2 = getFormState(apiData2);
|
|
67
69
|
async function autoSave(form) {
|
|
68
70
|
autoSaveStub(form.changedValue);
|
|
69
71
|
// Pretend to make an API call and update the local state
|
|
@@ -99,10 +101,9 @@ describe("useFormStates", () => {
|
|
|
99
101
|
rtl_utils_1.click(r.focusSetAndSaveField2());
|
|
100
102
|
// Awaits the promises for all methods triggered above
|
|
101
103
|
await rtl_utils_1.wait();
|
|
102
|
-
// Then expect the auto save to only have been called
|
|
103
|
-
expect(autoSaveStub).toBeCalledTimes(
|
|
104
|
-
expect(autoSaveStub).toBeCalledWith({ id: "a:1", firstName: "Foo" });
|
|
105
|
-
expect(autoSaveStub).toBeCalledWith({ id: "a:1", lastName: "Bar" });
|
|
104
|
+
// Then expect the auto save to only have been called two times. Once with each set of changedValues.
|
|
105
|
+
expect(autoSaveStub).toBeCalledTimes(2);
|
|
106
|
+
expect(autoSaveStub).toBeCalledWith({ id: "a:1", firstName: "Foo", lastName: "Bar" });
|
|
106
107
|
expect(autoSaveStub).toBeCalledWith({ id: "a:2", lastName: "Bar" });
|
|
107
108
|
});
|
|
108
109
|
it("clears out cache if configuration changes", async () => {
|
|
@@ -117,10 +118,10 @@ describe("useFormStates", () => {
|
|
|
117
118
|
};
|
|
118
119
|
function TestComponent() {
|
|
119
120
|
const [config, setConfig] = react_1.useState(originalConfig);
|
|
120
|
-
const {
|
|
121
|
+
const { getFormState } = useFormStates_1.useFormStates({ config, autoSave, getId: (o) => o.id });
|
|
121
122
|
// Memoize an original for comparing the update against.
|
|
122
|
-
const originalState = react_1.useMemo(() =>
|
|
123
|
-
const state =
|
|
123
|
+
const originalState = react_1.useMemo(() => getFormState(apiData), []);
|
|
124
|
+
const state = getFormState(apiData);
|
|
124
125
|
return (jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "statesEqual" }, { children: JSON.stringify(state === originalState) }), void 0),
|
|
125
126
|
jsx_runtime_1.jsx("button", { "data-testid": "updateConfig", onClick: () => setConfig(updatedConfig) }, void 0)] }, void 0));
|
|
126
127
|
}
|
|
@@ -138,19 +139,52 @@ describe("useFormStates", () => {
|
|
|
138
139
|
const addRules = jest.fn();
|
|
139
140
|
function TestComponent() {
|
|
140
141
|
const config = { id: { type: "value" }, firstName: { type: "value" } };
|
|
141
|
-
const {
|
|
142
|
+
const { getFormState } = useFormStates_1.useFormStates({
|
|
142
143
|
config,
|
|
143
144
|
addRules,
|
|
144
145
|
getId: (o) => o.id,
|
|
145
146
|
});
|
|
146
|
-
return (jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsx(ChildComponent, { os:
|
|
147
|
-
jsx_runtime_1.jsx(ChildComponent, { os:
|
|
147
|
+
return (jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsx(ChildComponent, { os: getFormState({ id: "a:1", firstName: "Brandon" }) }, void 0),
|
|
148
|
+
jsx_runtime_1.jsx(ChildComponent, { os: getFormState({ id: "a:1", firstName: "Brandon" }) }, void 0)] }, void 0));
|
|
148
149
|
}
|
|
149
150
|
// When we render
|
|
150
151
|
const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
|
|
151
152
|
// Then addRules was only called once
|
|
152
153
|
expect(addRules).toHaveBeenCalledTimes(1);
|
|
153
154
|
});
|
|
155
|
+
it("calls autoSave with results of calculations in addRules", async () => {
|
|
156
|
+
// Given a user wants to use auto save
|
|
157
|
+
const autoSave = jest.fn();
|
|
158
|
+
function TestComponent() {
|
|
159
|
+
const config = {
|
|
160
|
+
id: { type: "value" },
|
|
161
|
+
firstName: { type: "value" },
|
|
162
|
+
lastName: { type: "value" },
|
|
163
|
+
};
|
|
164
|
+
const { getFormState } = useFormStates_1.useFormStates({
|
|
165
|
+
config,
|
|
166
|
+
getId: (o) => o.id,
|
|
167
|
+
addRules(fs) {
|
|
168
|
+
// And they have a reactive true that calculates last name
|
|
169
|
+
mobx_1.reaction(() => fs.firstName.value, (curr) => {
|
|
170
|
+
console.log({ curr });
|
|
171
|
+
fs.lastName.set(curr);
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
async autoSave(fs) {
|
|
175
|
+
autoSave(fs.changedValue);
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
return jsx_runtime_1.jsx(FormStateApp_1.TextField, { field: getFormState({ id: "a:1", firstName: "Brandon" }).firstName }, void 0);
|
|
179
|
+
}
|
|
180
|
+
// When we render
|
|
181
|
+
const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
|
|
182
|
+
// And update the firstName
|
|
183
|
+
await rtl_utils_1.typeAndWait(r.firstName(), "first");
|
|
184
|
+
// Then autoSave was called once with both input+calc'd values
|
|
185
|
+
expect(autoSave).toHaveBeenCalledTimes(1);
|
|
186
|
+
expect(autoSave).toHaveBeenCalledWith({ id: "a:1", firstName: "first", lastName: "first" });
|
|
187
|
+
});
|
|
154
188
|
});
|
|
155
189
|
function ChildComponent({ os }) {
|
|
156
190
|
return jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "firstName" }, { children: os.firstName.value }), void 0);
|