@homebound/form-state 2.23.0 → 2.23.2
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/dist/fields/objectField.js +12 -0
- package/dist/useFormState.js +10 -1
- package/dist/useFormState.test.js +29 -0
- package/package.json +1 -1
|
@@ -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
|
package/dist/useFormState.js
CHANGED
|
@@ -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" },
|