@homebound/form-state 2.15.0 → 2.15.3

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/README.md CHANGED
@@ -59,3 +59,16 @@ See the [sample](https://github.com/homebound-team/form-state/blob/main/src/Form
59
59
  - Add `omitFromValue` so we can have two fields, `book.author.id` / `book.author.name`, where `book.author.name` is used for showing the author name, but `book.author.id` is the only field that is submitted to the server on mutation (maybe pair this with `Ref` based mutations)
60
60
 
61
61
  - Undo/redo would in theory be neat and easy to do on top of the existing infra
62
+
63
+ # Internal Implementation
64
+
65
+ form-state keeps the "actual data" (basically a POJO of your form data) separate from the "mobx proxies that track reactivity" (the `ObjectState` interface with `.get` / `.set` / `.errors` other methods).
66
+
67
+ This works well b/c the "actual data" returned from `ObjectState.value` or `FieldState.value` is always a non-proxy POJO that can be dropped on the wire without causing serialization issues.
68
+
69
+ However, it does mean that form-state internally uses a few "that looks odd" tricks like `_tick.value++` to ensure code like `formState.value.firstName` will be reactive, even though the `.firstName` is not actually a proxy access (but doing `formState.firstName.value` would be).
70
+
71
+ (To be clear, both `formState.firstName.value` and `formState.value.firstName` return the same value, and also have the same reactivity semantics, this is just noting that form-state's internals need to do a few extra tricks to get the latter to be reactive.)
72
+
73
+
74
+
@@ -79,6 +79,13 @@ function AutoSaveStatusProvider({ children, resetToIdleTimeout = 6000 }) {
79
79
  resetToIdleTimeoutRef.current = null;
80
80
  }, resetToIdleTimeout);
81
81
  }, [resetStatus, resetToIdleTimeout, status]);
82
- return (jsx_runtime_1.jsx(exports.AutoSaveStatusContext.Provider, Object.assign({ value: { status, resetStatus, errors, triggerAutoSave, resolveAutoSave } }, { children: children }), void 0));
82
+ const value = react_1.useMemo(() => ({ status, resetStatus, errors, triggerAutoSave, resolveAutoSave }), [
83
+ errors,
84
+ resetStatus,
85
+ resolveAutoSave,
86
+ status,
87
+ triggerAutoSave,
88
+ ]);
89
+ return jsx_runtime_1.jsx(exports.AutoSaveStatusContext.Provider, Object.assign({ value: value }, { children: children }), void 0);
83
90
  }
84
91
  exports.AutoSaveStatusProvider = AutoSaveStatusProvider;
@@ -1,2 +1,2 @@
1
- export { AutoSaveStatusContext, AutoSaveStatusProvider } from "./AutoSaveStatusProvider";
1
+ export { AutoSaveStatus, AutoSaveStatusContext, AutoSaveStatusProvider } from "./AutoSaveStatusProvider";
2
2
  export { useAutoSaveStatus } from "./useAutoSaveStatus";
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useAutoSaveStatus = exports.AutoSaveStatusProvider = exports.AutoSaveStatusContext = void 0;
3
+ exports.useAutoSaveStatus = exports.AutoSaveStatusProvider = exports.AutoSaveStatusContext = exports.AutoSaveStatus = void 0;
4
4
  var AutoSaveStatusProvider_1 = require("./AutoSaveStatusProvider");
5
+ Object.defineProperty(exports, "AutoSaveStatus", { enumerable: true, get: function () { return AutoSaveStatusProvider_1.AutoSaveStatus; } });
5
6
  Object.defineProperty(exports, "AutoSaveStatusContext", { enumerable: true, get: function () { return AutoSaveStatusProvider_1.AutoSaveStatusContext; } });
6
7
  Object.defineProperty(exports, "AutoSaveStatusProvider", { enumerable: true, get: function () { return AutoSaveStatusProvider_1.AutoSaveStatusProvider; } });
7
8
  var useAutoSaveStatus_1 = require("./useAutoSaveStatus");
@@ -9,13 +9,14 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
9
9
  // Keep a map of "item in the parent list" -> "that item's ObjectState"
10
10
  const rowMap = new Map();
11
11
  const _tick = mobx_1.observable({ value: 1 });
12
+ const _childTick = mobx_1.observable({ value: 1 });
12
13
  // this is for dirty checking, not object identity
13
14
  let originalCopy = [...(parentInstance[key] || [])];
14
15
  const list = {
15
16
  key: key,
16
17
  // Our fundamental state of wrapped Us
17
18
  get value() {
18
- return _tick.value > 0 ? parentInstance[key] : utils_1.fail();
19
+ return _tick.value > 0 && _childTick.value > 0 ? parentInstance[key] : utils_1.fail();
19
20
  },
20
21
  _focused: false,
21
22
  _readOnly: false,
@@ -67,7 +68,9 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
67
68
  // transitively register us as a dependency on it
68
69
  if (_tick.value < 0)
69
70
  utils_1.fail();
70
- return (this.value || []).map((child) => {
71
+ // Avoid using `this.value` to avoid registering `_childTick` as a dependency
72
+ const value = parentInstance[key];
73
+ return (value || []).map((child) => {
71
74
  // Because we're reading from this.value, child will be the proxy version
72
75
  let childState = rowMap.get(child);
73
76
  if (!childState) {
@@ -202,9 +205,13 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
202
205
  _tick.value++;
203
206
  },
204
207
  };
205
- return mobx_1.makeAutoObservable(list, {
208
+ const proxy = mobx_1.makeAutoObservable(list, {
206
209
  // See other makeAutoObservable comment
207
210
  value: mobx_1.computed({ equals: () => false }),
208
211
  });
212
+ // Any time a row's value changes, percolate that to our `.value` (so the callers to our
213
+ // `.value` will rerun given the value they saw has deeply changed.)
214
+ mobx_1.reaction(() => proxy.rows.map((r) => r.value), () => _childTick.value++);
215
+ return proxy;
209
216
  }
210
217
  exports.newListFieldState = newListFieldState;
@@ -1153,6 +1153,18 @@ describe("formState", () => {
1153
1153
  formState.firstName.value = "f2";
1154
1154
  expect(ticks).toEqual(1);
1155
1155
  });
1156
+ it("can observe value changes on lists", () => {
1157
+ const formState = objectField_1.createObjectState(authorWithBooksConfig, { books: [{ title: "b1" }] });
1158
+ let ticks = 0;
1159
+ mobx_1.reaction(() => formState.books.value, () => ticks++, { equals: () => false });
1160
+ expect(ticks).toEqual(0);
1161
+ formState.books.rows[0].title.value = "b1";
1162
+ expect(ticks).toEqual(0);
1163
+ formState.books.rows[0].title.value = "b1...";
1164
+ expect(ticks).toEqual(1);
1165
+ formState.books.add({ title: "b2" });
1166
+ expect(ticks).toEqual(2);
1167
+ });
1156
1168
  it("does not override a focused, changed field", () => {
1157
1169
  const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
1158
1170
  // Given first name is focused
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/form-state",
3
- "version": "2.15.0",
3
+ "version": "2.15.3",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",