@homebound/form-state 2.23.0 → 2.23.1

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.
@@ -91,6 +91,7 @@ function newObjectState(config, parentState, parentInstance, parentListState, in
91
91
  _kind: "object",
92
92
  _readOnly: false,
93
93
  _loading: false,
94
+ _isAutoSaving: false,
94
95
  _considerDeleted() {
95
96
  const deleteField = getFields(this).find((f) => f._isDeleteKey);
96
97
  return !!(deleteField === null || deleteField === void 0 ? void 0 : deleteField.value);
@@ -169,6 +170,17 @@ function newObjectState(config, parentState, parentInstance, parentListState, in
169
170
  },
170
171
  // Saves all current values into _originalValue
171
172
  commitChanges() {
173
+ if (this._isAutoSaving) {
174
+ // `commitChanges` will mark all WIP changes as committed, which might drop changes if there
175
+ // is an auto-save in-flight, and the user made more changes, that are waiting for their turn
176
+ // on the next auto-save request.
177
+ //
178
+ // Instead of doing a form-wide "all changes are committed", instead autosave forms should use
179
+ // init.map/input to have the server's latest results updated within the form, which will then
180
+ // selectively "un-dirty"/commit the just-saved data, but keep the "still-dirty" WIP data alone.
181
+ throw new Error("When using autoSave, you should not manually call commitChanges, instead have init.map/input update the form state");
182
+ }
183
+ // asdf
172
184
  getFields(this).forEach((f) => f.commitChanges());
173
185
  },
174
186
  // Create a result that is only populated with changed keys
@@ -32,6 +32,10 @@ function useFormState(opts) {
32
32
  pendingAutoSave = true;
33
33
  }
34
34
  else if (isAutoSaving === "queued") {
35
+ // If we've queued up an auto-save, that means we haven't called the `autoSave(...)` function yet, so
36
+ // we assume that the scheduled-but-not-yet-invoked invocation will find both it's original change, and
37
+ // also this potentially new change, and put them both onto the wire as a single operation.
38
+ // So we can just ignore this call.
35
39
  return;
36
40
  }
37
41
  // Don't use canSave() because we don't want to set touched for all the fields
@@ -41,8 +45,12 @@ function useFormState(opts) {
41
45
  // We use setTimeout as a cheap way to wait until the end of the current event listener
42
46
  setTimeout(async () => {
43
47
  try {
48
+ // Tell commitChanges to blow up.
49
+ form._isAutoSaving = true;
44
50
  // We technically don't flip to in-flight until after the call in case the
45
- // user's autoSave function itself wants to call a .set.
51
+ // user's autoSave function itself wants to call a .set (which would call `maybeAutoSave`,
52
+ // and we don't want it to see `isAutoSaving=in-flight` and think it "missed the boat",
53
+ // and so schedule an unnecessary follow-up autosave.)
46
54
  const promise = autoSaveRef.current(form);
47
55
  isAutoSaving = "in-flight";
48
56
  await promise;
@@ -53,6 +61,7 @@ function useFormState(opts) {
53
61
  }
54
62
  finally {
55
63
  isAutoSaving = false;
64
+ form._isAutoSaving = false;
56
65
  if (pendingAutoSave) {
57
66
  pendingAutoSave = false;
58
67
  // Push out the follow-up by 1 tick to allow refreshes to happen to potentially
@@ -539,6 +539,35 @@ describe("useFormState", () => {
539
539
  expect(r.originalValue).toHaveTextContent("a1");
540
540
  expect(r.objectValue).toHaveTextContent("a1");
541
541
  });
542
+ it("doesn't allow calling commitChanges", async () => {
543
+ // Given a component
544
+ const autoSave = jest.fn();
545
+ function TestComponent() {
546
+ const config = authorConfig;
547
+ const form = (0, useFormState_1.useFormState)({
548
+ config,
549
+ init: { firstName: "f1", lastName: "f1" },
550
+ autoSave: async (form) => {
551
+ autoSave(form.changedValue);
552
+ // And the autoSave functions erroneously calls commitChanges
553
+ form.commitChanges();
554
+ },
555
+ });
556
+ return (0, jsx_runtime_1.jsx)("button", { "data-testid": "set", onClick: () => form.firstName.set("f2") });
557
+ }
558
+ const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
559
+ // When we change a field, and try to commit, it blows up
560
+ // await expect(clickAndWait(r.set)).rejects.toThrow(
561
+ // "When using autoSave, you should not manually call commitChanges",
562
+ // );
563
+ //
564
+ // ...for some reason, I think maybe due to rtl-utils calling runOnlyPendingTimers
565
+ // instead of runOnlyPendingTimers async, the `clickAndWait` actually resolves
566
+ // successfully (it's not a rejected promise), and our test technically keeps running,
567
+ // except then somewhere in the guts of Jest, the fake timer infra realizes that a tick
568
+ // failed (our autosave is actually fired from a setTimout tick), and so the Jest test
569
+ // fails anyway, but without anyway for us to catch and do a `.toThrow` assertion.
570
+ });
542
571
  });
543
572
  const authorConfig = {
544
573
  id: { type: "value" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/form-state",
3
- "version": "2.23.0",
3
+ "version": "2.23.1",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",