@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.
- package/dist/index.d.ts +437 -9
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1109 -14
- package/dist/index.js.map +1 -0
- package/package.json +40 -43
- package/dist/FormStateApp.d.ts +0 -5
- package/dist/FormStateApp.js +0 -54
- package/dist/FormStateApp.test.d.ts +0 -1
- package/dist/FormStateApp.test.js +0 -26
- package/dist/config.d.ts +0 -78
- package/dist/config.js +0 -2
- package/dist/configBuilders.d.ts +0 -67
- package/dist/configBuilders.js +0 -115
- package/dist/configBuilders.test.d.ts +0 -1
- package/dist/configBuilders.test.js +0 -65
- package/dist/fields/deepEquals.d.ts +0 -7
- package/dist/fields/deepEquals.js +0 -53
- package/dist/fields/fragmentField.d.ts +0 -4
- package/dist/fields/fragmentField.js +0 -58
- package/dist/fields/listField.d.ts +0 -11
- package/dist/fields/listField.js +0 -355
- package/dist/fields/objectField.d.ts +0 -63
- package/dist/fields/objectField.js +0 -277
- package/dist/fields/valueField.d.ts +0 -81
- package/dist/fields/valueField.js +0 -244
- package/dist/formState.stories.d.ts +0 -4
- package/dist/formState.stories.js +0 -12
- package/dist/formState.test.d.ts +0 -11
- package/dist/formState.test.js +0 -2109
- package/dist/formStateDomain.d.ts +0 -47
- package/dist/formStateDomain.js +0 -28
- package/dist/mobx-behavior.test.d.ts +0 -1
- package/dist/mobx-behavior.test.js +0 -51
- package/dist/proxies.d.ts +0 -5
- package/dist/proxies.js +0 -20
- package/dist/rules.d.ts +0 -10
- package/dist/rules.js +0 -11
- package/dist/setupTests.d.ts +0 -1
- package/dist/setupTests.js +0 -13
- package/dist/stackoverlfow.d.ts +0 -17
- package/dist/stackoverlfow.js +0 -17
- package/dist/temporal.d.ts +0 -6
- package/dist/temporal.js +0 -34
- package/dist/temporal.test.d.ts +0 -1
- package/dist/temporal.test.js +0 -97
- package/dist/useFormState.d.ts +0 -82
- package/dist/useFormState.js +0 -158
- package/dist/useFormState.test.d.ts +0 -1
- package/dist/useFormState.test.js +0 -787
- package/dist/useFormStates.d.ts +0 -68
- package/dist/useFormStates.js +0 -101
- package/dist/useFormStates.test.d.ts +0 -1
- package/dist/useFormStates.test.js +0 -223
- package/dist/utils.d.ts +0 -52
- package/dist/utils.js +0 -198
- package/dist/utils.test.d.ts +0 -1
- package/dist/utils.test.js +0 -102
package/dist/useFormStates.d.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { ObjectConfig } from "./config";
|
|
2
|
-
import { ObjectState } from "./fields/objectField";
|
|
3
|
-
export type ObjectStateCache<T, I> = Record<string, [ObjectState<T>, I]>;
|
|
4
|
-
/**
|
|
5
|
-
* The opts has for `useFormStates`.
|
|
6
|
-
*
|
|
7
|
-
* @typeparam T the form type, which is usually as close as possible to your *GraphQL input*
|
|
8
|
-
* @typeparam I the *form input* type, which is usually the *GraphQL output* type, i.e. the type of the response from your GraphQL query
|
|
9
|
-
*/
|
|
10
|
-
type UseFormStatesOpts<T, I> = {
|
|
11
|
-
/**
|
|
12
|
-
* The config to use for each form state.
|
|
13
|
-
*
|
|
14
|
-
* Should be stable/useMemo'd.
|
|
15
|
-
*/
|
|
16
|
-
config: ObjectConfig<T>;
|
|
17
|
-
/**
|
|
18
|
-
* Fired for each individual `ObjectState` when it's had a value change.
|
|
19
|
-
*
|
|
20
|
-
* Does not need to be stable/useMemo'd.
|
|
21
|
-
*/
|
|
22
|
-
autoSave?: (state: ObjectState<T>) => Promise<void>;
|
|
23
|
-
/**
|
|
24
|
-
* A hook to add custom, cross-field validation rules that can be difficult to setup directly in the config DSL.
|
|
25
|
-
*
|
|
26
|
-
* This will be called once-per `ObjectState` instance, and so is effectively a `useEffect` hook with
|
|
27
|
-
* a `[config, objectState]` dependency.
|
|
28
|
-
*
|
|
29
|
-
* Does not need to be stable/useMemo'd.
|
|
30
|
-
*/
|
|
31
|
-
addRules?: (state: ObjectState<T>) => void;
|
|
32
|
-
/**
|
|
33
|
-
* Given an input to `getFormState`, returns the identity value that we'll cache that value's form state on.
|
|
34
|
-
*
|
|
35
|
-
* Does not need to be stable/useMemo'd.
|
|
36
|
-
*/
|
|
37
|
-
getId: (v: I) => string;
|
|
38
|
-
/**
|
|
39
|
-
* Maps an input to `getFormState` to the actual form shape `T`.
|
|
40
|
-
*
|
|
41
|
-
* Does not need to be stable/useMemo'd.
|
|
42
|
-
*/
|
|
43
|
-
map?: (input: Exclude<I, null | undefined>) => T;
|
|
44
|
-
/**
|
|
45
|
-
* Sets all `ObjectState`s to readOnly.
|
|
46
|
-
*/
|
|
47
|
-
readOnly?: boolean;
|
|
48
|
-
};
|
|
49
|
-
type UseFormStatesHook<T, I> = {
|
|
50
|
-
getFormState: (input: I, opts?: {
|
|
51
|
-
readOnly?: boolean;
|
|
52
|
-
}) => ObjectState<T>;
|
|
53
|
-
};
|
|
54
|
-
/**
|
|
55
|
-
* A hook to manage many "mini-forms" on a single page, typically one form per row
|
|
56
|
-
* in a table.
|
|
57
|
-
*
|
|
58
|
-
* This hook basically provides the page/table with a cache, so each table row naively ask "what's
|
|
59
|
-
* the form state for this given row's data?" and get back a new-or-existing `ObjectState` instance
|
|
60
|
-
* that, if already existing, still has any of the user's WIP changes.
|
|
61
|
-
*
|
|
62
|
-
* Each mini-form/row can have its own autoSave calls, independent of the other rows.
|
|
63
|
-
*
|
|
64
|
-
* @typeparam T the form type, which is usually as close as possible to your *GraphQL input*
|
|
65
|
-
* @typeparam I the *form input* type, which is usually the *GraphQL output* type, i.e. the type of the response from your GraphQL query
|
|
66
|
-
*/
|
|
67
|
-
export declare function useFormStates<T, I = T>(opts: UseFormStatesOpts<T, I>): UseFormStatesHook<T, I>;
|
|
68
|
-
export {};
|
package/dist/useFormStates.js
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.useFormStates = useFormStates;
|
|
4
|
-
const react_1 = require("react");
|
|
5
|
-
const objectField_1 = require("./fields/objectField");
|
|
6
|
-
const utils_1 = require("./utils");
|
|
7
|
-
/**
|
|
8
|
-
* A hook to manage many "mini-forms" on a single page, typically one form per row
|
|
9
|
-
* in a table.
|
|
10
|
-
*
|
|
11
|
-
* This hook basically provides the page/table with a cache, so each table row naively ask "what's
|
|
12
|
-
* the form state for this given row's data?" and get back a new-or-existing `ObjectState` instance
|
|
13
|
-
* that, if already existing, still has any of the user's WIP changes.
|
|
14
|
-
*
|
|
15
|
-
* Each mini-form/row can have its own autoSave calls, independent of the other rows.
|
|
16
|
-
*
|
|
17
|
-
* @typeparam T the form type, which is usually as close as possible to your *GraphQL input*
|
|
18
|
-
* @typeparam I the *form input* type, which is usually the *GraphQL output* type, i.e. the type of the response from your GraphQL query
|
|
19
|
-
*/
|
|
20
|
-
function useFormStates(opts) {
|
|
21
|
-
const { config, autoSave, getId, map, addRules, readOnly = false } = opts;
|
|
22
|
-
const objectStateCache = (0, react_1.useMemo)(() => ({}),
|
|
23
|
-
// Force resetting the cache if config changes
|
|
24
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
25
|
-
[config]);
|
|
26
|
-
// Keep track of ObjectStates that triggered auto-save when a save was already in progress.
|
|
27
|
-
const pendingAutoSaves = (0, react_1.useRef)(new Set());
|
|
28
|
-
// Use a ref so our memo'ized `autoSave` always see the latest value
|
|
29
|
-
const autoSaveRef = (0, react_1.useRef)(autoSave);
|
|
30
|
-
autoSaveRef.current = autoSave;
|
|
31
|
-
// Use a ref b/c we're memod
|
|
32
|
-
const readOnlyRef = (0, react_1.useRef)(readOnly);
|
|
33
|
-
readOnlyRef.current = readOnly;
|
|
34
|
-
const getFormState = (0, react_1.useCallback)((input, opts = {}) => {
|
|
35
|
-
const existing = objectStateCache[getId(input)];
|
|
36
|
-
let form = existing === null || existing === void 0 ? void 0 : existing[0];
|
|
37
|
-
async function maybeAutoSave(form) {
|
|
38
|
-
// Don't use form.canSave() because we don't want to set touched for all the fields
|
|
39
|
-
if (autoSaveRef.current && form.dirty) {
|
|
40
|
-
// It's very frustrating to not know why the form is savings, to go ahead and log these
|
|
41
|
-
if (!form.valid) {
|
|
42
|
-
console.debug("Skipping auto-save b/c form is invalid: ", form.errors);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
const { current: pending } = pendingAutoSaves;
|
|
46
|
-
if (isAutoSaving) {
|
|
47
|
-
pending.add(form);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
let maybeError;
|
|
51
|
-
try {
|
|
52
|
-
isAutoSaving = true;
|
|
53
|
-
// See if we have any reactions that want to run (i.e. added by addRules hooks)
|
|
54
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
55
|
-
// If a reaction re-queued our form during the ^ wait, remove it
|
|
56
|
-
pending.delete(form);
|
|
57
|
-
await autoSaveRef.current(form);
|
|
58
|
-
}
|
|
59
|
-
catch (e) {
|
|
60
|
-
maybeError = String(e);
|
|
61
|
-
throw e;
|
|
62
|
-
}
|
|
63
|
-
finally {
|
|
64
|
-
isAutoSaving = false;
|
|
65
|
-
if (pending.size > 0) {
|
|
66
|
-
const first = pending.values().next().value;
|
|
67
|
-
pending.delete(first);
|
|
68
|
-
await maybeAutoSave(first);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
// If it didn't exist, then add to the cache.
|
|
74
|
-
if (!form) {
|
|
75
|
-
form = (0, objectField_1.createObjectState)(config, (0, utils_1.initValue)(config, { map, input }), {
|
|
76
|
-
maybeAutoSave: () => maybeAutoSave(form),
|
|
77
|
-
});
|
|
78
|
-
if (addRules) {
|
|
79
|
-
addRules(form);
|
|
80
|
-
}
|
|
81
|
-
objectStateCache[getId(input)] = [form, input];
|
|
82
|
-
}
|
|
83
|
-
// If the source of truth changed, then update the existing state and return it.
|
|
84
|
-
if (existing && existing[1] !== input) {
|
|
85
|
-
form.set((0, utils_1.initValue)(config, { map, input }), {
|
|
86
|
-
refreshing: true,
|
|
87
|
-
});
|
|
88
|
-
existing[1] = input;
|
|
89
|
-
}
|
|
90
|
-
form.readOnly = readOnlyRef.current || !!opts.readOnly;
|
|
91
|
-
return form;
|
|
92
|
-
},
|
|
93
|
-
// Allow the user to not stable-ize getId, map, addRules, and autoSave
|
|
94
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
95
|
-
[objectStateCache, config]);
|
|
96
|
-
return { getFormState };
|
|
97
|
-
}
|
|
98
|
-
// If the user's autoSave hook makes some last-minute `.set` calls to sneak
|
|
99
|
-
// in some business logic right before their GraphQL mutation call, ignore it
|
|
100
|
-
// so that we don't infinite loop.
|
|
101
|
-
let isAutoSaving = false;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,223 +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 mobx_1 = require("mobx");
|
|
6
|
-
const react_1 = require("react");
|
|
7
|
-
const FormStateApp_1 = require("./FormStateApp");
|
|
8
|
-
const rules_1 = require("./rules");
|
|
9
|
-
const useFormStates_1 = require("./useFormStates");
|
|
10
|
-
describe("useFormStates", () => {
|
|
11
|
-
it("can lazily create form states", async () => {
|
|
12
|
-
const autoSave = jest.fn();
|
|
13
|
-
// Given a parent and child component, where the formState is created only for the child component.
|
|
14
|
-
function ChildComponent({ os }) {
|
|
15
|
-
return (0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: os.firstName.value });
|
|
16
|
-
}
|
|
17
|
-
function TestComponent() {
|
|
18
|
-
const config = { id: { type: "value" }, firstName: { type: "value" } };
|
|
19
|
-
const { getFormState } = (0, useFormStates_1.useFormStates)({
|
|
20
|
-
config,
|
|
21
|
-
autoSave,
|
|
22
|
-
getId: (o) => o.id,
|
|
23
|
-
});
|
|
24
|
-
return ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)(ChildComponent, { os: getFormState({ id: "a:1", firstName: "Brandon" }) }) }));
|
|
25
|
-
}
|
|
26
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
27
|
-
// And the child component has defined state
|
|
28
|
-
expect(r.firstName).toHaveTextContent("Brandon");
|
|
29
|
-
});
|
|
30
|
-
it("can update existing object state from cache with new values", async () => {
|
|
31
|
-
const autoSave = jest.fn();
|
|
32
|
-
const config = { id: { type: "value" }, firstName: { type: "value" } };
|
|
33
|
-
// Given a component using `getFormState` for lazily creating ObjectStates
|
|
34
|
-
function TestComponent() {
|
|
35
|
-
const [apiData, setApiData] = (0, react_1.useState)({ id: "a:1", firstName: "Brandon" });
|
|
36
|
-
const { getFormState } = (0, useFormStates_1.useFormStates)({ config, autoSave, getId: (o) => o.id });
|
|
37
|
-
// Memoize an original for comparing the update against.
|
|
38
|
-
const originalState = (0, react_1.useMemo)(() => getFormState(apiData), [getFormState]);
|
|
39
|
-
const state = getFormState(apiData);
|
|
40
|
-
return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: state.firstName.value }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "statesEqual", children: JSON.stringify(state === originalState) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "updateApiData", onClick: () => setApiData((prevState) => ({ ...prevState, firstName: "Bob" })) })] }));
|
|
41
|
-
}
|
|
42
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
43
|
-
// And the initial values for the form state display
|
|
44
|
-
expect(r.firstName).toHaveTextContent("Brandon");
|
|
45
|
-
expect(r.statesEqual).toHaveTextContent("true");
|
|
46
|
-
// When updating the API data
|
|
47
|
-
await (0, rtl_utils_1.clickAndWait)(r.updateApiData);
|
|
48
|
-
// Then the new value is shown in the component
|
|
49
|
-
expect(r.firstName).toHaveTextContent("Bob");
|
|
50
|
-
// And the two states are using the same reference
|
|
51
|
-
expect(r.statesEqual).toHaveTextContent("true");
|
|
52
|
-
});
|
|
53
|
-
it("can queue up changes for auto save if a save is already in progress - works across multiple states", async () => {
|
|
54
|
-
const autoSaveStub = jest.fn();
|
|
55
|
-
const config = {
|
|
56
|
-
id: { type: "value" },
|
|
57
|
-
firstName: { type: "value" },
|
|
58
|
-
lastName: { type: "value" },
|
|
59
|
-
};
|
|
60
|
-
// Given a component using `getFormState` for lazily creating ObjectStates
|
|
61
|
-
function TestComponent() {
|
|
62
|
-
const [apiData, setApiData] = (0, react_1.useState)({ id: "a:1", firstName: "Tony", lastName: "Stark" });
|
|
63
|
-
const [apiData2, setApiData2] = (0, react_1.useState)({ id: "a:2", firstName: "Steve", lastName: "Rogers" });
|
|
64
|
-
const { getFormState } = (0, useFormStates_1.useFormStates)({ config, autoSave, getId: (o) => o.id });
|
|
65
|
-
const state = getFormState(apiData);
|
|
66
|
-
const state2 = getFormState(apiData2);
|
|
67
|
-
async function autoSave(form) {
|
|
68
|
-
autoSaveStub(form.changedValue);
|
|
69
|
-
// Pretend to make an API call and update the local state
|
|
70
|
-
if (form.id.value === "a:1") {
|
|
71
|
-
setApiData((prevState) => ({ ...prevState, ...form.changedValue }));
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
setApiData2((prevState) => ({ ...prevState, ...form.changedValue }));
|
|
75
|
-
}
|
|
76
|
-
await Promise.resolve(1);
|
|
77
|
-
}
|
|
78
|
-
return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: state.firstName.value }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "focusSetAndSaveField", onClick: () => {
|
|
79
|
-
state.firstName.focus();
|
|
80
|
-
state.firstName.set("Foo");
|
|
81
|
-
state.firstName.maybeAutoSave();
|
|
82
|
-
} }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "focusSetAndSaveFieldLastName", onClick: () => {
|
|
83
|
-
state.lastName.focus();
|
|
84
|
-
state.lastName.set("Bar");
|
|
85
|
-
state.lastName.maybeAutoSave();
|
|
86
|
-
} }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "focusSetAndSaveField2", onClick: () => {
|
|
87
|
-
state2.lastName.focus();
|
|
88
|
-
state2.lastName.set("Bar");
|
|
89
|
-
state2.lastName.maybeAutoSave();
|
|
90
|
-
} })] }));
|
|
91
|
-
}
|
|
92
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
93
|
-
// And triggering the auto save behavior before awaiting the initial promise to resolve so we have pending changes.
|
|
94
|
-
(0, rtl_utils_1.click)(r.focusSetAndSaveField);
|
|
95
|
-
(0, rtl_utils_1.click)(r.focusSetAndSaveFieldLastName);
|
|
96
|
-
(0, rtl_utils_1.click)(r.focusSetAndSaveField2);
|
|
97
|
-
// Awaits the promises for all methods triggered above
|
|
98
|
-
await (0, rtl_utils_1.wait)();
|
|
99
|
-
// Then expect the auto save to only have been called two times. Once with each set of changedValues.
|
|
100
|
-
expect(autoSaveStub).toBeCalledTimes(2);
|
|
101
|
-
expect(autoSaveStub).toBeCalledWith({ id: "a:1", firstName: "Foo", lastName: "Bar" });
|
|
102
|
-
expect(autoSaveStub).toBeCalledWith({ id: "a:2", lastName: "Bar" });
|
|
103
|
-
});
|
|
104
|
-
it("clears out cache if configuration changes", async () => {
|
|
105
|
-
const autoSave = jest.fn();
|
|
106
|
-
// Given a component with stable API data.
|
|
107
|
-
const apiData = { id: "a:1", firstName: "Brandon", lastName: "Dow" };
|
|
108
|
-
// And two sets of configurations
|
|
109
|
-
const originalConfig = { id: { type: "value" }, firstName: { type: "value" } };
|
|
110
|
-
const updatedConfig = {
|
|
111
|
-
id: { type: "value" },
|
|
112
|
-
firstName: { type: "value", rules: [rules_1.required] },
|
|
113
|
-
};
|
|
114
|
-
function TestComponent() {
|
|
115
|
-
const [config, setConfig] = (0, react_1.useState)(originalConfig);
|
|
116
|
-
const { getFormState } = (0, useFormStates_1.useFormStates)({ config, autoSave, getId: (o) => o.id });
|
|
117
|
-
// Memoize an original for comparing the update against.
|
|
118
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
119
|
-
const originalState = (0, react_1.useMemo)(() => getFormState(apiData), []);
|
|
120
|
-
const state = getFormState(apiData);
|
|
121
|
-
return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "statesEqual", children: JSON.stringify(state === originalState) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "updateConfig", onClick: () => setConfig(updatedConfig) })] }));
|
|
122
|
-
}
|
|
123
|
-
// When rendered with the original configuration
|
|
124
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
125
|
-
// Then the two form-states generated are equal
|
|
126
|
-
expect(r.statesEqual).toHaveTextContent("true");
|
|
127
|
-
// When updating the configuration object
|
|
128
|
-
(0, rtl_utils_1.click)(r.updateConfig);
|
|
129
|
-
// Then the two form-states are no longer equal.
|
|
130
|
-
expect(r.statesEqual).toHaveTextContent("false");
|
|
131
|
-
});
|
|
132
|
-
it("calls addRules once per form state", async () => {
|
|
133
|
-
// Given a user wants to use addRules
|
|
134
|
-
const addRules = jest.fn();
|
|
135
|
-
function TestComponent() {
|
|
136
|
-
const config = { id: { type: "value" }, firstName: { type: "value" } };
|
|
137
|
-
const { getFormState } = (0, useFormStates_1.useFormStates)({
|
|
138
|
-
config,
|
|
139
|
-
addRules,
|
|
140
|
-
getId: (o) => o.id,
|
|
141
|
-
});
|
|
142
|
-
return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(ChildComponent, { os: getFormState({ id: "a:1", firstName: "Brandon" }) }), (0, jsx_runtime_1.jsx)(ChildComponent, { os: getFormState({ id: "a:1", firstName: "Brandon" }) })] }));
|
|
143
|
-
}
|
|
144
|
-
// When we render
|
|
145
|
-
await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
146
|
-
// Then addRules was only called once
|
|
147
|
-
expect(addRules).toHaveBeenCalledTimes(1);
|
|
148
|
-
});
|
|
149
|
-
it("calls autoSave with results of calculations in addRules", async () => {
|
|
150
|
-
// Given a user wants to use auto save
|
|
151
|
-
const autoSave = jest.fn();
|
|
152
|
-
function TestComponent() {
|
|
153
|
-
const { getFormState } = (0, useFormStates_1.useFormStates)({
|
|
154
|
-
config,
|
|
155
|
-
getId: (o) => o.id,
|
|
156
|
-
addRules(fs) {
|
|
157
|
-
// And they have a reactive true that calculates last name
|
|
158
|
-
(0, mobx_1.reaction)(() => fs.firstName.value, (curr) => {
|
|
159
|
-
fs.lastName.set(curr);
|
|
160
|
-
});
|
|
161
|
-
},
|
|
162
|
-
async autoSave(fs) {
|
|
163
|
-
autoSave(fs.changedValue);
|
|
164
|
-
},
|
|
165
|
-
});
|
|
166
|
-
return (0, jsx_runtime_1.jsx)(FormStateApp_1.TextField, { field: getFormState({ id: "a:1", firstName: "Brandon" }).firstName });
|
|
167
|
-
}
|
|
168
|
-
// When we render
|
|
169
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
170
|
-
// And update the firstName
|
|
171
|
-
await (0, rtl_utils_1.typeAndWait)(r.firstName, "first");
|
|
172
|
-
// Then autoSave was called once with both input+calc'd values
|
|
173
|
-
expect(autoSave).toHaveBeenCalledTimes(1);
|
|
174
|
-
expect(autoSave).toHaveBeenCalledWith({ id: "a:1", firstName: "first", lastName: "first" });
|
|
175
|
-
});
|
|
176
|
-
it("can set readOnly via the hook opt", async () => {
|
|
177
|
-
// Given a test component
|
|
178
|
-
function TestComponent({ readOnly }) {
|
|
179
|
-
const { getFormState } = (0, useFormStates_1.useFormStates)({
|
|
180
|
-
config,
|
|
181
|
-
getId: (o) => o.id,
|
|
182
|
-
// And it passes readOnly directly to useFormStates
|
|
183
|
-
readOnly,
|
|
184
|
-
});
|
|
185
|
-
return (0, jsx_runtime_1.jsx)(ChildComponent, { os: getFormState({ id: "a:1", firstName: "Brandon" }) });
|
|
186
|
-
}
|
|
187
|
-
// When we render
|
|
188
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, { readOnly: true }));
|
|
189
|
-
// Then it's read only
|
|
190
|
-
expect(r.firstName).toHaveAttribute("data-readonly", "true");
|
|
191
|
-
// And when we rerender
|
|
192
|
-
await r.rerender((0, jsx_runtime_1.jsx)(TestComponent, { readOnly: false }));
|
|
193
|
-
// Then it's not read only
|
|
194
|
-
expect(r.firstName).toHaveAttribute("data-readonly", "false");
|
|
195
|
-
});
|
|
196
|
-
it("can set readOnly via the getFormState function", async () => {
|
|
197
|
-
// Given a test component
|
|
198
|
-
function TestComponent({ readOnly }) {
|
|
199
|
-
const { getFormState } = (0, useFormStates_1.useFormStates)({
|
|
200
|
-
config,
|
|
201
|
-
getId: (o) => o.id,
|
|
202
|
-
});
|
|
203
|
-
// And it passes readOnly directly to getFormState
|
|
204
|
-
return (0, jsx_runtime_1.jsx)(ChildComponent, { os: getFormState({ id: "a:1", firstName: "Brandon" }, { readOnly }) });
|
|
205
|
-
}
|
|
206
|
-
// When we render
|
|
207
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, { readOnly: true }));
|
|
208
|
-
// Then it's read only
|
|
209
|
-
expect(r.firstName).toHaveAttribute("data-readonly", "true");
|
|
210
|
-
// And when we rerender
|
|
211
|
-
await r.rerender((0, jsx_runtime_1.jsx)(TestComponent, { readOnly: false }));
|
|
212
|
-
// Then it's not read only
|
|
213
|
-
expect(r.firstName).toHaveAttribute("data-readonly", "false");
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
const config = {
|
|
217
|
-
id: { type: "value" },
|
|
218
|
-
firstName: { type: "value" },
|
|
219
|
-
lastName: { type: "value" },
|
|
220
|
-
};
|
|
221
|
-
function ChildComponent({ os }) {
|
|
222
|
-
return ((0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", "data-readonly": os.firstName.readOnly, children: os.firstName.value }));
|
|
223
|
-
}
|
package/dist/utils.d.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { ObjectConfig } from "./config";
|
|
2
|
-
import { type SupportedTemporal } from "./temporal";
|
|
3
|
-
import { InputAndMap, QueryAndMap, UseFormStateOpts } from "./useFormState";
|
|
4
|
-
export type Builtin = Date | Function | SupportedTemporal | Uint8Array | string | number | boolean;
|
|
5
|
-
export type Primitive = undefined | null | boolean | string | number | Function | Date | {
|
|
6
|
-
toJSON(): any;
|
|
7
|
-
};
|
|
8
|
-
/** Makes the keys in `T` required while keeping the values undefined. */
|
|
9
|
-
export type DeepRequired<T> = T extends Primitive ? T : {
|
|
10
|
-
[P in keyof Required<T>]: T[P] extends Array<infer U> ? Array<DeepRequired<U>> : T[P] extends ReadonlyArray<infer U2> ? ReadonlyArray<DeepRequired<U2>> : DeepRequired<T[P]>;
|
|
11
|
-
};
|
|
12
|
-
export type OmitIf<Base, Condition> = Pick<Base, {
|
|
13
|
-
[Key in keyof Base]: Base[Key] extends Condition ? never : Key;
|
|
14
|
-
}[keyof Base]>;
|
|
15
|
-
export declare function fail(message?: string): never;
|
|
16
|
-
export declare function assertNever(x: never): never;
|
|
17
|
-
/** Introspects the `init` prop to see if it has a `map` function/etc. and returns the form value. */
|
|
18
|
-
export declare function initValue<T>(config: ObjectConfig<T>, init: any): T;
|
|
19
|
-
export declare function isInput<T, I>(init: UseFormStateOpts<T, I>["init"]): init is InputAndMap<T, I>;
|
|
20
|
-
export declare function isQuery<T, I>(init: UseFormStateOpts<T, I>["init"]): init is QueryAndMap<T, I>;
|
|
21
|
-
/**
|
|
22
|
-
* Picks ony fields out of `instance` that are in the `formConfig`.
|
|
23
|
-
*
|
|
24
|
-
* This is useful for creating a form input from a GraphQL query result,
|
|
25
|
-
* but ignoring extra data in the query that we don't want/need in the form.
|
|
26
|
-
*
|
|
27
|
-
* (Especially because the form.value will become our mutation's input type
|
|
28
|
-
* that goes on the wire, we don't want superfluous data in it.)
|
|
29
|
-
*/
|
|
30
|
-
export declare function pickFields<T, I>(formConfig: ObjectConfig<T>, instance: I): {
|
|
31
|
-
[K in keyof T]: K extends keyof I ? I[K] : never;
|
|
32
|
-
};
|
|
33
|
-
export declare function isNotUndefined<T>(value: T | undefined): value is T;
|
|
34
|
-
export declare function isEmpty(value: any): boolean;
|
|
35
|
-
/**
|
|
36
|
-
* An equals that does deep-ish equality.
|
|
37
|
-
*
|
|
38
|
-
* We only do non-identity equals for:
|
|
39
|
-
*
|
|
40
|
-
* - "plain" objects that have no custom prototype/i.e. are object literals
|
|
41
|
-
* - objects that implement `toJSON`
|
|
42
|
-
* - arrays
|
|
43
|
-
*/
|
|
44
|
-
export declare function areEqual<T>(a?: T, b?: T, strictOrder?: boolean): boolean;
|
|
45
|
-
export declare function hasToJSON(o?: unknown): o is {
|
|
46
|
-
toJSON(): void;
|
|
47
|
-
};
|
|
48
|
-
/** Normalizes values into stable object-hash-safe data using our existing `toJSON` semantics. */
|
|
49
|
-
export declare function normalizeHashValue(value: unknown, seen?: WeakMap<object, unknown>): unknown;
|
|
50
|
-
/** Make a clone of `obj`, but only recurse into POJOs and Arrays...and stores. */
|
|
51
|
-
export declare function deepClone<T>(obj: T, map?: WeakMap<WeakKey, any>): T;
|
|
52
|
-
export declare function groupBy<T, Y = T, K = string>(list: readonly T[], fn: (x: T) => K, valueFn?: (x: T) => Y): Map<K, Y[]>;
|
package/dist/utils.js
DELETED
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.fail = fail;
|
|
4
|
-
exports.assertNever = assertNever;
|
|
5
|
-
exports.initValue = initValue;
|
|
6
|
-
exports.isInput = isInput;
|
|
7
|
-
exports.isQuery = isQuery;
|
|
8
|
-
exports.pickFields = pickFields;
|
|
9
|
-
exports.isNotUndefined = isNotUndefined;
|
|
10
|
-
exports.isEmpty = isEmpty;
|
|
11
|
-
exports.areEqual = areEqual;
|
|
12
|
-
exports.hasToJSON = hasToJSON;
|
|
13
|
-
exports.normalizeHashValue = normalizeHashValue;
|
|
14
|
-
exports.deepClone = deepClone;
|
|
15
|
-
exports.groupBy = groupBy;
|
|
16
|
-
const is_plain_object_1 = require("is-plain-object");
|
|
17
|
-
const mobx_1 = require("mobx");
|
|
18
|
-
const deepEquals_1 = require("./fields/deepEquals");
|
|
19
|
-
const temporal_1 = require("./temporal");
|
|
20
|
-
function fail(message) {
|
|
21
|
-
throw new Error(message || "Failed");
|
|
22
|
-
}
|
|
23
|
-
function assertNever(x) {
|
|
24
|
-
throw new Error("Unexpected object: " + x);
|
|
25
|
-
}
|
|
26
|
-
/** Introspects the `init` prop to see if it has a `map` function/etc. and returns the form value. */
|
|
27
|
-
function initValue(config, init) {
|
|
28
|
-
let value;
|
|
29
|
-
if (isInput(init)) {
|
|
30
|
-
value = init.input ? (init.map ? init.map(init.input) : init.input) : init.ifUndefined;
|
|
31
|
-
}
|
|
32
|
-
else if (isQuery(init)) {
|
|
33
|
-
value = init.query.data ? init.map(init.query.data) : init.ifUndefined;
|
|
34
|
-
}
|
|
35
|
-
else if (init === undefined) {
|
|
36
|
-
// allow completely undefined init
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
throw new Error("init must have an input or query key");
|
|
40
|
-
}
|
|
41
|
-
// Given our form config, pick out only the subset of fields out of `value` (unless it's a mobx class)
|
|
42
|
-
return pickFields(config, value !== null && value !== void 0 ? value : {});
|
|
43
|
-
}
|
|
44
|
-
function isInput(init) {
|
|
45
|
-
return !!init && typeof init === "object" && "input" in init;
|
|
46
|
-
}
|
|
47
|
-
function isQuery(init) {
|
|
48
|
-
return !!init && typeof init === "object" && "query" in init && "map" in init;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Picks ony fields out of `instance` that are in the `formConfig`.
|
|
52
|
-
*
|
|
53
|
-
* This is useful for creating a form input from a GraphQL query result,
|
|
54
|
-
* but ignoring extra data in the query that we don't want/need in the form.
|
|
55
|
-
*
|
|
56
|
-
* (Especially because the form.value will become our mutation's input type
|
|
57
|
-
* that goes on the wire, we don't want superfluous data in it.)
|
|
58
|
-
*/
|
|
59
|
-
function pickFields(formConfig, instance) {
|
|
60
|
-
// If the caller is using classes, i.e. with their own custom observable behavior, then just use as-is
|
|
61
|
-
if (!(0, is_plain_object_1.isPlainObject)(instance)) {
|
|
62
|
-
return instance;
|
|
63
|
-
}
|
|
64
|
-
return Object.fromEntries(Object.entries(formConfig).map(([key, _keyConfig]) => {
|
|
65
|
-
const keyConfig = _keyConfig;
|
|
66
|
-
const value = instance[key];
|
|
67
|
-
if (keyConfig.type === "object") {
|
|
68
|
-
if (value) {
|
|
69
|
-
return [key, pickFields(keyConfig.config, value)];
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
return [key, value];
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
else if (keyConfig.type === "list") {
|
|
76
|
-
if ((0, mobx_1.isObservable)(value)) {
|
|
77
|
-
// If we hit an observable array, leave it as the existing proxy so the our
|
|
78
|
-
// ListFieldState will react to changes in the original array.
|
|
79
|
-
return [key, value];
|
|
80
|
-
}
|
|
81
|
-
else if (value) {
|
|
82
|
-
return [key, value.map((u) => pickFields(keyConfig.config, u))];
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
return [key, value];
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
else if (keyConfig.type === "value") {
|
|
89
|
-
return [key, value];
|
|
90
|
-
}
|
|
91
|
-
else if (keyConfig.type === "fragment") {
|
|
92
|
-
return [key, value];
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
return assertNever(keyConfig);
|
|
96
|
-
}
|
|
97
|
-
}));
|
|
98
|
-
}
|
|
99
|
-
function isNotUndefined(value) {
|
|
100
|
-
return value !== undefined;
|
|
101
|
-
}
|
|
102
|
-
function isEmpty(value) {
|
|
103
|
-
return value === undefined || value === null || value === "";
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* An equals that does deep-ish equality.
|
|
107
|
-
*
|
|
108
|
-
* We only do non-identity equals for:
|
|
109
|
-
*
|
|
110
|
-
* - "plain" objects that have no custom prototype/i.e. are object literals
|
|
111
|
-
* - objects that implement `toJSON`
|
|
112
|
-
* - arrays
|
|
113
|
-
*/
|
|
114
|
-
function areEqual(a, b, strictOrder) {
|
|
115
|
-
const temporalEquals = (0, temporal_1.areSupportedTemporalValuesEqual)(a, b);
|
|
116
|
-
if (temporalEquals !== undefined) {
|
|
117
|
-
return temporalEquals;
|
|
118
|
-
}
|
|
119
|
-
if ((0, is_plain_object_1.isPlainObject)(a)) {
|
|
120
|
-
return (0, deepEquals_1.deepEquals)((0, mobx_1.toJS)(a), (0, mobx_1.toJS)(b));
|
|
121
|
-
}
|
|
122
|
-
if (hasToJSON(a) || hasToJSON(b)) {
|
|
123
|
-
const a1 = hasToJSON(a) ? a.toJSON() : a;
|
|
124
|
-
const b1 = hasToJSON(b) ? b.toJSON() : b;
|
|
125
|
-
return (0, deepEquals_1.deepEquals)(a1, b1);
|
|
126
|
-
}
|
|
127
|
-
if (a && b && a instanceof Array && b instanceof Array) {
|
|
128
|
-
if (strictOrder !== false) {
|
|
129
|
-
return (0, deepEquals_1.deepEquals)(a, b);
|
|
130
|
-
}
|
|
131
|
-
if (a.length !== b.length)
|
|
132
|
-
return false;
|
|
133
|
-
return a.every((a1) => b.some((b1) => areEqual(a1, b1)));
|
|
134
|
-
}
|
|
135
|
-
return a === b;
|
|
136
|
-
}
|
|
137
|
-
function hasToJSON(o) {
|
|
138
|
-
return !!(o && typeof o === "object" && "toJSON" in o);
|
|
139
|
-
}
|
|
140
|
-
/** Normalizes values into stable object-hash-safe data using our existing `toJSON` semantics. */
|
|
141
|
-
function normalizeHashValue(value, seen = new WeakMap()) {
|
|
142
|
-
if (hasToJSON(value)) {
|
|
143
|
-
return normalizeHashValue(value.toJSON(), seen);
|
|
144
|
-
}
|
|
145
|
-
if (Array.isArray(value)) {
|
|
146
|
-
if (seen.has(value)) {
|
|
147
|
-
return seen.get(value);
|
|
148
|
-
}
|
|
149
|
-
const result = [];
|
|
150
|
-
seen.set(value, result);
|
|
151
|
-
value.forEach((entry) => result.push(normalizeHashValue(entry, seen)));
|
|
152
|
-
return result;
|
|
153
|
-
}
|
|
154
|
-
if (value && typeof value === "object" && (0, is_plain_object_1.isPlainObject)(value)) {
|
|
155
|
-
if (seen.has(value)) {
|
|
156
|
-
return seen.get(value);
|
|
157
|
-
}
|
|
158
|
-
const result = {};
|
|
159
|
-
seen.set(value, result);
|
|
160
|
-
Object.entries(value).forEach(([key, entry]) => {
|
|
161
|
-
result[key] = normalizeHashValue(entry, seen);
|
|
162
|
-
});
|
|
163
|
-
return result;
|
|
164
|
-
}
|
|
165
|
-
return value;
|
|
166
|
-
}
|
|
167
|
-
/** Make a clone of `obj`, but only recurse into POJOs and Arrays...and stores. */
|
|
168
|
-
function deepClone(obj, map = new WeakMap()) {
|
|
169
|
-
if (obj && typeof obj === "object" && ((0, is_plain_object_1.isPlainObject)(obj) || Array.isArray(obj) || (0, mobx_1.isObservable)(obj))) {
|
|
170
|
-
if (map.has(obj))
|
|
171
|
-
return map.get(obj);
|
|
172
|
-
const result = Array.isArray(obj) ? [] : {};
|
|
173
|
-
map.set(obj, result);
|
|
174
|
-
Object.assign(result, ...getAllPropertyNames(obj).map((key) => ({ [key]: deepClone(obj[key], map) })));
|
|
175
|
-
return result;
|
|
176
|
-
}
|
|
177
|
-
else {
|
|
178
|
-
return obj;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
function groupBy(list, fn, valueFn) {
|
|
182
|
-
const result = new Map();
|
|
183
|
-
list.forEach((o) => {
|
|
184
|
-
const group = fn(o);
|
|
185
|
-
if (!result.has(group)) {
|
|
186
|
-
result.set(group, []);
|
|
187
|
-
}
|
|
188
|
-
result.get(group).push(valueFn === undefined ? o : valueFn(o));
|
|
189
|
-
});
|
|
190
|
-
return result;
|
|
191
|
-
}
|
|
192
|
-
/** Returns all property names, including mobx computeds (non-enumerable) & inherited. */
|
|
193
|
-
function getAllPropertyNames(obj) {
|
|
194
|
-
const proto = Object.getPrototypeOf(obj);
|
|
195
|
-
// Don't crawl up into Object.prototype, or into arrays/observable arrays
|
|
196
|
-
const inherited = proto && proto !== Object.prototype && !Array.isArray(obj) ? getAllPropertyNames(proto) : [];
|
|
197
|
-
return [...new Set(Object.getOwnPropertyNames(obj).concat(inherited))];
|
|
198
|
-
}
|
package/dist/utils.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|