@homebound/form-state 2.22.4 → 2.22.5

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.
@@ -73,6 +73,9 @@ function newListFieldState(parentCopy, parentInstance, parentState, key, rules,
73
73
  get dirty() {
74
74
  return this.rows.some((r) => r.dirty) || this.hasChanged();
75
75
  },
76
+ get focused() {
77
+ return this.rows.some((r) => r.focused);
78
+ },
76
79
  get required() {
77
80
  return this.rules.some((rule) => rule === rules_1.required);
78
81
  },
@@ -97,6 +97,9 @@ function newObjectState(config, parentState, parentListState, instance, key, may
97
97
  const readOnlyField = getFields(this).find((f) => f._isReadOnlyKey);
98
98
  return !!(readOnlyField === null || readOnlyField === void 0 ? void 0 : readOnlyField.value);
99
99
  },
100
+ get focused() {
101
+ return getFields(this).some((f) => f.focused);
102
+ },
100
103
  get touched() {
101
104
  return getFields(this).some((f) => f.touched);
102
105
  },
@@ -22,6 +22,7 @@ export interface FieldState<V> {
22
22
  readonly required: boolean;
23
23
  readonly dirty: boolean;
24
24
  readonly valid: boolean;
25
+ readonly focused: boolean;
25
26
  readonly isNewEntity: boolean;
26
27
  rules: Rule<V>[];
27
28
  readonly errors: string[];
@@ -69,6 +70,5 @@ export interface FieldStateInternal<T, V> extends FieldState<V> {
69
70
  _isIdKey: boolean;
70
71
  _isDeleteKey: boolean;
71
72
  _isReadOnlyKey: boolean;
72
- _focused: boolean;
73
73
  }
74
74
  export declare function newValueFieldState<T, K extends keyof T>(parentCopy: T, parentInstance: T, parentState: () => ObjectState<T>, key: K, rules: Rule<T[K] | null | undefined>[], isIdKey: boolean, isDeleteKey: boolean, isReadOnlyKey: boolean, computed: boolean, readOnly: boolean, strictOrder: boolean, maybeAutoSave: () => void): FieldState<T[K] | null | undefined>;
@@ -17,9 +17,9 @@ function newValueFieldState(parentCopy, parentInstance, parentState, key, rules,
17
17
  /** Current readOnly value. */
18
18
  _readOnly: readOnly || false,
19
19
  _loading: false,
20
- _focused: false,
21
20
  // Expose so computed can be skipped in changedValue
22
21
  _computed: computed,
22
+ _focused: false,
23
23
  _isIdKey: isIdKey,
24
24
  _isDeleteKey: isDeleteKey,
25
25
  _isReadOnlyKey: isReadOnlyKey,
@@ -35,6 +35,9 @@ function newValueFieldState(parentCopy, parentInstance, parentState, key, rules,
35
35
  set value(v) {
36
36
  this.set(v);
37
37
  },
38
+ get focused() {
39
+ return this._focused;
40
+ },
38
41
  get dirty() {
39
42
  return !(0, utils_1.areEqual)(this.originalValue, this.value, strictOrder);
40
43
  },
@@ -167,6 +170,16 @@ function newValueFieldState(parentCopy, parentInstance, parentState, key, rules,
167
170
  }
168
171
  },
169
172
  };
173
+ // If we're wrapping a mobx observer, watching for external mutations, i.e. from callers not
174
+ // going through our FieldState.set/FieldState.value setters.
175
+ if ((0, mobx_1.isObservable)(parentInstance)) {
176
+ (0, mobx_1.reaction)(() => parentInstance[key], () => {
177
+ // Don't auto-save on maybe-external mutation if our field, or another other field (potentially a computed),
178
+ // is currently focused, because then it's probably just us doing the writes through `Bound...Field` components.
179
+ if (!parentState().focused)
180
+ maybeAutoSave();
181
+ });
182
+ }
170
183
  return field;
171
184
  }
172
185
  exports.newValueFieldState = newValueFieldState;
@@ -283,6 +283,32 @@ describe("useFormState", () => {
283
283
  expect(r.booksLength.textContent).toEqual("1");
284
284
  expect(r.booksDirty.textContent).toEqual("false");
285
285
  });
286
+ it("can trigger auto-save when underlying observable is changed", async () => {
287
+ // Given a component
288
+ // And it's using a class/mobx proxy as the basis for the data
289
+ class AuthorRow {
290
+ constructor(firstName) {
291
+ this.firstName = firstName;
292
+ (0, mobx_1.makeAutoObservable)(this);
293
+ }
294
+ }
295
+ const autoSave = jest.fn();
296
+ function TestComponent() {
297
+ // And the author starts out with f1
298
+ const author = (0, react_2.useMemo)(() => new AuthorRow("f1"), []);
299
+ const config = (0, react_2.useMemo)(() => ({ firstName: { type: "value" } }), []);
300
+ const form = (0, useFormState_1.useFormState)({ config, init: { input: author, map: (a) => a }, autoSave });
301
+ return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("button", { "data-testid": "changeFirstName", onClick: () => (author.firstName = "f2") }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "clearFirstName", onClick: () => (author.firstName = undefined) })] }));
302
+ }
303
+ // When we change the underlying observable
304
+ const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
305
+ expect(autoSave).toBeCalledTimes(0);
306
+ await (0, rtl_utils_1.clickAndWait)(r.changeFirstName);
307
+ // Then auto save is triggered
308
+ expect(autoSave).toBeCalledTimes(1);
309
+ await (0, rtl_utils_1.clickAndWait)(r.clearFirstName);
310
+ expect(autoSave).toBeCalledTimes(2);
311
+ });
286
312
  it("can trigger auto save for fields in list that were initially undefined", async () => {
287
313
  const autoSave = jest.fn();
288
314
  // Given a component
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/form-state",
3
- "version": "2.22.4",
3
+ "version": "2.22.5",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",