@homebound/form-state 2.22.3 → 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.
package/dist/fields/listField.js
CHANGED
|
@@ -31,7 +31,9 @@ function newListFieldState(parentCopy, parentInstance, parentState, key, rules,
|
|
|
31
31
|
if (!childState) {
|
|
32
32
|
for (const [, otherState] of rowMap.entries()) {
|
|
33
33
|
if (otherState.isSameEntity(child)) {
|
|
34
|
-
|
|
34
|
+
// If we're being called from `list.rows`, we should be careful to not trigger mutations
|
|
35
|
+
if (!(opts === null || opts === void 0 ? void 0 : opts.skipSet))
|
|
36
|
+
otherState.set(child, opts);
|
|
35
37
|
rowMap.set(child, otherState);
|
|
36
38
|
return otherState;
|
|
37
39
|
}
|
|
@@ -71,6 +73,9 @@ function newListFieldState(parentCopy, parentInstance, parentState, key, rules,
|
|
|
71
73
|
get dirty() {
|
|
72
74
|
return this.rows.some((r) => r.dirty) || this.hasChanged();
|
|
73
75
|
},
|
|
76
|
+
get focused() {
|
|
77
|
+
return this.rows.some((r) => r.focused);
|
|
78
|
+
},
|
|
74
79
|
get required() {
|
|
75
80
|
return this.rules.some((rule) => rule === rules_1.required);
|
|
76
81
|
},
|
|
@@ -100,7 +105,7 @@ function newListFieldState(parentCopy, parentInstance, parentState, key, rules,
|
|
|
100
105
|
(0, utils_1.fail)();
|
|
101
106
|
// Avoid using `this.value` to avoid registering `_childTick` as a dependency
|
|
102
107
|
const value = parentInstance[key];
|
|
103
|
-
return (value || []).map((child) => getOrCreateChildState(child));
|
|
108
|
+
return (value || []).map((child) => getOrCreateChildState(child, { skipSet: true }));
|
|
104
109
|
},
|
|
105
110
|
// TODO Should this be true when all rows are touched?
|
|
106
111
|
get touched() {
|
|
@@ -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;
|
package/dist/formState.test.js
CHANGED
|
@@ -442,6 +442,20 @@ describe("formState", () => {
|
|
|
442
442
|
a1.books.remove(0);
|
|
443
443
|
expect(a1.dirty).toBeFalsy();
|
|
444
444
|
});
|
|
445
|
+
it("handles list fields with duplicate ids", () => {
|
|
446
|
+
// Given an author that incorrectly has duplicate books
|
|
447
|
+
const a1 = createAuthorInputState({
|
|
448
|
+
books: [
|
|
449
|
+
{ id: "b:1", title: "b1" },
|
|
450
|
+
{ id: "b:1", title: "b2" },
|
|
451
|
+
],
|
|
452
|
+
});
|
|
453
|
+
// Then it didn't blow up
|
|
454
|
+
expect(a1.books.dirty).toBeFalsy();
|
|
455
|
+
// And the 1st book won
|
|
456
|
+
expect(a1.books.rows[0].title.value).toBe("b1");
|
|
457
|
+
expect(a1.books.rows[1].title.value).toBe("b1");
|
|
458
|
+
});
|
|
445
459
|
it("calls maybeAutoSave when adding or removing to a list", () => {
|
|
446
460
|
const maybeAutoSave = jest.fn();
|
|
447
461
|
const a1 = createAuthorInputState({ books: [] }, maybeAutoSave);
|
|
@@ -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
|