@homebound/form-state 2.10.0 → 2.12.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 +97 -62
- 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 +25 -6
- package/dist/useFormState.js +28 -5
- package/dist/useFormState.test.js +51 -6
- package/dist/useFormStates.d.ts +2 -1
- package/dist/useFormStates.js +2 -2
- package/dist/useFormStates.test.js +3 -3
- package/dist/utils.d.ts +27 -1
- package/dist/utils.js +60 -7
- package/dist/utils.test.js +3 -3
- package/package.json +1 -1
- package/dist/formState.d.ts +0 -192
- package/dist/formState.js +0 -559
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,7 +683,7 @@ 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
|
});
|
|
@@ -695,7 +696,7 @@ describe("formState", () => {
|
|
|
695
696
|
});
|
|
696
697
|
// And it is read-only
|
|
697
698
|
a1.readOnly = true;
|
|
698
|
-
// Then
|
|
699
|
+
// Then its fields are read-only
|
|
699
700
|
const fields = [a1, a1.firstName, a1.books, a1.books.rows[0].title, a1.books.rows[0].classification];
|
|
700
701
|
fields.forEach((f) => expect(f.readOnly).toBeTruthy());
|
|
701
702
|
// And even if a specific field tries to _not_ be read-only,
|
|
@@ -714,7 +715,7 @@ describe("formState", () => {
|
|
|
714
715
|
});
|
|
715
716
|
// And the top-level form is explicitly set to read-only=false
|
|
716
717
|
a1.readOnly = false;
|
|
717
|
-
// Then
|
|
718
|
+
// Then its fields are not read-only
|
|
718
719
|
const fields = [a1, a1.firstName, a1.books, a1.books.rows[0].title, a1.books.rows[0].classification];
|
|
719
720
|
fields.forEach((f) => expect(f.readOnly).toBeFalsy());
|
|
720
721
|
// But when one of the fields opts in to readOnly
|
|
@@ -722,16 +723,50 @@ describe("formState", () => {
|
|
|
722
723
|
// Then the field-level rule is respected
|
|
723
724
|
expect(a1.firstName.readOnly).toBeTruthy();
|
|
724
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
|
+
});
|
|
725
760
|
it("canSave returns dirty and touches", () => {
|
|
726
|
-
const a1 =
|
|
727
|
-
firstName: { type: "value", rules: [
|
|
761
|
+
const a1 = objectField_1.createObjectState({
|
|
762
|
+
firstName: { type: "value", rules: [rules_1.required] },
|
|
728
763
|
}, {});
|
|
729
764
|
expect(a1.firstName.touched).toBeFalsy();
|
|
730
765
|
expect(a1.canSave()).toBeFalsy();
|
|
731
766
|
expect(a1.firstName.touched).toBeTruthy();
|
|
732
767
|
});
|
|
733
768
|
it("uses toJSON if available for dirty checks", () => {
|
|
734
|
-
const a1 =
|
|
769
|
+
const a1 = objectField_1.createObjectState({
|
|
735
770
|
birthday: { type: "value" },
|
|
736
771
|
}, { birthday: new formStateDomain_1.DateOnly(formStateDomain_1.jan1) });
|
|
737
772
|
a1.birthday.set(new formStateDomain_1.DateOnly(formStateDomain_1.jan1));
|
|
@@ -746,10 +781,10 @@ describe("formState", () => {
|
|
|
746
781
|
expect(a.valid).toBeTruthy();
|
|
747
782
|
});
|
|
748
783
|
it("can validate a list input", () => {
|
|
749
|
-
const a =
|
|
784
|
+
const a = objectField_1.createObjectState({
|
|
750
785
|
books: {
|
|
751
786
|
type: "list",
|
|
752
|
-
config: { title: { type: "value", rules: [
|
|
787
|
+
config: { title: { type: "value", rules: [rules_1.required] } },
|
|
753
788
|
rules: [({ value }) => (value.find((b) => b.title.value === "t1") ? "Cannot have t1" : undefined)],
|
|
754
789
|
},
|
|
755
790
|
}, {});
|
|
@@ -763,11 +798,11 @@ describe("formState", () => {
|
|
|
763
798
|
expect(lastErrors).toEqual("");
|
|
764
799
|
});
|
|
765
800
|
it("can ignore a deleted list entry", () => {
|
|
766
|
-
const a =
|
|
801
|
+
const a = objectField_1.createObjectState({
|
|
767
802
|
books: {
|
|
768
803
|
type: "list",
|
|
769
804
|
config: {
|
|
770
|
-
title: { type: "value", rules: [
|
|
805
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
771
806
|
delete: { type: "value", isDeleteKey: true },
|
|
772
807
|
},
|
|
773
808
|
},
|
|
@@ -782,11 +817,11 @@ describe("formState", () => {
|
|
|
782
817
|
expect(a.books.valid).toBeTruthy();
|
|
783
818
|
});
|
|
784
819
|
it("can treat a list entry as read-only", () => {
|
|
785
|
-
const a =
|
|
820
|
+
const a = objectField_1.createObjectState({
|
|
786
821
|
books: {
|
|
787
822
|
type: "list",
|
|
788
823
|
config: {
|
|
789
|
-
title: { type: "value", rules: [
|
|
824
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
790
825
|
isPublished: { type: "value", isReadOnlyKey: true },
|
|
791
826
|
},
|
|
792
827
|
},
|
|
@@ -850,7 +885,7 @@ describe("formState", () => {
|
|
|
850
885
|
expect(config).toBeDefined();
|
|
851
886
|
});
|
|
852
887
|
it("can reset observable objects with computeds", () => {
|
|
853
|
-
const formState =
|
|
888
|
+
const formState = objectField_1.createObjectState({
|
|
854
889
|
firstName: { type: "value" },
|
|
855
890
|
lastName: { type: "value" },
|
|
856
891
|
fullName: { type: "value", computed: true },
|
|
@@ -863,7 +898,7 @@ describe("formState", () => {
|
|
|
863
898
|
expect(formState.firstName.value).toEqual("first");
|
|
864
899
|
});
|
|
865
900
|
it("can set computeds that have setters", () => {
|
|
866
|
-
const formState =
|
|
901
|
+
const formState = objectField_1.createObjectState({
|
|
867
902
|
firstName: { type: "value" },
|
|
868
903
|
lastName: { type: "value" },
|
|
869
904
|
fullName: { type: "value", computed: true },
|
|
@@ -876,7 +911,7 @@ describe("formState", () => {
|
|
|
876
911
|
expect(formState.firstName.value).toEqual("first");
|
|
877
912
|
});
|
|
878
913
|
it("can mark fields as read only", () => {
|
|
879
|
-
const formState =
|
|
914
|
+
const formState = objectField_1.createObjectState({
|
|
880
915
|
firstName: { type: "value" },
|
|
881
916
|
lastName: { type: "value" },
|
|
882
917
|
fullName: { type: "value", readOnly: true },
|
|
@@ -889,7 +924,7 @@ describe("formState", () => {
|
|
|
889
924
|
expect(formState.firstName.value).toEqual("first");
|
|
890
925
|
});
|
|
891
926
|
it("lets rules validate against other fields", () => {
|
|
892
|
-
const formState =
|
|
927
|
+
const formState = objectField_1.createObjectState({
|
|
893
928
|
firstName: { type: "value", rules: [] },
|
|
894
929
|
lastName: {
|
|
895
930
|
type: "value",
|
|
@@ -908,7 +943,7 @@ describe("formState", () => {
|
|
|
908
943
|
});
|
|
909
944
|
it("can return only changed primitive fields", () => {
|
|
910
945
|
// Given an author
|
|
911
|
-
const formState =
|
|
946
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, {
|
|
912
947
|
id: "a:1",
|
|
913
948
|
firstName: "f",
|
|
914
949
|
lastName: "l",
|
|
@@ -926,7 +961,7 @@ describe("formState", () => {
|
|
|
926
961
|
});
|
|
927
962
|
it("can return only changed object fields", () => {
|
|
928
963
|
// Given an author with an address object
|
|
929
|
-
const formState =
|
|
964
|
+
const formState = objectField_1.createObjectState(authorWithAddressConfig, {
|
|
930
965
|
id: "a:1",
|
|
931
966
|
firstName: "f",
|
|
932
967
|
address: { city: "c1", street: "s1" },
|
|
@@ -943,7 +978,7 @@ describe("formState", () => {
|
|
|
943
978
|
});
|
|
944
979
|
it("changedValue skips empty nested fields", () => {
|
|
945
980
|
// Given a new author with an address object
|
|
946
|
-
const formState =
|
|
981
|
+
const formState = objectField_1.createObjectState(authorWithAddressConfig, {
|
|
947
982
|
firstName: "f",
|
|
948
983
|
// And nothing is in the address
|
|
949
984
|
address: {},
|
|
@@ -954,7 +989,7 @@ describe("formState", () => {
|
|
|
954
989
|
});
|
|
955
990
|
it("changedValue skips effectively empty nested fields", () => {
|
|
956
991
|
// Given a new author with an address object
|
|
957
|
-
const formState =
|
|
992
|
+
const formState = objectField_1.createObjectState(authorWithAddressConfig, {
|
|
958
993
|
firstName: "f",
|
|
959
994
|
// And only undefined keys are in the address
|
|
960
995
|
address: { city: undefined },
|
|
@@ -965,7 +1000,7 @@ describe("formState", () => {
|
|
|
965
1000
|
});
|
|
966
1001
|
it("can return only changed list fields", () => {
|
|
967
1002
|
// Given an author with some books
|
|
968
|
-
const formState =
|
|
1003
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, {
|
|
969
1004
|
id: "a:1",
|
|
970
1005
|
firstName: "f",
|
|
971
1006
|
books: [
|
|
@@ -986,7 +1021,7 @@ describe("formState", () => {
|
|
|
986
1021
|
});
|
|
987
1022
|
it("can return only changed but incremental list fields", () => {
|
|
988
1023
|
// Given an author
|
|
989
|
-
const formState =
|
|
1024
|
+
const formState = objectField_1.createObjectState({
|
|
990
1025
|
id: { type: "value" },
|
|
991
1026
|
// And the books collection is marked as incremental (i.e. line items)
|
|
992
1027
|
books: {
|
|
@@ -994,7 +1029,7 @@ describe("formState", () => {
|
|
|
994
1029
|
update: "incremental",
|
|
995
1030
|
config: {
|
|
996
1031
|
id: { type: "value" },
|
|
997
|
-
title: { type: "value", rules: [
|
|
1032
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
998
1033
|
},
|
|
999
1034
|
},
|
|
1000
1035
|
}, {
|
|
@@ -1017,14 +1052,14 @@ describe("formState", () => {
|
|
|
1017
1052
|
});
|
|
1018
1053
|
it("uses the op key as a hint to use incremental list behavior", () => {
|
|
1019
1054
|
// Given an author
|
|
1020
|
-
const formState =
|
|
1055
|
+
const formState = objectField_1.createObjectState({
|
|
1021
1056
|
id: { type: "value" },
|
|
1022
1057
|
// And the books collection is not explicitly marked as incremental
|
|
1023
1058
|
books: {
|
|
1024
1059
|
type: "list",
|
|
1025
1060
|
config: {
|
|
1026
1061
|
id: { type: "value" },
|
|
1027
|
-
title: { type: "value", rules: [
|
|
1062
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
1028
1063
|
// But it does have an op key
|
|
1029
1064
|
op: { type: "value" },
|
|
1030
1065
|
},
|
|
@@ -1050,14 +1085,14 @@ describe("formState", () => {
|
|
|
1050
1085
|
});
|
|
1051
1086
|
it("defaults the incremental op key to included when not set", () => {
|
|
1052
1087
|
// Given an author
|
|
1053
|
-
const formState =
|
|
1088
|
+
const formState = objectField_1.createObjectState({
|
|
1054
1089
|
id: { type: "value" },
|
|
1055
1090
|
// And the books collection is not explicitly marked as incremental
|
|
1056
1091
|
books: {
|
|
1057
1092
|
type: "list",
|
|
1058
1093
|
config: {
|
|
1059
1094
|
id: { type: "value" },
|
|
1060
|
-
title: { type: "value", rules: [
|
|
1095
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
1061
1096
|
// But it does have an op key
|
|
1062
1097
|
op: { type: "value" },
|
|
1063
1098
|
},
|
|
@@ -1091,7 +1126,7 @@ describe("formState", () => {
|
|
|
1091
1126
|
});
|
|
1092
1127
|
});
|
|
1093
1128
|
it("can observe value changes", () => {
|
|
1094
|
-
const formState =
|
|
1129
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f" });
|
|
1095
1130
|
let ticks = 0;
|
|
1096
1131
|
mobx_1.reaction(() => formState.value, () => ticks++, { equals: () => false });
|
|
1097
1132
|
expect(ticks).toEqual(0);
|
|
@@ -1101,7 +1136,7 @@ describe("formState", () => {
|
|
|
1101
1136
|
expect(ticks).toEqual(1);
|
|
1102
1137
|
});
|
|
1103
1138
|
it("does not override a focused, changed field", () => {
|
|
1104
|
-
const formState =
|
|
1139
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1105
1140
|
// Given first name is focused
|
|
1106
1141
|
formState.firstName.focus();
|
|
1107
1142
|
// And they have wip edits
|
|
@@ -1116,7 +1151,7 @@ describe("formState", () => {
|
|
|
1116
1151
|
expect(formState.firstName.value).toEqual("fff");
|
|
1117
1152
|
});
|
|
1118
1153
|
it("does update a focused, unchanged field", () => {
|
|
1119
|
-
const formState =
|
|
1154
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1120
1155
|
// Given first name is focused, but has not changed
|
|
1121
1156
|
formState.firstName.focus();
|
|
1122
1157
|
// When a mutation result sets both firstName and lastName
|
|
@@ -1125,7 +1160,7 @@ describe("formState", () => {
|
|
|
1125
1160
|
expect(formState.firstName.value).toEqual("f2");
|
|
1126
1161
|
});
|
|
1127
1162
|
it("trims string values on blur", () => {
|
|
1128
|
-
const formState =
|
|
1163
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1129
1164
|
// Given the user is typing with spaces
|
|
1130
1165
|
formState.firstName.focus();
|
|
1131
1166
|
formState.firstName.set("f ");
|
|
@@ -1137,7 +1172,7 @@ describe("formState", () => {
|
|
|
1137
1172
|
expect(formState.firstName.value).toEqual("f");
|
|
1138
1173
|
});
|
|
1139
1174
|
it("trims string values on maybeAutoSave", () => {
|
|
1140
|
-
const formState =
|
|
1175
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1141
1176
|
// Given the user is typing with spaces
|
|
1142
1177
|
formState.firstName.focus();
|
|
1143
1178
|
formState.firstName.set("f ");
|
|
@@ -1151,7 +1186,7 @@ describe("formState", () => {
|
|
|
1151
1186
|
expect(formState.firstName.touched).toEqual(true);
|
|
1152
1187
|
});
|
|
1153
1188
|
it("trims empty values to undefined on blur", () => {
|
|
1154
|
-
const formState =
|
|
1189
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1155
1190
|
// Given the user is typing with only spaces
|
|
1156
1191
|
formState.firstName.focus();
|
|
1157
1192
|
formState.firstName.set(" ");
|
|
@@ -1169,7 +1204,7 @@ describe("formState", () => {
|
|
|
1169
1204
|
});
|
|
1170
1205
|
it("can work with both required inputs and optional fields", () => {
|
|
1171
1206
|
// And we drop it in a form.
|
|
1172
|
-
const form =
|
|
1207
|
+
const form = objectField_1.createObjectState({
|
|
1173
1208
|
id: { type: "value" },
|
|
1174
1209
|
firstName: { type: "value" },
|
|
1175
1210
|
}, { id: "a:1" });
|
|
@@ -1190,7 +1225,7 @@ describe("formState", () => {
|
|
|
1190
1225
|
});
|
|
1191
1226
|
it("can have child object states passed in as field states", () => {
|
|
1192
1227
|
// Given an author
|
|
1193
|
-
const a =
|
|
1228
|
+
const a = objectField_1.createObjectState({
|
|
1194
1229
|
id: { type: "value" },
|
|
1195
1230
|
// And address is modeled as a value object
|
|
1196
1231
|
address: { type: "value" },
|
|
@@ -1207,7 +1242,7 @@ describe("formState", () => {
|
|
|
1207
1242
|
});
|
|
1208
1243
|
it("provides isNewEntity", () => {
|
|
1209
1244
|
// Given an author without an id
|
|
1210
|
-
const formState =
|
|
1245
|
+
const formState = objectField_1.createObjectState(authorWithAddressConfig, {
|
|
1211
1246
|
firstName: "f",
|
|
1212
1247
|
address: { city: "c1", street: "s1" },
|
|
1213
1248
|
});
|
|
@@ -1222,7 +1257,7 @@ describe("formState", () => {
|
|
|
1222
1257
|
});
|
|
1223
1258
|
it("isNewEntity is false if there is no id field", () => {
|
|
1224
1259
|
// Given an author without an id field defined
|
|
1225
|
-
const formState =
|
|
1260
|
+
const formState = objectField_1.createObjectState({
|
|
1226
1261
|
firstName: { type: "value" },
|
|
1227
1262
|
address: { type: "object", config: { street: { type: "value" } } },
|
|
1228
1263
|
}, {});
|
|
@@ -1232,7 +1267,7 @@ describe("formState", () => {
|
|
|
1232
1267
|
});
|
|
1233
1268
|
it("provides isNewEntity for lists", () => {
|
|
1234
1269
|
// Given an author & books without an id
|
|
1235
|
-
const formState =
|
|
1270
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, {
|
|
1236
1271
|
books: [{ title: "t1" }, { title: "t2" }],
|
|
1237
1272
|
});
|
|
1238
1273
|
// Then both books know they are new
|
|
@@ -1300,13 +1335,13 @@ const authorWithBooksConfig = {
|
|
|
1300
1335
|
type: "list",
|
|
1301
1336
|
config: {
|
|
1302
1337
|
id: { type: "value" },
|
|
1303
|
-
title: { type: "value", rules: [
|
|
1338
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
1304
1339
|
classification: { type: "value" },
|
|
1305
1340
|
},
|
|
1306
1341
|
},
|
|
1307
1342
|
};
|
|
1308
1343
|
function createAuthorInputState(input, maybeAutoSave) {
|
|
1309
|
-
return
|
|
1344
|
+
return objectField_1.createObjectState(authorWithBooksConfig, input, { maybeAutoSave });
|
|
1310
1345
|
}
|
|
1311
1346
|
const authorWithAddressConfig = {
|
|
1312
1347
|
id: { type: "value" },
|
|
@@ -1315,7 +1350,7 @@ const authorWithAddressConfig = {
|
|
|
1315
1350
|
address: {
|
|
1316
1351
|
type: "object",
|
|
1317
1352
|
config: {
|
|
1318
|
-
street: { type: "value", rules: [
|
|
1353
|
+
street: { type: "value", rules: [rules_1.required] },
|
|
1319
1354
|
city: { type: "value" },
|
|
1320
1355
|
},
|
|
1321
1356
|
},
|
|
@@ -1328,7 +1363,7 @@ const authorWithAddressAndBooksConfig = {
|
|
|
1328
1363
|
type: "object",
|
|
1329
1364
|
config: {
|
|
1330
1365
|
id: { type: "value" },
|
|
1331
|
-
street: { type: "value", rules: [
|
|
1366
|
+
street: { type: "value", rules: [rules_1.required] },
|
|
1332
1367
|
city: { type: "value" },
|
|
1333
1368
|
},
|
|
1334
1369
|
},
|
|
@@ -1336,11 +1371,11 @@ const authorWithAddressAndBooksConfig = {
|
|
|
1336
1371
|
type: "list",
|
|
1337
1372
|
config: {
|
|
1338
1373
|
id: { type: "value" },
|
|
1339
|
-
title: { type: "value", rules: [
|
|
1374
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
1340
1375
|
classification: { type: "value" },
|
|
1341
1376
|
},
|
|
1342
1377
|
},
|
|
1343
1378
|
};
|
|
1344
1379
|
function createAuthorWithAddressInputState(input) {
|
|
1345
|
-
return
|
|
1380
|
+
return objectField_1.createObjectState(authorWithAddressConfig, input);
|
|
1346
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,20 @@
|
|
|
1
|
-
import { ObjectConfig
|
|
1
|
+
import { ObjectConfig } from "./config";
|
|
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
|
+
};
|
|
2
18
|
export declare type UseFormStateOpts<T, I> = {
|
|
3
19
|
/** The form configuration, should be a module-level const or useMemo'd. */
|
|
4
20
|
config: ObjectConfig<T>;
|
|
@@ -19,11 +35,7 @@ export declare type UseFormStateOpts<T, I> = {
|
|
|
19
35
|
* only call `init.map` if it's set, otherwise we'll use `init.ifDefined` or `{}`, saving you
|
|
20
36
|
* from having to null check within your `init.map` function.
|
|
21
37
|
*/
|
|
22
|
-
init?: T |
|
|
23
|
-
input: I;
|
|
24
|
-
map: (input: Exclude<I, null | undefined>) => T;
|
|
25
|
-
ifUndefined?: T;
|
|
26
|
-
};
|
|
38
|
+
init?: T | InputAndMap<T, I> | QueryAndMap<T, I>;
|
|
27
39
|
/**
|
|
28
40
|
* A hook to add custom, cross-field validation rules that can be difficult to setup directly in the config DSL.
|
|
29
41
|
*
|
|
@@ -32,6 +44,13 @@ export declare type UseFormStateOpts<T, I> = {
|
|
|
32
44
|
addRules?: (state: ObjectState<T>) => void;
|
|
33
45
|
/** Whether the form should be read only, when changed it won't re-create the whole form. */
|
|
34
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;
|
|
35
54
|
/**
|
|
36
55
|
* Fired when the form should auto-save, i.e. after a) blur and b) all fields are valid.
|
|
37
56
|
*
|