@homebound/form-state 2.10.1 → 2.13.0
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/FormStateApp.js +2 -2
- package/dist/FormStateApp.test.js +2 -2
- package/dist/fields/listField.js +11 -6
- package/dist/fields/objectField.js +11 -4
- package/dist/fields/valueField.d.ts +3 -2
- package/dist/fields/valueField.js +14 -5
- package/dist/formState.test.js +47 -13
- package/dist/useFormState.d.ts +23 -5
- package/dist/useFormState.js +26 -3
- package/dist/useFormState.test.js +47 -2
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +20 -7
- package/package.json +1 -1
package/dist/FormStateApp.js
CHANGED
|
@@ -52,8 +52,8 @@ function FormStateApp() {
|
|
|
52
52
|
jsx_runtime_1.jsx("td", { children: formState.valid.toString() }, void 0),
|
|
53
53
|
jsx_runtime_1.jsx("td", { children: formState.dirty.toString() }, void 0)] }, void 0) }, void 0)] }), void 0),
|
|
54
54
|
jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsx("button", Object.assign({ "data-testid": "touch", onClick: () => (formState.touched = !formState.touched) }, { children: "touch" }), void 0),
|
|
55
|
-
jsx_runtime_1.jsx("button", Object.assign({ "data-testid": "
|
|
56
|
-
jsx_runtime_1.jsx("button", Object.assign({ "data-testid": "
|
|
55
|
+
jsx_runtime_1.jsx("button", Object.assign({ "data-testid": "revertChanges", onClick: () => formState.revertChanges() }, { children: "revert" }), void 0),
|
|
56
|
+
jsx_runtime_1.jsx("button", Object.assign({ "data-testid": "commitChanges", onClick: () => formState.commitChanges() }, { children: "commit" }), void 0),
|
|
57
57
|
jsx_runtime_1.jsx("button", Object.assign({ "data-testid": "set", onClick: () => formState.set({ firstName: "a2" }) }, { children: "set" }), void 0)] }, void 0)] }, void 0)] }), void 0) }), void 0));
|
|
58
58
|
} }, void 0));
|
|
59
59
|
}
|
|
@@ -12,7 +12,7 @@ describe("FormStateApp", () => {
|
|
|
12
12
|
expect(r.firstName_dirty()).toHaveTextContent("true");
|
|
13
13
|
react_1.fireEvent.blur(r.firstName());
|
|
14
14
|
expect(r.firstName_touched()).toHaveTextContent("true");
|
|
15
|
-
rtl_utils_1.click(r.
|
|
15
|
+
rtl_utils_1.click(r.commitChanges);
|
|
16
16
|
expect(r.firstName_dirty()).toHaveTextContent("false");
|
|
17
17
|
expect(r.firstName_touched()).toHaveTextContent("false");
|
|
18
18
|
});
|
|
@@ -20,7 +20,7 @@ describe("FormStateApp", () => {
|
|
|
20
20
|
const r = await rtl_utils_1.render(jsx_runtime_1.jsx(FormStateApp_1.FormStateApp, {}, void 0));
|
|
21
21
|
expect(r.firstName_original()).toHaveTextContent("a1");
|
|
22
22
|
rtl_utils_1.click(r.set);
|
|
23
|
-
rtl_utils_1.click(r.
|
|
23
|
+
rtl_utils_1.click(r.commitChanges);
|
|
24
24
|
expect(r.firstName_original()).toHaveTextContent("a2");
|
|
25
25
|
});
|
|
26
26
|
});
|
package/dist/fields/listField.js
CHANGED
|
@@ -19,12 +19,19 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
19
19
|
},
|
|
20
20
|
_focused: false,
|
|
21
21
|
_readOnly: false,
|
|
22
|
+
_loading: false,
|
|
22
23
|
get readOnly() {
|
|
23
24
|
return this._readOnly || parentState().readOnly;
|
|
24
25
|
},
|
|
25
26
|
set readOnly(readOnly) {
|
|
26
27
|
this._readOnly = readOnly;
|
|
27
28
|
},
|
|
29
|
+
get loading() {
|
|
30
|
+
return this._loading || parentState().loading;
|
|
31
|
+
},
|
|
32
|
+
set loading(loading) {
|
|
33
|
+
this._loading = loading;
|
|
34
|
+
},
|
|
28
35
|
set value(v) {
|
|
29
36
|
this.set(v);
|
|
30
37
|
},
|
|
@@ -177,16 +184,14 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
177
184
|
_tick.value++;
|
|
178
185
|
maybeAutoSave();
|
|
179
186
|
},
|
|
180
|
-
|
|
187
|
+
revertChanges() {
|
|
181
188
|
if (originalCopy) {
|
|
182
189
|
this.set(originalCopy, { resetting: true });
|
|
183
|
-
this.rows.forEach((r) => r.
|
|
190
|
+
this.rows.forEach((r) => r.revertChanges());
|
|
184
191
|
}
|
|
185
192
|
},
|
|
186
|
-
|
|
187
|
-
this.rows.forEach((r) =>
|
|
188
|
-
r.save();
|
|
189
|
-
});
|
|
193
|
+
commitChanges() {
|
|
194
|
+
this.rows.forEach((r) => r.commitChanges());
|
|
190
195
|
originalCopy = [...(parentInstance[key] || [])];
|
|
191
196
|
_tick.value++;
|
|
192
197
|
},
|
|
@@ -68,6 +68,7 @@ function newObjectState(config, parentState, parentListState, instance, key, may
|
|
|
68
68
|
},
|
|
69
69
|
// private
|
|
70
70
|
_readOnly: false,
|
|
71
|
+
_loading: false,
|
|
71
72
|
_considerDeleted() {
|
|
72
73
|
const deleteField = getFields(this).find((f) => f._isDeleteKey);
|
|
73
74
|
return !!(deleteField === null || deleteField === void 0 ? void 0 : deleteField.value);
|
|
@@ -91,6 +92,12 @@ function newObjectState(config, parentState, parentListState, instance, key, may
|
|
|
91
92
|
set readOnly(readOnly) {
|
|
92
93
|
this._readOnly = readOnly;
|
|
93
94
|
},
|
|
95
|
+
get loading() {
|
|
96
|
+
return this._loading || (parentState && parentState().loading) || !!(parentListState === null || parentListState === void 0 ? void 0 : parentListState.loading);
|
|
97
|
+
},
|
|
98
|
+
set loading(loading) {
|
|
99
|
+
this._loading = loading;
|
|
100
|
+
},
|
|
94
101
|
get valid() {
|
|
95
102
|
return getFields(this).every((f) => f.valid);
|
|
96
103
|
},
|
|
@@ -125,12 +132,12 @@ function newObjectState(config, parentState, parentListState, instance, key, may
|
|
|
125
132
|
});
|
|
126
133
|
},
|
|
127
134
|
// Resets all fields back to their original values
|
|
128
|
-
|
|
129
|
-
getFields(this).forEach((f) => f.
|
|
135
|
+
revertChanges() {
|
|
136
|
+
getFields(this).forEach((f) => f.revertChanges());
|
|
130
137
|
},
|
|
131
138
|
// Saves all current values into _originalValue
|
|
132
|
-
|
|
133
|
-
getFields(this).forEach((f) => f.
|
|
139
|
+
commitChanges() {
|
|
140
|
+
getFields(this).forEach((f) => f.commitChanges());
|
|
134
141
|
},
|
|
135
142
|
// Create a result that is only populated with changed keys
|
|
136
143
|
get changedValue() {
|
|
@@ -16,6 +16,7 @@ export interface FieldState<T, V> {
|
|
|
16
16
|
readonly originalValue: V;
|
|
17
17
|
touched: boolean;
|
|
18
18
|
readOnly: boolean;
|
|
19
|
+
loading: boolean;
|
|
19
20
|
readonly required: boolean;
|
|
20
21
|
readonly dirty: boolean;
|
|
21
22
|
readonly valid: boolean;
|
|
@@ -32,9 +33,9 @@ export interface FieldState<T, V> {
|
|
|
32
33
|
maybeAutoSave(): void;
|
|
33
34
|
set(value: V, opts?: SetOpts): void;
|
|
34
35
|
/** Reverts back to the original value and resets dirty/touched. */
|
|
35
|
-
|
|
36
|
+
revertChanges(): void;
|
|
36
37
|
/** Accepts the current changed value (if any) as the original and resets dirty/touched. */
|
|
37
|
-
|
|
38
|
+
commitChanges(): void;
|
|
38
39
|
}
|
|
39
40
|
/** Public options for our `set` command. */
|
|
40
41
|
export interface SetOpts {
|
|
@@ -18,6 +18,7 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
|
|
|
18
18
|
touched: false,
|
|
19
19
|
/** Current readOnly value. */
|
|
20
20
|
_readOnly: readOnly || false,
|
|
21
|
+
_loading: false,
|
|
21
22
|
_focused: false,
|
|
22
23
|
_isIdKey: isIdKey,
|
|
23
24
|
_isDeleteKey: isDeleteKey,
|
|
@@ -40,9 +41,17 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
|
|
|
40
41
|
get readOnly() {
|
|
41
42
|
return parentState().readOnly || this._readOnly;
|
|
42
43
|
},
|
|
43
|
-
/** Sets the field readOnly, but `
|
|
44
|
-
set readOnly(
|
|
45
|
-
this._readOnly =
|
|
44
|
+
/** Sets the field readOnly, but `loading` will still be or-d with the parent's readOnly state. */
|
|
45
|
+
set readOnly(readOnly) {
|
|
46
|
+
this._readOnly = readOnly;
|
|
47
|
+
},
|
|
48
|
+
/** Returns whether this field is loading, or if our parent is loading. */
|
|
49
|
+
get loading() {
|
|
50
|
+
return parentState().loading || this._loading;
|
|
51
|
+
},
|
|
52
|
+
/** Sets the field loading, but `loading` will still be or-d with the parent's loading state. */
|
|
53
|
+
set loading(loading) {
|
|
54
|
+
this._loading = loading;
|
|
46
55
|
},
|
|
47
56
|
// For primitive fields, the changed value is just the value.
|
|
48
57
|
get changedValue() {
|
|
@@ -109,13 +118,13 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
|
|
|
109
118
|
this.maybeAutoSave();
|
|
110
119
|
}
|
|
111
120
|
},
|
|
112
|
-
|
|
121
|
+
revertChanges() {
|
|
113
122
|
if (!computed) {
|
|
114
123
|
this.set(this.originalValue, { resetting: true });
|
|
115
124
|
}
|
|
116
125
|
this.touched = false;
|
|
117
126
|
},
|
|
118
|
-
|
|
127
|
+
commitChanges() {
|
|
119
128
|
if (is_plain_object_1.isPlainObject(this.originalValue)) {
|
|
120
129
|
this.originalValue = mobx_1.toJS(this.value);
|
|
121
130
|
}
|
package/dist/formState.test.js
CHANGED
|
@@ -452,7 +452,7 @@ describe("formState", () => {
|
|
|
452
452
|
a1.set({ firstName: "first" });
|
|
453
453
|
expect(maybeAutoSave).toBeCalledTimes(1);
|
|
454
454
|
// When we reset
|
|
455
|
-
a1.
|
|
455
|
+
a1.revertChanges();
|
|
456
456
|
// We don't call blur again
|
|
457
457
|
expect(maybeAutoSave).toBeCalledTimes(1);
|
|
458
458
|
});
|
|
@@ -541,7 +541,7 @@ describe("formState", () => {
|
|
|
541
541
|
a1.books.add({ title: "b3" });
|
|
542
542
|
expect(a1.books.touched).toEqual(true);
|
|
543
543
|
expect(a1.dirty).toBeTruthy();
|
|
544
|
-
a1.
|
|
544
|
+
a1.revertChanges();
|
|
545
545
|
expect(a1.firstName.value).toBe("a1");
|
|
546
546
|
expect(a1.firstName.touched).toBeFalsy();
|
|
547
547
|
expect(a1.lastName.value).toBe("aL1");
|
|
@@ -577,7 +577,7 @@ describe("formState", () => {
|
|
|
577
577
|
// And the "readOnly=true" operation has "beat" the reset operation
|
|
578
578
|
a1.readOnly = true;
|
|
579
579
|
// When we reset
|
|
580
|
-
a1.
|
|
580
|
+
a1.revertChanges();
|
|
581
581
|
// Then it still works
|
|
582
582
|
expect(a1.firstName.value).toBe("a1");
|
|
583
583
|
expect(a1.firstName.touched).toBeFalsy();
|
|
@@ -607,15 +607,15 @@ describe("formState", () => {
|
|
|
607
607
|
a1.books.rows[1].set({ title: "bb3" });
|
|
608
608
|
// verify ValueFieldState is dirty, then save, then no longer dirty.
|
|
609
609
|
expect(a1.firstName.dirty).toBeTruthy();
|
|
610
|
-
a1.firstName.
|
|
610
|
+
a1.firstName.commitChanges();
|
|
611
611
|
expect(a1.firstName.dirty).toBeFalsy();
|
|
612
612
|
// verify ListFieldState is dirty, then save, then no longer dirty.
|
|
613
613
|
expect(a1.books.dirty).toBeTruthy();
|
|
614
|
-
a1.books.
|
|
614
|
+
a1.books.commitChanges();
|
|
615
615
|
expect(a1.books.dirty).toBeFalsy();
|
|
616
616
|
// Verify the remaining form is still dirty
|
|
617
617
|
expect(a1.dirty).toBeTruthy();
|
|
618
|
-
a1.
|
|
618
|
+
a1.commitChanges();
|
|
619
619
|
// Verify after save the whole form is no longer dirty.
|
|
620
620
|
expect(a1.dirty).toBeFalsy();
|
|
621
621
|
});
|
|
@@ -696,7 +696,7 @@ describe("formState", () => {
|
|
|
696
696
|
});
|
|
697
697
|
// And it is read-only
|
|
698
698
|
a1.readOnly = true;
|
|
699
|
-
// Then
|
|
699
|
+
// Then its fields are read-only
|
|
700
700
|
const fields = [a1, a1.firstName, a1.books, a1.books.rows[0].title, a1.books.rows[0].classification];
|
|
701
701
|
fields.forEach((f) => expect(f.readOnly).toBeTruthy());
|
|
702
702
|
// And even if a specific field tries to _not_ be read-only,
|
|
@@ -715,7 +715,7 @@ describe("formState", () => {
|
|
|
715
715
|
});
|
|
716
716
|
// And the top-level form is explicitly set to read-only=false
|
|
717
717
|
a1.readOnly = false;
|
|
718
|
-
// Then
|
|
718
|
+
// Then its fields are not read-only
|
|
719
719
|
const fields = [a1, a1.firstName, a1.books, a1.books.rows[0].title, a1.books.rows[0].classification];
|
|
720
720
|
fields.forEach((f) => expect(f.readOnly).toBeFalsy());
|
|
721
721
|
// But when one of the fields opts in to readOnly
|
|
@@ -723,6 +723,40 @@ describe("formState", () => {
|
|
|
723
723
|
// Then the field-level rule is respected
|
|
724
724
|
expect(a1.firstName.readOnly).toBeTruthy();
|
|
725
725
|
});
|
|
726
|
+
it("has an object-level loading=true override field-level loading=false", () => {
|
|
727
|
+
// Given a top-level object
|
|
728
|
+
const a1 = createAuthorInputState({
|
|
729
|
+
firstName: "a1",
|
|
730
|
+
lastName: "aL1",
|
|
731
|
+
books: [{ title: "b1", classification: formStateDomain_1.dd100 }],
|
|
732
|
+
});
|
|
733
|
+
// And it is loading
|
|
734
|
+
a1.loading = true;
|
|
735
|
+
// Then its fields are read-only
|
|
736
|
+
const fields = [a1, a1.firstName, a1.books, a1.books.rows[0].title, a1.books.rows[0].classification];
|
|
737
|
+
fields.forEach((f) => expect(f.loading).toBeTruthy());
|
|
738
|
+
// And even if a specific field tries to _not_ be loading
|
|
739
|
+
a1.firstName.loading = false;
|
|
740
|
+
// Then the field-level value is ignored, and it's still treated as loading
|
|
741
|
+
expect(a1.firstName.loading).toBeTruthy();
|
|
742
|
+
});
|
|
743
|
+
it("has an field-level loading=true override object-level loading=false", () => {
|
|
744
|
+
// Given a top-level object
|
|
745
|
+
const a1 = createAuthorInputState({
|
|
746
|
+
firstName: "a1",
|
|
747
|
+
lastName: "aL1",
|
|
748
|
+
books: [{ title: "b1", classification: formStateDomain_1.dd100 }],
|
|
749
|
+
});
|
|
750
|
+
// And the top-level form is explicitly set to loading=false
|
|
751
|
+
a1.loading = false;
|
|
752
|
+
// Then its fields are not loading
|
|
753
|
+
const fields = [a1, a1.firstName, a1.books, a1.books.rows[0].title, a1.books.rows[0].classification];
|
|
754
|
+
fields.forEach((f) => expect(f.loading).toBeFalsy());
|
|
755
|
+
// But when one of the fields opts in to loading
|
|
756
|
+
a1.firstName.loading = true;
|
|
757
|
+
// Then the field-level loading is respected
|
|
758
|
+
expect(a1.firstName.loading).toBeTruthy();
|
|
759
|
+
});
|
|
726
760
|
it("canSave returns dirty and touches", () => {
|
|
727
761
|
const a1 = objectField_1.createObjectState({
|
|
728
762
|
firstName: { type: "value", rules: [rules_1.required] },
|
|
@@ -805,7 +839,7 @@ describe("formState", () => {
|
|
|
805
839
|
expect(a.books.rows[1].readOnly).toBeFalsy();
|
|
806
840
|
expect(a.books.rows[1].title.readOnly).toBeFalsy();
|
|
807
841
|
// And reset does not blow up
|
|
808
|
-
a.
|
|
842
|
+
a.revertChanges();
|
|
809
843
|
});
|
|
810
844
|
it("can set nested values when original null", () => {
|
|
811
845
|
const a = createAuthorWithAddressInputState({ address: null });
|
|
@@ -860,7 +894,7 @@ describe("formState", () => {
|
|
|
860
894
|
expect(formState.fullName.value).toEqual("first last");
|
|
861
895
|
formState.firstName.value = "change";
|
|
862
896
|
expect(formState.fullName.value).toEqual("change last");
|
|
863
|
-
formState.
|
|
897
|
+
formState.revertChanges();
|
|
864
898
|
expect(formState.firstName.value).toEqual("first");
|
|
865
899
|
});
|
|
866
900
|
it("can set computeds that have setters", () => {
|
|
@@ -873,7 +907,7 @@ describe("formState", () => {
|
|
|
873
907
|
expect(formState.firstName.value).toEqual("Bob");
|
|
874
908
|
formState.fullName.set("Fred Smith");
|
|
875
909
|
expect(formState.firstName.value).toEqual("Fred");
|
|
876
|
-
formState.
|
|
910
|
+
formState.revertChanges();
|
|
877
911
|
expect(formState.firstName.value).toEqual("first");
|
|
878
912
|
});
|
|
879
913
|
it("can mark fields as read only", () => {
|
|
@@ -886,7 +920,7 @@ describe("formState", () => {
|
|
|
886
920
|
expect(formState.fullName.readOnly).toEqual(true);
|
|
887
921
|
formState.firstName.value = "change";
|
|
888
922
|
expect(formState.fullName.value).toEqual("change last");
|
|
889
|
-
formState.
|
|
923
|
+
formState.revertChanges();
|
|
890
924
|
expect(formState.firstName.value).toEqual("first");
|
|
891
925
|
});
|
|
892
926
|
it("lets rules validate against other fields", () => {
|
|
@@ -1266,7 +1300,7 @@ describe("formState", () => {
|
|
|
1266
1300
|
// Then the list state is dirty
|
|
1267
1301
|
expect(a1.books.dirty).toBeTruthy();
|
|
1268
1302
|
// When saving the form state
|
|
1269
|
-
a1.
|
|
1303
|
+
a1.commitChanges();
|
|
1270
1304
|
// Then the list is no longer dirty
|
|
1271
1305
|
expect(a1.books.dirty).toBeFalsy();
|
|
1272
1306
|
// When adding a new book
|
package/dist/useFormState.d.ts
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { ObjectConfig } from "./config";
|
|
2
2
|
import { ObjectState } from "./fields/objectField";
|
|
3
|
+
export declare type Query<I> = {
|
|
4
|
+
data: I;
|
|
5
|
+
loading: boolean;
|
|
6
|
+
error?: any;
|
|
7
|
+
};
|
|
8
|
+
export declare type InputAndMap<T, I> = {
|
|
9
|
+
input: I;
|
|
10
|
+
map: (input: Exclude<I, null | undefined>) => T;
|
|
11
|
+
ifUndefined?: T;
|
|
12
|
+
};
|
|
13
|
+
export declare type QueryAndMap<T, I> = {
|
|
14
|
+
query: Query<I>;
|
|
15
|
+
map: (input: Exclude<I, null | undefined>) => T;
|
|
16
|
+
ifUndefined?: T;
|
|
17
|
+
};
|
|
3
18
|
export declare type UseFormStateOpts<T, I> = {
|
|
4
19
|
/** The form configuration, should be a module-level const or useMemo'd. */
|
|
5
20
|
config: ObjectConfig<T>;
|
|
@@ -20,11 +35,7 @@ export declare type UseFormStateOpts<T, I> = {
|
|
|
20
35
|
* only call `init.map` if it's set, otherwise we'll use `init.ifDefined` or `{}`, saving you
|
|
21
36
|
* from having to null check within your `init.map` function.
|
|
22
37
|
*/
|
|
23
|
-
init?: T |
|
|
24
|
-
input: I;
|
|
25
|
-
map: (input: Exclude<I, null | undefined>) => T;
|
|
26
|
-
ifUndefined?: T;
|
|
27
|
-
};
|
|
38
|
+
init?: T | InputAndMap<T, I> | QueryAndMap<T, I>;
|
|
28
39
|
/**
|
|
29
40
|
* A hook to add custom, cross-field validation rules that can be difficult to setup directly in the config DSL.
|
|
30
41
|
*
|
|
@@ -33,6 +44,13 @@ export declare type UseFormStateOpts<T, I> = {
|
|
|
33
44
|
addRules?: (state: ObjectState<T>) => void;
|
|
34
45
|
/** Whether the form should be read only, when changed it won't re-create the whole form. */
|
|
35
46
|
readOnly?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Whether the form is loading.
|
|
49
|
+
*
|
|
50
|
+
* Note that we also will infer loading from `init.input === undefined` or `init.query.loading`,
|
|
51
|
+
* so you only need to set this directly if you're not using either of those conventions.
|
|
52
|
+
*/
|
|
53
|
+
loading?: boolean;
|
|
36
54
|
/**
|
|
37
55
|
* Fired when the form should auto-save, i.e. after a) blur and b) all fields are valid.
|
|
38
56
|
*
|
package/dist/useFormState.js
CHANGED
|
@@ -16,7 +16,7 @@ let pendingAutoSave = false;
|
|
|
16
16
|
* Creates a formState instance for editing in a form.
|
|
17
17
|
*/
|
|
18
18
|
function useFormState(opts) {
|
|
19
|
-
const { config, init, addRules, readOnly = false, autoSave } = opts;
|
|
19
|
+
const { config, init, addRules, readOnly = false, loading, autoSave } = opts;
|
|
20
20
|
// Use a ref so our memo'ized `onBlur` always see the latest value
|
|
21
21
|
const autoSaveRef = react_1.useRef(autoSave);
|
|
22
22
|
autoSaveRef.current = autoSave;
|
|
@@ -25,7 +25,7 @@ function useFormState(opts) {
|
|
|
25
25
|
const [firstInitValue] = react_1.useState(() => utils_1.initValue(config, init));
|
|
26
26
|
const isWrappingMobxProxy = !is_plain_object_1.isPlainObject(firstInitValue);
|
|
27
27
|
// If they're using init.input, useMemo on it (and it might be an array), otherwise allow the identity of init be unstable
|
|
28
|
-
const dep = init
|
|
28
|
+
const dep = utils_1.isInput(init) ? makeArray(init.input) : utils_1.isQuery(init) ? [init.query.data, init.query.loading] : [];
|
|
29
29
|
const form = react_1.useMemo(() => {
|
|
30
30
|
function maybeAutoSave() {
|
|
31
31
|
if (isAutoSaving === "in-flight") {
|
|
@@ -62,6 +62,7 @@ function useFormState(opts) {
|
|
|
62
62
|
const value = firstRunRef.current ? firstInitValue : utils_1.initValue(config, init);
|
|
63
63
|
const form = objectField_1.createObjectState(config, value, { maybeAutoSave });
|
|
64
64
|
form.readOnly = readOnly;
|
|
65
|
+
setLoading(form, opts);
|
|
65
66
|
// The identity of `addRules` is not stable, but assume that it is for better UX.
|
|
66
67
|
(addRules || (() => { }))(form);
|
|
67
68
|
firstRunRef.current = true;
|
|
@@ -80,12 +81,34 @@ function useFormState(opts) {
|
|
|
80
81
|
firstRunRef.current = false;
|
|
81
82
|
return;
|
|
82
83
|
}
|
|
84
|
+
setLoading(form, opts);
|
|
83
85
|
form.set(utils_1.initValue(config, init), { refreshing: true });
|
|
84
86
|
}, [form, ...dep]);
|
|
85
87
|
// Use useEffect so that we don't touch the form.init proxy during a render
|
|
86
88
|
react_1.useEffect(() => {
|
|
87
89
|
form.readOnly = readOnly;
|
|
88
|
-
|
|
90
|
+
if (loading !== undefined) {
|
|
91
|
+
form.loading = loading;
|
|
92
|
+
}
|
|
93
|
+
}, [form, readOnly, loading]);
|
|
89
94
|
return form;
|
|
90
95
|
}
|
|
91
96
|
exports.useFormState = useFormState;
|
|
97
|
+
function setLoading(form, opts) {
|
|
98
|
+
const { loading, init } = opts;
|
|
99
|
+
if (loading !== undefined) {
|
|
100
|
+
// Prefer the explicit/top-level opts.loading if it's set
|
|
101
|
+
form.loading = loading;
|
|
102
|
+
}
|
|
103
|
+
else if (utils_1.isInput(init) && !init.ifUndefined) {
|
|
104
|
+
// Otherwise, check for `init.input`
|
|
105
|
+
form.loading = init.input === undefined;
|
|
106
|
+
}
|
|
107
|
+
else if (utils_1.isQuery(init)) {
|
|
108
|
+
// Or `query.loading`
|
|
109
|
+
form.loading = init.query.loading;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function makeArray(input) {
|
|
113
|
+
return Array.isArray(input) ? input : [input];
|
|
114
|
+
}
|
|
@@ -15,10 +15,10 @@ describe("useFormState", () => {
|
|
|
15
15
|
function TestComponent() {
|
|
16
16
|
const config = { firstName: { type: "value" } };
|
|
17
17
|
// And we have query data that may or may not be defined
|
|
18
|
-
const data =
|
|
19
|
-
// Then the lambda is passed the "de-undefined" data
|
|
18
|
+
const data = { firstName: "bob" };
|
|
20
19
|
const form = useFormState_1.useFormState({
|
|
21
20
|
config,
|
|
21
|
+
// Then the lambda is passed the "de-undefined" data, i.e. `d.firstName` is not a compile error
|
|
22
22
|
init: { input: data, map: (d) => ({ firstName: d.firstName }) },
|
|
23
23
|
});
|
|
24
24
|
return jsx_runtime_1.jsx("div", { children: form.firstName.value }, void 0);
|
|
@@ -454,6 +454,51 @@ describe("useFormState", () => {
|
|
|
454
454
|
expect(autoSaveStub).toBeCalledTimes(1);
|
|
455
455
|
expect(autoSaveStub).toBeCalledWith({ id: "a:1", firstName: "first", lastName: "first" });
|
|
456
456
|
});
|
|
457
|
+
it("sets loading if opts.loading is true", async () => {
|
|
458
|
+
const config = { firstName: { type: "value" } };
|
|
459
|
+
function TestComponent({ loading }) {
|
|
460
|
+
const form = useFormState_1.useFormState({ config, loading });
|
|
461
|
+
return jsx_runtime_1.jsx(mobx_react_1.Observer, { children: () => jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "loading" }, { children: String(form.loading) }), void 0) }, void 0);
|
|
462
|
+
}
|
|
463
|
+
// And we initially pass in `init.query.loading: true`
|
|
464
|
+
const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, { loading: true }, void 0));
|
|
465
|
+
// Then the form is marked as loading
|
|
466
|
+
expect(r.loading()).toHaveTextContent("true");
|
|
467
|
+
// And when the query is not loading
|
|
468
|
+
await r.rerender(jsx_runtime_1.jsx(TestComponent, { loading: false }, void 0));
|
|
469
|
+
// Then the form is marked as not loading
|
|
470
|
+
expect(r.loading()).toHaveTextContent("false");
|
|
471
|
+
});
|
|
472
|
+
it("sets loading if input.data is undefined", async () => {
|
|
473
|
+
const config = { firstName: { type: "value" } };
|
|
474
|
+
function TestComponent({ data }) {
|
|
475
|
+
const form = useFormState_1.useFormState({ config, init: { input: data, map: (d) => d } });
|
|
476
|
+
return jsx_runtime_1.jsx(mobx_react_1.Observer, { children: () => jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "loading" }, { children: String(form.loading) }), void 0) }, void 0);
|
|
477
|
+
}
|
|
478
|
+
// And we initially pass in `init.input: undefined`
|
|
479
|
+
const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, { data: undefined }, void 0));
|
|
480
|
+
// Then the form is marked as loading
|
|
481
|
+
expect(r.loading()).toHaveTextContent("true");
|
|
482
|
+
// And when the data is no longer undefined
|
|
483
|
+
await r.rerender(jsx_runtime_1.jsx(TestComponent, { data: { firstName: "first" } }, void 0));
|
|
484
|
+
// Then the form is marked as not loading
|
|
485
|
+
expect(r.loading()).toHaveTextContent("false");
|
|
486
|
+
});
|
|
487
|
+
it("sets loading if query.loading is true", async () => {
|
|
488
|
+
const config = { firstName: { type: "value" } };
|
|
489
|
+
function TestComponent({ loading, data }) {
|
|
490
|
+
const form = useFormState_1.useFormState({ config, init: { query: { data, loading, error: null }, map: (d) => d } });
|
|
491
|
+
return jsx_runtime_1.jsx(mobx_react_1.Observer, { children: () => jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "loading" }, { children: String(form.loading) }), void 0) }, void 0);
|
|
492
|
+
}
|
|
493
|
+
// And we initially pass in `init.query.loading: true`
|
|
494
|
+
const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, { loading: true, data: undefined }, void 0));
|
|
495
|
+
// Then the form is marked as loading
|
|
496
|
+
expect(r.loading()).toHaveTextContent("true");
|
|
497
|
+
// And when the query is not loading
|
|
498
|
+
await r.rerender(jsx_runtime_1.jsx(TestComponent, { loading: false, data: { firstName: "first" } }, void 0));
|
|
499
|
+
// Then the form is marked as not loading
|
|
500
|
+
expect(r.loading()).toHaveTextContent("false");
|
|
501
|
+
});
|
|
457
502
|
});
|
|
458
503
|
const authorConfig = {
|
|
459
504
|
id: { type: "value" },
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ObjectConfig } from "./config";
|
|
2
|
+
import { InputAndMap, QueryAndMap, UseFormStateOpts } from "./useFormState";
|
|
2
3
|
export declare type Builtin = Date | Function | Uint8Array | string | number | boolean;
|
|
3
4
|
export declare type Primitive = undefined | null | boolean | string | number | Function | Date | {
|
|
4
5
|
toJSON(): any;
|
|
@@ -11,6 +12,8 @@ export declare function fail(message?: string): never;
|
|
|
11
12
|
export declare function assertNever(x: never): never;
|
|
12
13
|
/** Introspects the `init` prop to see if has a `map` function/etc. and returns the form value. */
|
|
13
14
|
export declare function initValue<T>(config: ObjectConfig<T>, init: any): T;
|
|
15
|
+
export declare function isInput<T, I>(init: UseFormStateOpts<T, I>["init"]): init is InputAndMap<T, I>;
|
|
16
|
+
export declare function isQuery<T, I>(init: UseFormStateOpts<T, I>["init"]): init is QueryAndMap<T, I>;
|
|
14
17
|
/**
|
|
15
18
|
* Picks ony fields out of `instance` that are in the `formConfig`.
|
|
16
19
|
*
|
package/dist/utils.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.hasToJSON = exports.areEqual = exports.isEmpty = exports.isNotUndefined = exports.pickFields = exports.initValue = exports.assertNever = exports.fail = void 0;
|
|
6
|
+
exports.hasToJSON = exports.areEqual = exports.isEmpty = exports.isNotUndefined = exports.pickFields = exports.isQuery = exports.isInput = exports.initValue = exports.assertNever = exports.fail = void 0;
|
|
7
7
|
const fast_deep_equal_1 = __importDefault(require("fast-deep-equal"));
|
|
8
8
|
const is_plain_object_1 = require("is-plain-object");
|
|
9
9
|
const mobx_1 = require("mobx");
|
|
@@ -17,14 +17,27 @@ function assertNever(x) {
|
|
|
17
17
|
exports.assertNever = assertNever;
|
|
18
18
|
/** Introspects the `init` prop to see if has a `map` function/etc. and returns the form value. */
|
|
19
19
|
function initValue(config, init) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
let value;
|
|
21
|
+
if (isInput(init)) {
|
|
22
|
+
value = init.input ? init.map(init.input) : init.ifUndefined;
|
|
23
|
+
}
|
|
24
|
+
else if (isQuery(init)) {
|
|
25
|
+
value = init.query.data ? init.map(init.query.data) : init.ifUndefined;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
value = init;
|
|
29
|
+
}
|
|
30
|
+
return pickFields(config, value !== null && value !== void 0 ? value : {});
|
|
26
31
|
}
|
|
27
32
|
exports.initValue = initValue;
|
|
33
|
+
function isInput(init) {
|
|
34
|
+
return !!init && "input" in init && "map" in init;
|
|
35
|
+
}
|
|
36
|
+
exports.isInput = isInput;
|
|
37
|
+
function isQuery(init) {
|
|
38
|
+
return !!init && "query" in init && "map" in init;
|
|
39
|
+
}
|
|
40
|
+
exports.isQuery = isQuery;
|
|
28
41
|
/**
|
|
29
42
|
* Picks ony fields out of `instance` that are in the `formConfig`.
|
|
30
43
|
*
|