@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.
@@ -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
  *
@@ -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 && "input" in init && "map" in init ? (Array.isArray(init.input) ? init.input : [init.input]) : [];
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
- if (init && "input" in init && !("ifUndefined" in init)) {
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
- if (init && "input" in init && !("ifUndefined" in init)) {
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
- }, [form, readOnly]);
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 loading
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
- const initValue = init && "input" in init && "map" in init
21
- ? init.input
22
- ? init.map(init.input)
23
- : init.ifUndefined || {}
24
- : init || {};
25
- return pickFields(config, initValue);
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
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/form-state",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",