@homebound/form-state 2.11.0 → 2.12.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/useFormState.d.ts +23 -5
- package/dist/useFormState.js +26 -9
- package/dist/useFormState.test.js +31 -1
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +20 -7
- package/package.json +1 -1
package/dist/useFormState.d.ts
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { ObjectConfig } from "./config";
|
|
2
2
|
import { ObjectState } from "./fields/objectField";
|
|
3
|
+
export declare type Query<I> = {
|
|
4
|
+
data: I;
|
|
5
|
+
loading: boolean;
|
|
6
|
+
error?: any;
|
|
7
|
+
};
|
|
8
|
+
export declare type InputAndMap<T, I> = {
|
|
9
|
+
input: I;
|
|
10
|
+
map: (input: Exclude<I, null | undefined>) => T;
|
|
11
|
+
ifUndefined?: T;
|
|
12
|
+
};
|
|
13
|
+
export declare type QueryAndMap<T, I> = {
|
|
14
|
+
query: Query<I>;
|
|
15
|
+
map: (input: Exclude<I, null | undefined>) => T;
|
|
16
|
+
ifUndefined?: T;
|
|
17
|
+
};
|
|
3
18
|
export declare type UseFormStateOpts<T, I> = {
|
|
4
19
|
/** The form configuration, should be a module-level const or useMemo'd. */
|
|
5
20
|
config: ObjectConfig<T>;
|
|
@@ -20,11 +35,7 @@ export declare type UseFormStateOpts<T, I> = {
|
|
|
20
35
|
* only call `init.map` if it's set, otherwise we'll use `init.ifDefined` or `{}`, saving you
|
|
21
36
|
* from having to null check within your `init.map` function.
|
|
22
37
|
*/
|
|
23
|
-
init?: T |
|
|
24
|
-
input: I;
|
|
25
|
-
map: (input: Exclude<I, null | undefined>) => T;
|
|
26
|
-
ifUndefined?: T;
|
|
27
|
-
};
|
|
38
|
+
init?: T | InputAndMap<T, I> | QueryAndMap<T, I>;
|
|
28
39
|
/**
|
|
29
40
|
* A hook to add custom, cross-field validation rules that can be difficult to setup directly in the config DSL.
|
|
30
41
|
*
|
|
@@ -33,6 +44,13 @@ export declare type UseFormStateOpts<T, I> = {
|
|
|
33
44
|
addRules?: (state: ObjectState<T>) => void;
|
|
34
45
|
/** Whether the form should be read only, when changed it won't re-create the whole form. */
|
|
35
46
|
readOnly?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Whether the form is loading.
|
|
49
|
+
*
|
|
50
|
+
* Note that we also will infer loading from `init.input === undefined` or `init.query.loading`,
|
|
51
|
+
* so you only need to set this directly if you're not using either of those conventions.
|
|
52
|
+
*/
|
|
53
|
+
loading?: boolean;
|
|
36
54
|
/**
|
|
37
55
|
* Fired when the form should auto-save, i.e. after a) blur and b) all fields are valid.
|
|
38
56
|
*
|
package/dist/useFormState.js
CHANGED
|
@@ -16,7 +16,7 @@ let pendingAutoSave = false;
|
|
|
16
16
|
* Creates a formState instance for editing in a form.
|
|
17
17
|
*/
|
|
18
18
|
function useFormState(opts) {
|
|
19
|
-
const { config, init, addRules, readOnly = false, autoSave } = opts;
|
|
19
|
+
const { config, init, addRules, readOnly = false, loading, autoSave } = opts;
|
|
20
20
|
// Use a ref so our memo'ized `onBlur` always see the latest value
|
|
21
21
|
const autoSaveRef = react_1.useRef(autoSave);
|
|
22
22
|
autoSaveRef.current = autoSave;
|
|
@@ -25,7 +25,7 @@ function useFormState(opts) {
|
|
|
25
25
|
const [firstInitValue] = react_1.useState(() => utils_1.initValue(config, init));
|
|
26
26
|
const isWrappingMobxProxy = !is_plain_object_1.isPlainObject(firstInitValue);
|
|
27
27
|
// If they're using init.input, useMemo on it (and it might be an array), otherwise allow the identity of init be unstable
|
|
28
|
-
const dep = init
|
|
28
|
+
const dep = utils_1.isInput(init) ? makeArray(init.input) : utils_1.isQuery(init) ? [init.query.data, init.query.loading] : [];
|
|
29
29
|
const form = react_1.useMemo(() => {
|
|
30
30
|
function maybeAutoSave() {
|
|
31
31
|
if (isAutoSaving === "in-flight") {
|
|
@@ -62,9 +62,7 @@ function useFormState(opts) {
|
|
|
62
62
|
const value = firstRunRef.current ? firstInitValue : utils_1.initValue(config, init);
|
|
63
63
|
const form = objectField_1.createObjectState(config, value, { maybeAutoSave });
|
|
64
64
|
form.readOnly = readOnly;
|
|
65
|
-
|
|
66
|
-
form.loading = init.input === undefined;
|
|
67
|
-
}
|
|
65
|
+
setLoading(form, opts);
|
|
68
66
|
// The identity of `addRules` is not stable, but assume that it is for better UX.
|
|
69
67
|
(addRules || (() => { }))(form);
|
|
70
68
|
firstRunRef.current = true;
|
|
@@ -83,15 +81,34 @@ function useFormState(opts) {
|
|
|
83
81
|
firstRunRef.current = false;
|
|
84
82
|
return;
|
|
85
83
|
}
|
|
86
|
-
|
|
87
|
-
form.loading = init.input === undefined;
|
|
88
|
-
}
|
|
84
|
+
setLoading(form, opts);
|
|
89
85
|
form.set(utils_1.initValue(config, init), { refreshing: true });
|
|
90
86
|
}, [form, ...dep]);
|
|
91
87
|
// Use useEffect so that we don't touch the form.init proxy during a render
|
|
92
88
|
react_1.useEffect(() => {
|
|
93
89
|
form.readOnly = readOnly;
|
|
94
|
-
|
|
90
|
+
if (loading !== undefined) {
|
|
91
|
+
form.loading = loading;
|
|
92
|
+
}
|
|
93
|
+
}, [form, readOnly, loading]);
|
|
95
94
|
return form;
|
|
96
95
|
}
|
|
97
96
|
exports.useFormState = useFormState;
|
|
97
|
+
function setLoading(form, opts) {
|
|
98
|
+
const { loading, init } = opts;
|
|
99
|
+
if (loading !== undefined) {
|
|
100
|
+
// Prefer the explicit/top-level opts.loading if it's set
|
|
101
|
+
form.loading = loading;
|
|
102
|
+
}
|
|
103
|
+
else if (utils_1.isInput(init) && !init.ifUndefined) {
|
|
104
|
+
// Otherwise, check for `init.input`
|
|
105
|
+
form.loading = init.input === undefined;
|
|
106
|
+
}
|
|
107
|
+
else if (utils_1.isQuery(init)) {
|
|
108
|
+
// Or `query.loading`
|
|
109
|
+
form.loading = init.query.loading;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function makeArray(input) {
|
|
113
|
+
return Array.isArray(input) ? input : [input];
|
|
114
|
+
}
|
|
@@ -454,6 +454,21 @@ describe("useFormState", () => {
|
|
|
454
454
|
expect(autoSaveStub).toBeCalledTimes(1);
|
|
455
455
|
expect(autoSaveStub).toBeCalledWith({ id: "a:1", firstName: "first", lastName: "first" });
|
|
456
456
|
});
|
|
457
|
+
it("sets loading if opts.loading is true", async () => {
|
|
458
|
+
const config = { firstName: { type: "value" } };
|
|
459
|
+
function TestComponent({ loading }) {
|
|
460
|
+
const form = useFormState_1.useFormState({ config, loading });
|
|
461
|
+
return jsx_runtime_1.jsx(mobx_react_1.Observer, { children: () => jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "loading" }, { children: String(form.loading) }), void 0) }, void 0);
|
|
462
|
+
}
|
|
463
|
+
// And we initially pass in `init.query.loading: true`
|
|
464
|
+
const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, { loading: true }, void 0));
|
|
465
|
+
// Then the form is marked as loading
|
|
466
|
+
expect(r.loading()).toHaveTextContent("true");
|
|
467
|
+
// And when the query is not loading
|
|
468
|
+
await r.rerender(jsx_runtime_1.jsx(TestComponent, { loading: false }, void 0));
|
|
469
|
+
// Then the form is marked as not loading
|
|
470
|
+
expect(r.loading()).toHaveTextContent("false");
|
|
471
|
+
});
|
|
457
472
|
it("sets loading if input.data is undefined", async () => {
|
|
458
473
|
const config = { firstName: { type: "value" } };
|
|
459
474
|
function TestComponent({ data }) {
|
|
@@ -464,11 +479,26 @@ describe("useFormState", () => {
|
|
|
464
479
|
const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, { data: undefined }, void 0));
|
|
465
480
|
// Then the form is marked as loading
|
|
466
481
|
expect(r.loading()).toHaveTextContent("true");
|
|
467
|
-
// And when the data is
|
|
482
|
+
// And when the data is no longer undefined
|
|
468
483
|
await r.rerender(jsx_runtime_1.jsx(TestComponent, { data: { firstName: "first" } }, void 0));
|
|
469
484
|
// Then the form is marked as not loading
|
|
470
485
|
expect(r.loading()).toHaveTextContent("false");
|
|
471
486
|
});
|
|
487
|
+
it("sets loading if query.loading is true", async () => {
|
|
488
|
+
const config = { firstName: { type: "value" } };
|
|
489
|
+
function TestComponent({ loading, data }) {
|
|
490
|
+
const form = useFormState_1.useFormState({ config, init: { query: { data, loading, error: null }, map: (d) => d } });
|
|
491
|
+
return jsx_runtime_1.jsx(mobx_react_1.Observer, { children: () => jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "loading" }, { children: String(form.loading) }), void 0) }, void 0);
|
|
492
|
+
}
|
|
493
|
+
// And we initially pass in `init.query.loading: true`
|
|
494
|
+
const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, { loading: true, data: undefined }, void 0));
|
|
495
|
+
// Then the form is marked as loading
|
|
496
|
+
expect(r.loading()).toHaveTextContent("true");
|
|
497
|
+
// And when the query is not loading
|
|
498
|
+
await r.rerender(jsx_runtime_1.jsx(TestComponent, { loading: false, data: { firstName: "first" } }, void 0));
|
|
499
|
+
// Then the form is marked as not loading
|
|
500
|
+
expect(r.loading()).toHaveTextContent("false");
|
|
501
|
+
});
|
|
472
502
|
});
|
|
473
503
|
const authorConfig = {
|
|
474
504
|
id: { type: "value" },
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ObjectConfig } from "./config";
|
|
2
|
+
import { InputAndMap, QueryAndMap, UseFormStateOpts } from "./useFormState";
|
|
2
3
|
export declare type Builtin = Date | Function | Uint8Array | string | number | boolean;
|
|
3
4
|
export declare type Primitive = undefined | null | boolean | string | number | Function | Date | {
|
|
4
5
|
toJSON(): any;
|
|
@@ -11,6 +12,8 @@ export declare function fail(message?: string): never;
|
|
|
11
12
|
export declare function assertNever(x: never): never;
|
|
12
13
|
/** Introspects the `init` prop to see if has a `map` function/etc. and returns the form value. */
|
|
13
14
|
export declare function initValue<T>(config: ObjectConfig<T>, init: any): T;
|
|
15
|
+
export declare function isInput<T, I>(init: UseFormStateOpts<T, I>["init"]): init is InputAndMap<T, I>;
|
|
16
|
+
export declare function isQuery<T, I>(init: UseFormStateOpts<T, I>["init"]): init is QueryAndMap<T, I>;
|
|
14
17
|
/**
|
|
15
18
|
* Picks ony fields out of `instance` that are in the `formConfig`.
|
|
16
19
|
*
|
package/dist/utils.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.hasToJSON = exports.areEqual = exports.isEmpty = exports.isNotUndefined = exports.pickFields = exports.initValue = exports.assertNever = exports.fail = void 0;
|
|
6
|
+
exports.hasToJSON = exports.areEqual = exports.isEmpty = exports.isNotUndefined = exports.pickFields = exports.isQuery = exports.isInput = exports.initValue = exports.assertNever = exports.fail = void 0;
|
|
7
7
|
const fast_deep_equal_1 = __importDefault(require("fast-deep-equal"));
|
|
8
8
|
const is_plain_object_1 = require("is-plain-object");
|
|
9
9
|
const mobx_1 = require("mobx");
|
|
@@ -17,14 +17,27 @@ function assertNever(x) {
|
|
|
17
17
|
exports.assertNever = assertNever;
|
|
18
18
|
/** Introspects the `init` prop to see if has a `map` function/etc. and returns the form value. */
|
|
19
19
|
function initValue(config, init) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
let value;
|
|
21
|
+
if (isInput(init)) {
|
|
22
|
+
value = init.input ? init.map(init.input) : init.ifUndefined;
|
|
23
|
+
}
|
|
24
|
+
else if (isQuery(init)) {
|
|
25
|
+
value = init.query.data ? init.map(init.query.data) : init.ifUndefined;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
value = init;
|
|
29
|
+
}
|
|
30
|
+
return pickFields(config, value !== null && value !== void 0 ? value : {});
|
|
26
31
|
}
|
|
27
32
|
exports.initValue = initValue;
|
|
33
|
+
function isInput(init) {
|
|
34
|
+
return !!init && "input" in init && "map" in init;
|
|
35
|
+
}
|
|
36
|
+
exports.isInput = isInput;
|
|
37
|
+
function isQuery(init) {
|
|
38
|
+
return !!init && "query" in init && "map" in init;
|
|
39
|
+
}
|
|
40
|
+
exports.isQuery = isQuery;
|
|
28
41
|
/**
|
|
29
42
|
* Picks ony fields out of `instance` that are in the `formConfig`.
|
|
30
43
|
*
|