@homebound/form-state 2.10.1 → 2.13.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.
@@ -52,8 +52,8 @@ function FormStateApp() {
52
52
  jsx_runtime_1.jsx("td", { children: formState.valid.toString() }, void 0),
53
53
  jsx_runtime_1.jsx("td", { children: formState.dirty.toString() }, void 0)] }, void 0) }, void 0)] }), void 0),
54
54
  jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsx("button", Object.assign({ "data-testid": "touch", onClick: () => (formState.touched = !formState.touched) }, { children: "touch" }), void 0),
55
- jsx_runtime_1.jsx("button", Object.assign({ "data-testid": "reset", onClick: () => formState.reset() }, { children: "reset" }), void 0),
56
- jsx_runtime_1.jsx("button", Object.assign({ "data-testid": "save", onClick: () => formState.save() }, { children: "save" }), void 0),
55
+ jsx_runtime_1.jsx("button", Object.assign({ "data-testid": "revertChanges", onClick: () => formState.revertChanges() }, { children: "revert" }), void 0),
56
+ jsx_runtime_1.jsx("button", Object.assign({ "data-testid": "commitChanges", onClick: () => formState.commitChanges() }, { children: "commit" }), void 0),
57
57
  jsx_runtime_1.jsx("button", Object.assign({ "data-testid": "set", onClick: () => formState.set({ firstName: "a2" }) }, { children: "set" }), void 0)] }, void 0)] }, void 0)] }), void 0) }), void 0));
58
58
  } }, void 0));
59
59
  }
@@ -12,7 +12,7 @@ describe("FormStateApp", () => {
12
12
  expect(r.firstName_dirty()).toHaveTextContent("true");
13
13
  react_1.fireEvent.blur(r.firstName());
14
14
  expect(r.firstName_touched()).toHaveTextContent("true");
15
- rtl_utils_1.click(r.save);
15
+ rtl_utils_1.click(r.commitChanges);
16
16
  expect(r.firstName_dirty()).toHaveTextContent("false");
17
17
  expect(r.firstName_touched()).toHaveTextContent("false");
18
18
  });
@@ -20,7 +20,7 @@ describe("FormStateApp", () => {
20
20
  const r = await rtl_utils_1.render(jsx_runtime_1.jsx(FormStateApp_1.FormStateApp, {}, void 0));
21
21
  expect(r.firstName_original()).toHaveTextContent("a1");
22
22
  rtl_utils_1.click(r.set);
23
- rtl_utils_1.click(r.save);
23
+ rtl_utils_1.click(r.commitChanges);
24
24
  expect(r.firstName_original()).toHaveTextContent("a2");
25
25
  });
26
26
  });
@@ -19,12 +19,19 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
19
19
  },
20
20
  _focused: false,
21
21
  _readOnly: false,
22
+ _loading: false,
22
23
  get readOnly() {
23
24
  return this._readOnly || parentState().readOnly;
24
25
  },
25
26
  set readOnly(readOnly) {
26
27
  this._readOnly = readOnly;
27
28
  },
29
+ get loading() {
30
+ return this._loading || parentState().loading;
31
+ },
32
+ set loading(loading) {
33
+ this._loading = loading;
34
+ },
28
35
  set value(v) {
29
36
  this.set(v);
30
37
  },
@@ -177,16 +184,14 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
177
184
  _tick.value++;
178
185
  maybeAutoSave();
179
186
  },
