@homebound/form-state 2.10.1 → 2.11.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.
@@ -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
  },
@@ -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
  },
@@ -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;
@@ -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() {
@@ -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] },
@@ -62,6 +62,9 @@ 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
68
  // The identity of `addRules` is not stable, but assume that it is for better UX.
66
69
  (addRules || (() => { }))(form);
67
70
  firstRunRef.current = true;
@@ -80,6 +83,9 @@ function useFormState(opts) {
80
83
  firstRunRef.current = false;
81
84
  return;
82
85
  }
86
+ if (init && "input" in init && !("ifUndefined" in init)) {
87
+ form.loading = init.input === undefined;
88
+ }
83
89
  form.set(utils_1.initValue(config, init), { refreshing: true });
84
90
  }, [form, ...dep]);
85
91
  // Use useEffect so that we don't touch the form.init proxy during a render
@@ -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,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 input.data is undefined", async () => {
458
+ const config = { firstName: { type: "value" } };
459
+ function TestComponent({ data }) {
460
+ const form = useFormState_1.useFormState({ config, init: { input: data, map: (d) => d } });
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.input: undefined`
464
+ const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, { data: undefined }, void 0));
465
+ // Then the form is marked as loading
466
+ expect(r.loading()).toHaveTextContent("true");
467
+ // And when the data is loading
468
+ await r.rerender(jsx_runtime_1.jsx(TestComponent, { data: { firstName: "first" } }, void 0));
469
+ // Then the form is marked as not loading
470
+ expect(r.loading()).toHaveTextContent("false");
471
+ });
457
472
  });
458
473
  const authorConfig = {
459
474
  id: { type: "value" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/form-state",
3
- "version": "2.10.1",
3
+ "version": "2.11.0",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",