@homebound/form-state 2.9.1 → 2.11.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/config.d.ts +66 -0
- package/dist/config.js +2 -0
- package/dist/fields/listField.d.ts +11 -0
- package/dist/fields/listField.js +212 -0
- package/dist/fields/objectField.d.ts +54 -0
- package/dist/fields/objectField.js +187 -0
- package/dist/fields/valueField.d.ts +59 -0
- package/dist/fields/valueField.js +158 -0
- package/dist/formState.test.js +131 -60
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -3
- package/dist/rules.d.ts +14 -0
- package/dist/rules.js +11 -0
- package/dist/useFormState.d.ts +2 -1
- package/dist/useFormState.js +8 -2
- package/dist/useFormState.test.js +21 -6
- package/dist/useFormStates.d.ts +11 -3
- package/dist/useFormStates.js +8 -4
- package/dist/useFormStates.test.js +48 -9
- package/dist/utils.d.ts +24 -1
- package/dist/utils.js +41 -1
- package/dist/utils.test.js +3 -3
- package/package.json +1 -1
- package/dist/formState.d.ts +0 -192
- package/dist/formState.js +0 -573
package/dist/formState.test.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const mobx_1 = require("mobx");
|
|
4
|
+
const objectField_1 = require("./fields/objectField");
|
|
4
5
|
const formStateDomain_1 = require("./formStateDomain");
|
|
5
|
-
const
|
|
6
|
+
const rules_1 = require("./rules");
|
|
6
7
|
describe("formState", () => {
|
|
7
8
|
it("mobx lists maintain observable identity", () => {
|
|
8
9
|
// given a parent observable
|
|
@@ -14,11 +15,11 @@ describe("formState", () => {
|
|
|
14
15
|
expect(a.list[0] === c1).toEqual(true);
|
|
15
16
|
});
|
|
16
17
|
it("can create a simple object", () => {
|
|
17
|
-
const a =
|
|
18
|
+
const a = objectField_1.createObjectState({ title: { type: "value", rules: [rules_1.required] } }, {});
|
|
18
19
|
expect(a.valid).toBeFalsy();
|
|
19
20
|
});
|
|
20
21
|
it("can validate a simple input", () => {
|
|
21
|
-
const a =
|
|
22
|
+
const a = objectField_1.createObjectState({ title: { type: "value", rules: [rules_1.required] } }, { title: "b1" });
|
|
22
23
|
let numErrors = 0;
|
|
23
24
|
mobx_1.autorun(() => {
|
|
24
25
|
numErrors = a.title.errors.length;
|
|
@@ -32,15 +33,15 @@ describe("formState", () => {
|
|
|
32
33
|
expect(a.errors).toEqual(["title: Required"]);
|
|
33
34
|
});
|
|
34
35
|
it("can tell what is required", () => {
|
|
35
|
-
const a =
|
|
36
|
-
title: { type: "value", rules: [
|
|
36
|
+
const a = objectField_1.createObjectState({
|
|
37
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
37
38
|
isPublished: { type: "value" },
|
|
38
39
|
}, {});
|
|
39
40
|
expect(a.title.required).toBeTruthy();
|
|
40
41
|
expect(a.isPublished.required).toBeFalsy();
|
|
41
42
|
});
|
|
42
43
|
it("trims whitespace within the required rule", () => {
|
|
43
|
-
const a =
|
|
44
|
+
const a = objectField_1.createObjectState({ title: { type: "value", rules: [rules_1.required] } }, { title: "initial valid title" });
|
|
44
45
|
let numErrors = 0;
|
|
45
46
|
mobx_1.autorun(() => {
|
|
46
47
|
numErrors = a.title.errors.length;
|
|
@@ -55,16 +56,16 @@ describe("formState", () => {
|
|
|
55
56
|
expect(a.errors).toEqual(["title: Required"]);
|
|
56
57
|
});
|
|
57
58
|
it("can set values", () => {
|
|
58
|
-
const a =
|
|
59
|
+
const a = objectField_1.createObjectState({ title: { type: "value" } }, { title: "b1" });
|
|
59
60
|
expect(a.title.value).toEqual("b1");
|
|
60
61
|
});
|
|
61
62
|
it("can read values", () => {
|
|
62
|
-
const a =
|
|
63
|
+
const a = objectField_1.createObjectState({ title: { type: "value" } }, { title: "b1" });
|
|
63
64
|
expect(a.value.title).toEqual("b1");
|
|
64
65
|
expect(a.title.value).toEqual("b1");
|
|
65
66
|
});
|
|
66
67
|
it("can set dates", () => {
|
|
67
|
-
const a =
|
|
68
|
+
const a = objectField_1.createObjectState({
|
|
68
69
|
birthday: {
|
|
69
70
|
type: "value",
|
|
70
71
|
rules: [({ value }) => ((value === null || value === void 0 ? void 0 : value.getTime()) === formStateDomain_1.jan2.getTime() ? "cannot be born on jan2" : undefined)],
|
|
@@ -149,7 +150,7 @@ describe("formState", () => {
|
|
|
149
150
|
};
|
|
150
151
|
// And we initially have ids in the input
|
|
151
152
|
const a1 = { id: "1", firstName: "a1", books: [{ id: "2", title: "t1" }] };
|
|
152
|
-
const state =
|
|
153
|
+
const state = objectField_1.createObjectState(config, a1);
|
|
153
154
|
// And we edit a few things
|
|
154
155
|
state.firstName.set("a2");
|
|
155
156
|
state.books.add({ title: "t2" });
|
|
@@ -286,7 +287,7 @@ describe("formState", () => {
|
|
|
286
287
|
expect(numErrors).toEqual(0);
|
|
287
288
|
});
|
|
288
289
|
it("can validate across fields", () => {
|
|
289
|
-
const a =
|
|
290
|
+
const a = objectField_1.createObjectState({
|
|
290
291
|
firstName: { type: "value", rules: [] },
|
|
291
292
|
lastName: { type: "value" },
|
|
292
293
|
}, {});
|
|
@@ -303,7 +304,7 @@ describe("formState", () => {
|
|
|
303
304
|
it("passes sanitized values to validation rules", () => {
|
|
304
305
|
// Given a field with a validation rule
|
|
305
306
|
const rule = jest.fn().mockReturnValue("Required");
|
|
306
|
-
const a =
|
|
307
|
+
const a = objectField_1.createObjectState({ firstName: { type: "value", rules: [rule] } }, {});
|
|
307
308
|
expect(rule).toHaveBeenCalledTimes(0);
|
|
308
309
|
// When the field is set to empty string
|
|
309
310
|
a.firstName.value = "";
|
|
@@ -320,7 +321,7 @@ describe("formState", () => {
|
|
|
320
321
|
it("passes trimmed values to validation rules on maybeAutoSave", () => {
|
|
321
322
|
// Given we have a required rule
|
|
322
323
|
const rule = jest.fn().mockReturnValue("Required");
|
|
323
|
-
const a =
|
|
324
|
+
const a = objectField_1.createObjectState({ firstName: { type: "value", rules: [rule] } }, {});
|
|
324
325
|
expect(rule).toHaveBeenCalledTimes(0);
|
|
325
326
|
// When the user initially sets an empty string value
|
|
326
327
|
a.firstName.focus();
|
|
@@ -347,8 +348,8 @@ describe("formState", () => {
|
|
|
347
348
|
});
|
|
348
349
|
});
|
|
349
350
|
it("simple value changes trigger observers", () => {
|
|
350
|
-
const a =
|
|
351
|
-
title: { type: "value", rules: [
|
|
351
|
+
const a = objectField_1.createObjectState({
|
|
352
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
352
353
|
}, { title: "t2" });
|
|
353
354
|
let lastTitle = undefined;
|
|
354
355
|
let ticks = 0;
|
|
@@ -465,7 +466,7 @@ describe("formState", () => {
|
|
|
465
466
|
expect(maybeAutoSave).toBeCalledTimes(0);
|
|
466
467
|
});
|
|
467
468
|
it("knows list of primitives are dirty", () => {
|
|
468
|
-
const a1 =
|
|
469
|
+
const a1 = objectField_1.createObjectState({ favoriteColors: { type: "value" } }, {});
|
|
469
470
|
expect(a1.favoriteColors.dirty).toBeFalsy();
|
|
470
471
|
a1.favoriteColors.set([formStateDomain_1.Color.Blue]);
|
|
471
472
|
expect(a1.dirty).toBeTruthy();
|
|
@@ -476,7 +477,7 @@ describe("formState", () => {
|
|
|
476
477
|
expect(a1.dirty).toBeFalsy();
|
|
477
478
|
});
|
|
478
479
|
it("knows list of primitives are dirty with initialized as empty list", () => {
|
|
479
|
-
const a1 =
|
|
480
|
+
const a1 = objectField_1.createObjectState({ favoriteColors: { type: "value" } }, { favoriteColors: [] });
|
|
480
481
|
expect(a1.favoriteColors.dirty).toBeFalsy();
|
|
481
482
|
a1.favoriteColors.set([formStateDomain_1.Color.Blue]);
|
|
482
483
|
expect(a1.dirty).toBeTruthy();
|
|
@@ -656,7 +657,7 @@ describe("formState", () => {
|
|
|
656
657
|
expect(a1.value.firstName).toBeNull();
|
|
657
658
|
});
|
|
658
659
|
it("can map properties to other types", () => {
|
|
659
|
-
const a1 =
|
|
660
|
+
const a1 = objectField_1.createObjectState({
|
|
660
661
|
person: { type: "value" },
|
|
661
662
|
}, { person: { firstName: "a1", lastName: "b1" } });
|
|
662
663
|
a1.person.set({ firstName: "a2", lastName: "b2" });
|
|
@@ -682,20 +683,90 @@ describe("formState", () => {
|
|
|
682
683
|
});
|
|
683
684
|
it("maintain field readOnly state when form is readOnly", () => {
|
|
684
685
|
// Given a formState
|
|
685
|
-
const formState =
|
|
686
|
+
const formState = objectField_1.createObjectState({ title: { type: "value", rules: [rules_1.required], readOnly: true } }, {});
|
|
686
687
|
// Then expect form
|
|
687
688
|
expect(formState.title.readOnly).toBeTruthy();
|
|
688
689
|
});
|
|
690
|
+
it("has an object-level readOnly=true override field-level readOnly=false", () => {
|
|
691
|
+
// Given a top-level object
|
|
692
|
+
const a1 = createAuthorInputState({
|
|
693
|
+
firstName: "a1",
|
|
694
|
+
lastName: "aL1",
|
|
695
|
+
books: [{ title: "b1", classification: formStateDomain_1.dd100 }],
|
|
696
|
+
});
|
|
697
|
+
// And it is read-only
|
|
698
|
+
a1.readOnly = true;
|
|
699
|
+
// Then its fields are read-only
|
|
700
|
+
const fields = [a1, a1.firstName, a1.books, a1.books.rows[0].title, a1.books.rows[0].classification];
|
|
701
|
+
fields.forEach((f) => expect(f.readOnly).toBeTruthy());
|
|
702
|
+
// And even if a specific field tries to _not_ be read-only,
|
|
703
|
+
// i.e. due to a more granular business rule that happens to
|
|
704
|
+
// be allowed right now
|
|
705
|
+
a1.firstName.readOnly = false;
|
|
706
|
+
// Then the field-level rule is ignored, and it's still treated as read-only
|
|
707
|
+
expect(a1.firstName.readOnly).toBeTruthy();
|
|
708
|
+
});
|
|
709
|
+
it("has an field-level readOnly=true override object-level readOnly=false", () => {
|
|
710
|
+
// Given a top-level object
|
|
711
|
+
const a1 = createAuthorInputState({
|
|
712
|
+
firstName: "a1",
|
|
713
|
+
lastName: "aL1",
|
|
714
|
+
books: [{ title: "b1", classification: formStateDomain_1.dd100 }],
|
|
715
|
+
});
|
|
716
|
+
// And the top-level form is explicitly set to read-only=false
|
|
717
|
+
a1.readOnly = false;
|
|
718
|
+
// Then its fields are not read-only
|
|
719
|
+
const fields = [a1, a1.firstName, a1.books, a1.books.rows[0].title, a1.books.rows[0].classification];
|
|
720
|
+
fields.forEach((f) => expect(f.readOnly).toBeFalsy());
|
|
721
|
+
// But when one of the fields opts in to readOnly
|
|
722
|
+
a1.firstName.readOnly = true;
|
|
723
|
+
// Then the field-level rule is respected
|
|
724
|
+
expect(a1.firstName.readOnly).toBeTruthy();
|
|
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
|
+
});
|
|
689
760
|
it("canSave returns dirty and touches", () => {
|
|
690
|
-
const a1 =
|
|
691
|
-
firstName: { type: "value", rules: [
|
|
761
|
+
const a1 = objectField_1.createObjectState({
|
|
762
|
+
firstName: { type: "value", rules: [rules_1.required] },
|
|
692
763
|
}, {});
|
|
693
764
|
expect(a1.firstName.touched).toBeFalsy();
|
|
694
765
|
expect(a1.canSave()).toBeFalsy();
|
|
695
766
|
expect(a1.firstName.touched).toBeTruthy();
|
|
696
767
|
});
|
|
697
768
|
it("uses toJSON if available for dirty checks", () => {
|
|
698
|
-
const a1 =
|
|
769
|
+
const a1 = objectField_1.createObjectState({
|
|
699
770
|
birthday: { type: "value" },
|
|
700
771
|
}, { birthday: new formStateDomain_1.DateOnly(formStateDomain_1.jan1) });
|
|
701
772
|
a1.birthday.set(new formStateDomain_1.DateOnly(formStateDomain_1.jan1));
|
|
@@ -710,10 +781,10 @@ describe("formState", () => {
|
|
|
710
781
|
expect(a.valid).toBeTruthy();
|
|
711
782
|
});
|
|
712
783
|
it("can validate a list input", () => {
|
|
713
|
-
const a =
|
|
784
|
+
const a = objectField_1.createObjectState({
|
|
714
785
|
books: {
|
|
715
786
|
type: "list",
|
|
716
|
-
config: { title: { type: "value", rules: [
|
|
787
|
+
config: { title: { type: "value", rules: [rules_1.required] } },
|
|
717
788
|
rules: [({ value }) => (value.find((b) => b.title.value === "t1") ? "Cannot have t1" : undefined)],
|
|
718
789
|
},
|
|
719
790
|
}, {});
|
|
@@ -727,11 +798,11 @@ describe("formState", () => {
|
|
|
727
798
|
expect(lastErrors).toEqual("");
|
|
728
799
|
});
|
|
729
800
|
it("can ignore a deleted list entry", () => {
|
|
730
|
-
const a =
|
|
801
|
+
const a = objectField_1.createObjectState({
|
|
731
802
|
books: {
|
|
732
803
|
type: "list",
|
|
733
804
|
config: {
|
|
734
|
-
title: { type: "value", rules: [
|
|
805
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
735
806
|
delete: { type: "value", isDeleteKey: true },
|
|
736
807
|
},
|
|
737
808
|
},
|
|
@@ -746,11 +817,11 @@ describe("formState", () => {
|
|
|
746
817
|
expect(a.books.valid).toBeTruthy();
|
|
747
818
|
});
|
|
748
819
|
it("can treat a list entry as read-only", () => {
|
|
749
|
-
const a =
|
|
820
|
+
const a = objectField_1.createObjectState({
|
|
750
821
|
books: {
|
|
751
822
|
type: "list",
|
|
752
823
|
config: {
|
|
753
|
-
title: { type: "value", rules: [
|
|
824
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
754
825
|
isPublished: { type: "value", isReadOnlyKey: true },
|
|
755
826
|
},
|
|
756
827
|
},
|
|
@@ -814,7 +885,7 @@ describe("formState", () => {
|
|
|
814
885
|
expect(config).toBeDefined();
|
|
815
886
|
});
|
|
816
887
|
it("can reset observable objects with computeds", () => {
|
|
817
|
-
const formState =
|
|
888
|
+
const formState = objectField_1.createObjectState({
|
|
818
889
|
firstName: { type: "value" },
|
|
819
890
|
lastName: { type: "value" },
|
|
820
891
|
fullName: { type: "value", computed: true },
|
|
@@ -827,7 +898,7 @@ describe("formState", () => {
|
|
|
827
898
|
expect(formState.firstName.value).toEqual("first");
|
|
828
899
|
});
|
|
829
900
|
it("can set computeds that have setters", () => {
|
|
830
|
-
const formState =
|
|
901
|
+
const formState = objectField_1.createObjectState({
|
|
831
902
|
firstName: { type: "value" },
|
|
832
903
|
lastName: { type: "value" },
|
|
833
904
|
fullName: { type: "value", computed: true },
|
|
@@ -840,7 +911,7 @@ describe("formState", () => {
|
|
|
840
911
|
expect(formState.firstName.value).toEqual("first");
|
|
841
912
|
});
|
|
842
913
|
it("can mark fields as read only", () => {
|
|
843
|
-
const formState =
|
|
914
|
+
const formState = objectField_1.createObjectState({
|
|
844
915
|
firstName: { type: "value" },
|
|
845
916
|
lastName: { type: "value" },
|
|
846
917
|
fullName: { type: "value", readOnly: true },
|
|
@@ -853,7 +924,7 @@ describe("formState", () => {
|
|
|
853
924
|
expect(formState.firstName.value).toEqual("first");
|
|
854
925
|
});
|
|
855
926
|
it("lets rules validate against other fields", () => {
|
|
856
|
-
const formState =
|
|
927
|
+
const formState = objectField_1.createObjectState({
|
|
857
928
|
firstName: { type: "value", rules: [] },
|
|
858
929
|
lastName: {
|
|
859
930
|
type: "value",
|
|
@@ -872,7 +943,7 @@ describe("formState", () => {
|
|
|
872
943
|
});
|
|
873
944
|
it("can return only changed primitive fields", () => {
|
|
874
945
|
// Given an author
|
|
875
|
-
const formState =
|
|
946
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, {
|
|
876
947
|
id: "a:1",
|
|
877
948
|
firstName: "f",
|
|
878
949
|
lastName: "l",
|
|
@@ -890,7 +961,7 @@ describe("formState", () => {
|
|
|
890
961
|
});
|
|
891
962
|
it("can return only changed object fields", () => {
|
|
892
963
|
// Given an author with an address object
|
|
893
|
-
const formState =
|
|
964
|
+
const formState = objectField_1.createObjectState(authorWithAddressConfig, {
|
|
894
965
|
id: "a:1",
|
|
895
966
|
firstName: "f",
|
|
896
967
|
address: { city: "c1", street: "s1" },
|
|
@@ -907,7 +978,7 @@ describe("formState", () => {
|
|
|
907
978
|
});
|
|
908
979
|
it("changedValue skips empty nested fields", () => {
|
|
909
980
|
// Given a new author with an address object
|
|
910
|
-
const formState =
|
|
981
|
+
const formState = objectField_1.createObjectState(authorWithAddressConfig, {
|
|
911
982
|
firstName: "f",
|
|
912
983
|
// And nothing is in the address
|
|
913
984
|
address: {},
|
|
@@ -918,7 +989,7 @@ describe("formState", () => {
|
|
|
918
989
|
});
|
|
919
990
|
it("changedValue skips effectively empty nested fields", () => {
|
|
920
991
|
// Given a new author with an address object
|
|
921
|
-
const formState =
|
|
992
|
+
const formState = objectField_1.createObjectState(authorWithAddressConfig, {
|
|
922
993
|
firstName: "f",
|
|
923
994
|
// And only undefined keys are in the address
|
|
924
995
|
address: { city: undefined },
|
|
@@ -929,7 +1000,7 @@ describe("formState", () => {
|
|
|
929
1000
|
});
|
|
930
1001
|
it("can return only changed list fields", () => {
|
|
931
1002
|
// Given an author with some books
|
|
932
|
-
const formState =
|
|
1003
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, {
|
|
933
1004
|
id: "a:1",
|
|
934
1005
|
firstName: "f",
|
|
935
1006
|
books: [
|
|
@@ -950,7 +1021,7 @@ describe("formState", () => {
|
|
|
950
1021
|
});
|
|
951
1022
|
it("can return only changed but incremental list fields", () => {
|
|
952
1023
|
// Given an author
|
|
953
|
-
const formState =
|
|
1024
|
+
const formState = objectField_1.createObjectState({
|
|
954
1025
|
id: { type: "value" },
|
|
955
1026
|
// And the books collection is marked as incremental (i.e. line items)
|
|
956
1027
|
books: {
|
|
@@ -958,7 +1029,7 @@ describe("formState", () => {
|
|
|
958
1029
|
update: "incremental",
|
|
959
1030
|
config: {
|
|
960
1031
|
id: { type: "value" },
|
|
961
|
-
title: { type: "value", rules: [
|
|
1032
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
962
1033
|
},
|
|
963
1034
|
},
|
|
964
1035
|
}, {
|
|
@@ -981,14 +1052,14 @@ describe("formState", () => {
|
|
|
981
1052
|
});
|
|
982
1053
|
it("uses the op key as a hint to use incremental list behavior", () => {
|
|
983
1054
|
// Given an author
|
|
984
|
-
const formState =
|
|
1055
|
+
const formState = objectField_1.createObjectState({
|
|
985
1056
|
id: { type: "value" },
|
|
986
1057
|
// And the books collection is not explicitly marked as incremental
|
|
987
1058
|
books: {
|
|
988
1059
|
type: "list",
|
|
989
1060
|
config: {
|
|
990
1061
|
id: { type: "value" },
|
|
991
|
-
title: { type: "value", rules: [
|
|
1062
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
992
1063
|
// But it does have an op key
|
|
993
1064
|
op: { type: "value" },
|
|
994
1065
|
},
|
|
@@ -1014,14 +1085,14 @@ describe("formState", () => {
|
|
|
1014
1085
|
});
|
|
1015
1086
|
it("defaults the incremental op key to included when not set", () => {
|
|
1016
1087
|
// Given an author
|
|
1017
|
-
const formState =
|
|
1088
|
+
const formState = objectField_1.createObjectState({
|
|
1018
1089
|
id: { type: "value" },
|
|
1019
1090
|
// And the books collection is not explicitly marked as incremental
|
|
1020
1091
|
books: {
|
|
1021
1092
|
type: "list",
|
|
1022
1093
|
config: {
|
|
1023
1094
|
id: { type: "value" },
|
|
1024
|
-
title: { type: "value", rules: [
|
|
1095
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
1025
1096
|
// But it does have an op key
|
|
1026
1097
|
op: { type: "value" },
|
|
1027
1098
|
},
|
|
@@ -1055,7 +1126,7 @@ describe("formState", () => {
|
|
|
1055
1126
|
});
|
|
1056
1127
|
});
|
|
1057
1128
|
it("can observe value changes", () => {
|
|
1058
|
-
const formState =
|
|
1129
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f" });
|
|
1059
1130
|
let ticks = 0;
|
|
1060
1131
|
mobx_1.reaction(() => formState.value, () => ticks++, { equals: () => false });
|
|
1061
1132
|
expect(ticks).toEqual(0);
|
|
@@ -1065,7 +1136,7 @@ describe("formState", () => {
|
|
|
1065
1136
|
expect(ticks).toEqual(1);
|
|
1066
1137
|
});
|
|
1067
1138
|
it("does not override a focused, changed field", () => {
|
|
1068
|
-
const formState =
|
|
1139
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1069
1140
|
// Given first name is focused
|
|
1070
1141
|
formState.firstName.focus();
|
|
1071
1142
|
// And they have wip edits
|
|
@@ -1080,7 +1151,7 @@ describe("formState", () => {
|
|
|
1080
1151
|
expect(formState.firstName.value).toEqual("fff");
|
|
1081
1152
|
});
|
|
1082
1153
|
it("does update a focused, unchanged field", () => {
|
|
1083
|
-
const formState =
|
|
1154
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1084
1155
|
// Given first name is focused, but has not changed
|
|
1085
1156
|
formState.firstName.focus();
|
|
1086
1157
|
// When a mutation result sets both firstName and lastName
|
|
@@ -1089,7 +1160,7 @@ describe("formState", () => {
|
|
|
1089
1160
|
expect(formState.firstName.value).toEqual("f2");
|
|
1090
1161
|
});
|
|
1091
1162
|
it("trims string values on blur", () => {
|
|
1092
|
-
const formState =
|
|
1163
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1093
1164
|
// Given the user is typing with spaces
|
|
1094
1165
|
formState.firstName.focus();
|
|
1095
1166
|
formState.firstName.set("f ");
|
|
@@ -1101,7 +1172,7 @@ describe("formState", () => {
|
|
|
1101
1172
|
expect(formState.firstName.value).toEqual("f");
|
|
1102
1173
|
});
|
|
1103
1174
|
it("trims string values on maybeAutoSave", () => {
|
|
1104
|
-
const formState =
|
|
1175
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1105
1176
|
// Given the user is typing with spaces
|
|
1106
1177
|
formState.firstName.focus();
|
|
1107
1178
|
formState.firstName.set("f ");
|
|
@@ -1115,7 +1186,7 @@ describe("formState", () => {
|
|
|
1115
1186
|
expect(formState.firstName.touched).toEqual(true);
|
|
1116
1187
|
});
|
|
1117
1188
|
it("trims empty values to undefined on blur", () => {
|
|
1118
|
-
const formState =
|
|
1189
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1119
1190
|
// Given the user is typing with only spaces
|
|
1120
1191
|
formState.firstName.focus();
|
|
1121
1192
|
formState.firstName.set(" ");
|
|
@@ -1133,7 +1204,7 @@ describe("formState", () => {
|
|
|
1133
1204
|
});
|
|
1134
1205
|
it("can work with both required inputs and optional fields", () => {
|
|
1135
1206
|
// And we drop it in a form.
|
|
1136
|
-
const form =
|
|
1207
|
+
const form = objectField_1.createObjectState({
|
|
1137
1208
|
id: { type: "value" },
|
|
1138
1209
|
firstName: { type: "value" },
|
|
1139
1210
|
}, { id: "a:1" });
|
|
@@ -1154,7 +1225,7 @@ describe("formState", () => {
|
|
|
1154
1225
|
});
|
|
1155
1226
|
it("can have child object states passed in as field states", () => {
|
|
1156
1227
|
// Given an author
|
|
1157
|
-
const a =
|
|
1228
|
+
const a = objectField_1.createObjectState({
|
|
1158
1229
|
id: { type: "value" },
|
|
1159
1230
|
// And address is modeled as a value object
|
|
1160
1231
|
address: { type: "value" },
|
|
@@ -1171,7 +1242,7 @@ describe("formState", () => {
|
|
|
1171
1242
|
});
|
|
1172
1243
|
it("provides isNewEntity", () => {
|
|
1173
1244
|
// Given an author without an id
|
|
1174
|
-
const formState =
|
|
1245
|
+
const formState = objectField_1.createObjectState(authorWithAddressConfig, {
|
|
1175
1246
|
firstName: "f",
|
|
1176
1247
|
address: { city: "c1", street: "s1" },
|
|
1177
1248
|
});
|
|
@@ -1186,7 +1257,7 @@ describe("formState", () => {
|
|
|
1186
1257
|
});
|
|
1187
1258
|
it("isNewEntity is false if there is no id field", () => {
|
|
1188
1259
|
// Given an author without an id field defined
|
|
1189
|
-
const formState =
|
|
1260
|
+
const formState = objectField_1.createObjectState({
|
|
1190
1261
|
firstName: { type: "value" },
|
|
1191
1262
|
address: { type: "object", config: { street: { type: "value" } } },
|
|
1192
1263
|
}, {});
|
|
@@ -1196,7 +1267,7 @@ describe("formState", () => {
|
|
|
1196
1267
|
});
|
|
1197
1268
|
it("provides isNewEntity for lists", () => {
|
|
1198
1269
|
// Given an author & books without an id
|
|
1199
|
-
const formState =
|
|
1270
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, {
|
|
1200
1271
|
books: [{ title: "t1" }, { title: "t2" }],
|
|
1201
1272
|
});
|
|
1202
1273
|
// Then both books know they are new
|
|
@@ -1264,13 +1335,13 @@ const authorWithBooksConfig = {
|
|
|
1264
1335
|
type: "list",
|
|
1265
1336
|
config: {
|
|
1266
1337
|
id: { type: "value" },
|
|
1267
|
-
title: { type: "value", rules: [
|
|
1338
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
1268
1339
|
classification: { type: "value" },
|
|
1269
1340
|
},
|
|
1270
1341
|
},
|
|
1271
1342
|
};
|
|
1272
1343
|
function createAuthorInputState(input, maybeAutoSave) {
|
|
1273
|
-
return
|
|
1344
|
+
return objectField_1.createObjectState(authorWithBooksConfig, input, { maybeAutoSave });
|
|
1274
1345
|
}
|
|
1275
1346
|
const authorWithAddressConfig = {
|
|
1276
1347
|
id: { type: "value" },
|
|
@@ -1279,7 +1350,7 @@ const authorWithAddressConfig = {
|
|
|
1279
1350
|
address: {
|
|
1280
1351
|
type: "object",
|
|
1281
1352
|
config: {
|
|
1282
|
-
street: { type: "value", rules: [
|
|
1353
|
+
street: { type: "value", rules: [rules_1.required] },
|
|
1283
1354
|
city: { type: "value" },
|
|
1284
1355
|
},
|
|
1285
1356
|
},
|
|
@@ -1292,7 +1363,7 @@ const authorWithAddressAndBooksConfig = {
|
|
|
1292
1363
|
type: "object",
|
|
1293
1364
|
config: {
|
|
1294
1365
|
id: { type: "value" },
|
|
1295
|
-
street: { type: "value", rules: [
|
|
1366
|
+
street: { type: "value", rules: [rules_1.required] },
|
|
1296
1367
|
city: { type: "value" },
|
|
1297
1368
|
},
|
|
1298
1369
|
},
|
|
@@ -1300,11 +1371,11 @@ const authorWithAddressAndBooksConfig = {
|
|
|
1300
1371
|
type: "list",
|
|
1301
1372
|
config: {
|
|
1302
1373
|
id: { type: "value" },
|
|
1303
|
-
title: { type: "value", rules: [
|
|
1374
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
1304
1375
|
classification: { type: "value" },
|
|
1305
1376
|
},
|
|
1306
1377
|
},
|
|
1307
1378
|
};
|
|
1308
1379
|
function createAuthorWithAddressInputState(input) {
|
|
1309
|
-
return
|
|
1380
|
+
return objectField_1.createObjectState(authorWithAddressConfig, input);
|
|
1310
1381
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { ObjectConfig } from "./config";
|
|
2
|
+
export { ListFieldState } from "./fields/listField";
|
|
3
|
+
export { createObjectState, ObjectState } from "./fields/objectField";
|
|
4
|
+
export { FieldState } from "./fields/valueField";
|
|
5
|
+
export { required, Rule } from "./rules";
|
|
2
6
|
export { useFormState } from "./useFormState";
|
|
3
7
|
export { useFormStates } from "./useFormStates";
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.useFormStates = exports.useFormState = exports.required = exports.createObjectState = void 0;
|
|
4
|
-
var
|
|
5
|
-
Object.defineProperty(exports, "createObjectState", { enumerable: true, get: function () { return
|
|
6
|
-
|
|
4
|
+
var objectField_1 = require("./fields/objectField");
|
|
5
|
+
Object.defineProperty(exports, "createObjectState", { enumerable: true, get: function () { return objectField_1.createObjectState; } });
|
|
6
|
+
var rules_1 = require("./rules");
|
|
7
|
+
Object.defineProperty(exports, "required", { enumerable: true, get: function () { return rules_1.required; } });
|
|
7
8
|
var useFormState_1 = require("./useFormState");
|
|
8
9
|
Object.defineProperty(exports, "useFormState", { enumerable: true, get: function () { return useFormState_1.useFormState; } });
|
|
9
10
|
var useFormStates_1 = require("./useFormStates");
|
package/dist/rules.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ObjectState } from "./fields/objectField";
|
|
2
|
+
declare type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N;
|
|
3
|
+
/** A validation rule, given the value and name, return the error string if valid, or undefined if valid. */
|
|
4
|
+
export declare type Rule<T, V> = (opts: {
|
|
5
|
+
value: V;
|
|
6
|
+
key: string;
|
|
7
|
+
originalValue: V;
|
|
8
|
+
object: IfAny<T, any, ObjectState<T>>;
|
|
9
|
+
}) => string | undefined;
|
|
10
|
+
/** A rule that validates `value` is not `undefined`, `null`, or empty string. */
|
|
11
|
+
export declare const required: <V>({ value: v }: {
|
|
12
|
+
value: V;
|
|
13
|
+
}) => string | undefined;
|
|
14
|
+
export {};
|
package/dist/rules.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.required = void 0;
|
|
4
|
+
const mobx_1 = require("mobx");
|
|
5
|
+
/** A rule that validates `value` is not `undefined`, `null`, or empty string. */
|
|
6
|
+
// We pre-emptively make this a mobx action so that it's identity doesn't change when proxied
|
|
7
|
+
// and breaks our ability to do `rules.some(r => r === required)`.
|
|
8
|
+
exports.required = mobx_1.action(({ value: v }) => {
|
|
9
|
+
const isEmptyString = typeof v === "string" ? v.trim() === "" : false;
|
|
10
|
+
return v !== undefined && v !== null && !isEmptyString ? undefined : "Required";
|
|
11
|
+
});
|
package/dist/useFormState.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { ObjectConfig
|
|
1
|
+
import { ObjectConfig } from "./config";
|
|
2
|
+
import { ObjectState } from "./fields/objectField";
|
|
2
3
|
export declare type UseFormStateOpts<T, I> = {
|
|
3
4
|
/** The form configuration, should be a module-level const or useMemo'd. */
|
|
4
5
|
config: ObjectConfig<T>;
|
package/dist/useFormState.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.useFormState = void 0;
|
|
4
4
|
const is_plain_object_1 = require("is-plain-object");
|
|
5
5
|
const react_1 = require("react");
|
|
6
|
-
const
|
|
6
|
+
const objectField_1 = require("./fields/objectField");
|
|
7
7
|
const utils_1 = require("./utils");
|
|
8
8
|
// If the user's autoSave hook makes some last-minute `.set` calls to sneak
|
|
9
9
|
// in some business logic right before their GraphQL mutation call, ignore it
|
|
@@ -60,8 +60,11 @@ function useFormState(opts) {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
const value = firstRunRef.current ? firstInitValue : utils_1.initValue(config, init);
|
|
63
|
-
const form =
|
|
63
|
+
const form = objectField_1.createObjectState(config, value, { maybeAutoSave });
|
|
64
64
|
form.readOnly = readOnly;
|
|
65
|
+
if (init && "input" in init && !("ifUndefined" in init)) {
|
|
66
|
+
form.loading = init.input === undefined;
|
|
67
|
+
}
|
|
65
68
|
// The identity of `addRules` is not stable, but assume that it is for better UX.
|
|
66
69
|
(addRules || (() => { }))(form);
|
|
67
70
|
firstRunRef.current = true;
|
|
@@ -80,6 +83,9 @@ function useFormState(opts) {
|
|
|
80
83
|
firstRunRef.current = false;
|
|
81
84
|
return;
|
|
82
85
|
}
|
|
86
|
+
if (init && "input" in init && !("ifUndefined" in init)) {
|
|
87
|
+
form.loading = init.input === undefined;
|
|
88
|
+
}
|
|
83
89
|
form.set(utils_1.initValue(config, init), { refreshing: true });
|
|
84
90
|
}, [form, ...dep]);
|
|
85
91
|
// Use useEffect so that we don't touch the form.init proxy during a render
|