180
- reset() {
187
+ revertChanges() {
181
188
  if (originalCopy) {
182
189
  this.set(originalCopy, { resetting: true });
183
- this.rows.forEach((r) => r.reset());
190
+ this.rows.forEach((r) => r.revertChanges());
184
191
  }
185
192
  },
186
- save() {
187
- this.rows.forEach((r) => {
188
- r.save();
189
- });
193
+ commitChanges() {
194
+ this.rows.forEach((r) => r.commitChanges());
190
195
  originalCopy = [...(parentInstance[key] || [])];
191
196
  _tick.value++;
192
197
  },
@@ -68,6 +68,7 @@ function newObjectState(config, parentState, parentListState, instance, key, may
68
68
  },
69
69
  // private
70
70
  _readOnly: false,
71
+ _loading: false,
71
72
  _considerDeleted() {
72
73
  const deleteField = getFields(this).find((f) => f._isDeleteKey);
73
74
  return !!(deleteField === null || deleteField === void 0 ? void 0 : deleteField.value);
@@ -91,6 +92,12 @@ function newObjectState(config, parentState, parentListState, instance, key, may
91
92
  set readOnly(readOnly) {
92
93
  this._readOnly = readOnly;
93
94
  },
95
+ get loading() {
96
+ return this._loading || (parentState && parentState().loading) || !!(parentListState === null || parentListState === void 0 ? void 0 : parentListState.loading);
97
+ },
98
+ set loading(loading) {
99
+ this._loading = loading;
100
+ },
94
101
  get valid() {
95
102
  return getFields(this).every((f) => f.valid);
96
103
  },
@@ -125,12 +132,12 @@ function newObjectState(config, parentState, parentListState, instance, key, may
125
132
  });
126
133
  },
127
134
  // Resets all fields back to their original values
128
- reset() {
129
- getFields(this).forEach((f) => f.reset());
135
+ revertChanges() {
136
+ getFields(this).forEach((f) => f.revertChanges());
130
137
  },
131
138
  // Saves all current values into _originalValue
132
- save() {
133
- getFields(this).forEach((f) => f.save());
139
+ commitChanges() {
140
+ getFields(this).forEach((f) => f.commitChanges());
134
141
  },
135
142
  // Create a result that is only populated with changed keys
136
143
  get changedValue() {
@@ -16,6 +16,7 @@ export interface FieldState<T, V> {
16
16
  readonly originalValue: V;
17
17
  touched: boolean;
18
18
  readOnly: boolean;
19
+ loading: boolean;
19
20
  readonly required: boolean;
20
21
  readonly dirty: boolean;
21
22
  readonly valid: boolean;
@@ -32,9 +33,9 @@ export interface FieldState<T, V> {
32
33
  maybeAutoSave(): void;
33
34
  set(value: V, opts?: SetOpts): void;
34
35
  /** Reverts back to the original value and resets dirty/touched. */
35
- reset(): void;
36
+ revertChanges(): void;
36
37
  /** Accepts the current changed value (if any) as the original and resets dirty/touched. */
37
- save(): void;
38
+ commitChanges(): void;
38
39
  }
39
40
  /** Public options for our `set` command. */
40
41
  export interface SetOpts {
@@ -18,6 +18,7 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
18
18
  touched: false,
19
19
  /** Current readOnly value. */
20
20
  _readOnly: readOnly || false,
21
+ _loading: false,
21
22
  _focused: false,
22
23
  _isIdKey: isIdKey,
23
24
  _isDeleteKey: isDeleteKey,
@@ -40,9 +41,17 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
40
41
  get readOnly() {
41
42
  return parentState().readOnly || this._readOnly;
42
43
  },
43
- /** Sets the field readOnly, but `v` will still be or-d with the parent's readOnly state. */
44
- set readOnly(v) {
45
- this._readOnly = v;
44
+ /** Sets the field readOnly, but `loading` will still be or-d with the parent's readOnly state. */
45
+ set readOnly(readOnly) {
46
+ this._readOnly = readOnly;
47
+ },
48
+ /** Returns whether this field is loading, or if our parent is loading. */
49
+ get loading() {
50
+ return parentState().loading || this._loading;
51
+ },
52
+ /** Sets the field loading, but `loading` will still be or-d with the parent's loading state. */
53
+ set loading(loading) {
54
+ this._loading = loading;
46
55
  },
47
56
  // For primitive fields, the changed value is just the value.
48
57
  get changedValue() {
@@ -109,13 +118,13 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
109
118
  this.maybeAutoSave();
110
119
  }
111
120
  },
112
- reset() {
121
+ revertChanges() {
113
122
  if (!computed) {
114
123
  this.set(this.originalValue, { resetting: true });
115
124
  }
116
125
  this.touched = false;
117
126
  },
118
- save() {
127
+ commitChanges() {
119
128
  if (is_plain_object_1.isPlainObject(this.originalValue)) {
120
129
  this.originalValue = mobx_1.toJS(this.value);
121
130
  }
@@ -452,7 +452,7 @@ describe("formState", () => {
452
452
  a1.set({ firstName: "first" });
453
453
  expect(maybeAutoSave).toBeCalledTimes(1);
454
454
  // When we reset
455
- a1.reset();
455
+ a1.revertChanges();
456
456
  // We don't call blur again
457
457
  expect(maybeAutoSave).toBeCalledTimes(1);
458
458
  });
@@ -541,7 +541,7 @@ describe("formState", () => {
541
541
  a1.books.add({ title: "b3" });
542
542
  expect(a1.books.touched).toEqual(true);
543
543
  expect(a1.dirty).toBeTruthy();
544
- a1.reset();
544
+ a1.revertChanges();
545
545
  expect(a1.firstName.value).toBe("a1");
546
546
  expect(a1.firstName.touched).toBeFalsy();
547
547
  expect(a1.lastName.value).toBe("aL1");
@@ -577,7 +577,7 @@ describe("formState", () => {
577
577
  // And the "readOnly=true" operation has "beat" the reset operation
578
578
  a1.readOnly = true;
579
579
  // When we reset
580
- a1.reset();
580
+ a1.revertChanges();
581
581
  // Then it still works
582
582
  expect(a1.firstName.value).toBe("a1");
583
583
  expect(a1.firstName.touched).toBeFalsy();
@@ -607,15 +607,15 @@ describe("formState", () => {
607
607
  a1.books.rows[1].set({ title: "bb3" });
608
608
  // verify ValueFieldState is dirty, then save, then no longer dirty.
609
609
  expect(a1.firstName.dirty).toBeTruthy();
610
- a1.firstName.save();
610
+ a1.firstName.commitChanges();
611
611
  expect(a1.firstName.dirty).toBeFalsy();
612
612
  // verify ListFieldState is dirty, then save, then no longer dirty.
613
613
  expect(a1.books.dirty).toBeTruthy();
614
- a1.books.save();
614
+ a1.books.commitChanges();
615
615
  expect(a1.books.dirty).toBeFalsy();
616
616
  // Verify the remaining form is still dirty
617
617
  expect(a1.dirty).toBeTruthy();
618
- a1.save();
618
+ a1.commitChanges();
619
619
  // Verify after save the whole form is no longer dirty.
620
620
  expect(a1.dirty).toBeFalsy();
621
621
  });
@@ -696,7 +696,7 @@ describe("formState", () => {
696
696
  });
697
697
  // And it is read-only
698
698
  a1.readOnly = true;
699
- // Then it's fields are read-only
699
+ // Then its fields are read-only
700
700
  const fields = [a1, a1.firstName, a1.books, a1.books.rows[0].title, a1.books.rows[0].classification];
701
701
  fields.forEach((f) => expect(f.readOnly).toBeTruthy());
702
702
  // And even if a specific field tries to _not_ be read-only,
@@ -715,7 +715,7 @@ describe("formState", () => {
715
715
  });
716
716
  // And the top-level form is explicitly set to read-only=false
717
717
  a1.readOnly = false;
718
- // Then it's fields are not read-only
718
+ // Then its fields are not read-only
719
719
  const fields = [a1, a1.firstName, a1.books, a1.books.rows[0].title, a1.books.rows[0].classification];
720
720
  fields.forEach((f) => expect(f.readOnly).toBeFalsy());
721
721
  // But when one of the fields opts in to readOnly
@@ -723,6 +723,40 @@ describe("formState", () => {
723
723
  // Then the field-level rule is respected
724
724
  expect(a1.firstName.readOnly).toBeTruthy();
725
725
  });
726
+ it("has an object-level loading=true override field-level loading=false", () => {
727
+ // Given a top-level object
728
+ const a1 = createAuthorInputState({
729
+ firstName: "a1",
730
+ lastName: "aL1",
731
+ books: [{ title: "b1", classification: formStateDomain_1.dd100 }],
732
+ });
733
+ // And it is loading
734
+ a1.loading = true;
735
+ // Then its fields are read-only
736
+ const fields = [a1, a1.firstName, a1.books, a1.books.rows[0].title, a1.books.rows[0].classification];
737
+ fields.forEach((f) => expect(f.loading).toBeTruthy());
738
+ // And even if a specific field tries to _not_ be loading
739
+ a1.firstName.loading = false;
740
+ // Then the field-level value is ignored, and it's still treated as loading
741
+ expect(a1.firstName.loading).toBeTruthy();
742
+ });
743
+ it("has an field-level loading=true override object-level loading=false", () => {
744
+ // Given a top-level object
745
+ const a1 = createAuthorInputState({
746
+ firstName: "a1",
747
+ lastName: "aL1",
748
+ books: [{ title: "b1", classification: formStateDomain_1.dd100 }],
749
+ });
750
+ // And the top-level form is explicitly set to loading=false
751
+ a1.loading = false;
752
+ // Then its fields are not loading
753
+ const fields = [a1, a1.firstName, a1.books, a1.books.rows[0].title, a1.books.rows[0].classification];
754
+ fields.forEach((f) => expect(f.loading).toBeFalsy());
755
+ // But when one of the fields opts in to loading
756
+ a1.firstName.loading = true;
757
+ // Then the field-level loading is respected
758
+ expect(a1.firstName.loading).toBeTruthy();
759
+ });
726
760
  it("canSave returns dirty and touches", () => {
727
761
  const a1 = objectField_1.createObjectState({
728
762
  firstName: { type: "value", rules: [rules_1.required] },
@@ -805,7 +839,7 @@ describe("formState", () => {
805
839
  expect(a.books.rows[1].readOnly).toBeFalsy();
806
840
  expect(a.books.rows[1].title.readOnly).toBeFalsy();
807
841
  // And reset does not blow up
808
- a.reset();
842
+ a.revertChanges();
809
843
  });
810
844
  it("can set nested values when original null", () => {
811
845
  const a = createAuthorWithAddressInputState({ address: null });
@@ -860,7 +894,7 @@ describe("formState", () => {
860
894
  expect(formState.fullName.value).toEqual("first last");
861
895
  formState.firstName.value = "change";
862
896
  expect(formState.fullName.value).toEqual("change last");
863
- formState.reset();
897
+ formState.revertChanges();
864
898
  expect(formState.firstName.value).toEqual("first");
865
899
  });
866
900
  it("can set computeds that have setters", () => {
@@ -873,7 +907,7 @@ describe("formState", () => {
873
907
  expect(formState.firstName.value).toEqual("Bob");
874
908
  formState.fullName.set("Fred Smith");
875
909
  expect(formState.firstName.value).toEqual("Fred");
876
- formState.reset();
910
+ formState.revertChanges();
877
911
  expect(formState.firstName.value).toEqual("first");
878
912
  });
879
913
  it("can mark fields as read only", () => {
@@ -886,7 +920,7 @@ describe("formState", () => {
886
920
  expect(formState.fullName.readOnly).toEqual(true);
887
921
  formState.firstName.value = "change";
888
922
  expect(formState.fullName.value).toEqual("change last");
889
- formState.reset();
923
+ formState.revertChanges();
890
924
  expect(formState.firstName.value).toEqual("first");
891
925
  });
892
926
  it("lets rules validate against other fields", () => {
@@ -1266,7 +1300,7 @@ describe("formState", () => {
1266
1300
  // Then the list state is dirty
1267
1301
  expect(a1.books.dirty).toBeTruthy();
1268
1302
  // When saving the form state
1269
- a1.save();
1303
+ a1.commitChanges();
1270
1304
  // Then the list is no longer dirty
1271
1305
  expect(a1.books.dirty).toBeFalsy();
1272
1306
  // When adding a new book
@@ -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,6 +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
+ setLoading(form, opts);
65
66
  // The identity of `addRules` is not stable, but assume that it is for better UX.
66
67
  (addRules || (() => { }))(form);
67
68
  firstRunRef.current = true;
@@ -80,12 +81,34 @@ function useFormState(opts) {
80
81
  firstRunRef.current = false;
81
82
  return;
82
83
  }
84
+ setLoading(form, opts);
83
85
  form.set(utils_1.initValue(config, init), { refreshing: true });
84
86
  }, [form, ...dep]);
85
87
  // Use useEffect so that we don't touch the form.init proxy during a render
86
88
  react_1.useEffect(() => {
87
89
  form.readOnly = readOnly;
88
- }, [form, readOnly]);
90
+ if (loading !== undefined) {
91
+ form.loading = loading;
92
+ }
93
+ }, [form, readOnly, loading]);
89
94
  return form;
90
95
  }
91
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
+ }
@@ -15,10 +15,10 @@ describe("useFormState", () => {
15
15
  function TestComponent() {
16
16
  const config = { firstName: { type: "value" } };
17
17
  // And we have query data that may or may not be defined
18
- const data = Math.random() >= 0 ? { firstName: "bob" } : undefined;
19
- // Then the lambda is passed the "de-undefined" data
18
+ const data = { firstName: "bob" };
20
19
  const form = useFormState_1.useFormState({
21
20
  config,
21
+ // Then the lambda is passed the "de-undefined" data, i.e. `d.firstName` is not a compile error
22
22
  init: { input: data, map: (d) => ({ firstName: d.firstName }) },
23
23
  });
24
24
  return jsx_runtime_1.jsx("div", { children: form.firstName.value }, void 0);
@@ -454,6 +454,51 @@ 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
+ });
472
+ it("sets loading if input.data is undefined", async () => {
473
+ const config = { firstName: { type: "value" } };
474
+ function TestComponent({ data }) {
475
+ const form = useFormState_1.useFormState({ config, init: { input: data, map: (d) => d } });
476
+ 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);
477
+ }
478
+ // And we initially pass in `init.input: undefined`
479
+ const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, { data: undefined }, void 0));
480
+ // Then the form is marked as loading
481
+ expect(r.loading()).toHaveTextContent("true");
482
+ // And when the data is no longer undefined
483
+ await r.rerender(jsx_runtime_1.jsx(TestComponent, { data: { firstName: "first" } }, void 0));
484
+ // Then the form is marked as not loading
485
+ expect(r.loading()).toHaveTextContent("false");
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
+ });
457
502
  });
458
503
  const authorConfig = {
459
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.10.1",
3
+ "version": "2.13.0",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",