@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.
@@ -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 formState_1 = require("./formState");
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 = formState_1.createObjectState({ title: { type: "value", rules: [formState_1.required] } }, {});
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 = formState_1.createObjectState({ title: { type: "value", rules: [formState_1.required] } }, { title: "b1" });
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 = formState_1.createObjectState({
36
- title: { type: "value", rules: [formState_1.required] },
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 = formState_1.createObjectState({ title: { type: "value", rules: [formState_1.required] } }, { title: "initial valid title" });
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 = formState_1.createObjectState({ title: { type: "value" } }, { title: "b1" });
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 = formState_1.createObjectState({ title: { type: "value" } }, { title: "b1" });
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 = formState_1.createObjectState({
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 = formState_1.createObjectState(config, a1);
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 = formState_1.createObjectState({
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 = formState_1.createObjectState({ firstName: { type: "value", rules: [rule] } }, {});
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 = formState_1.createObjectState({ firstName: { type: "value", rules: [rule] } }, {});
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 = formState_1.createObjectState({
351
- title: { type: "value", rules: [formState_1.required] },
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 = formState_1.createObjectState({ favoriteColors: { type: "value" } }, {});
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 = formState_1.createObjectState({ favoriteColors: { type: "value" } }, { favoriteColors: [] });
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 = formState_1.createObjectState({
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 = formState_1.createObjectState({ title: { type: "value", rules: [formState_1.required], readOnly: true } }, {});
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 = formState_1.createObjectState({
691
- firstName: { type: "value", rules: [formState_1.required] },
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 = formState_1.createObjectState({
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 = formState_1.createObjectState({
784
+ const a = objectField_1.createObjectState({
714
785
  books: {
715
786
  type: "list",
716
- config: { title: { type: "value", rules: [formState_1.required] } },
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 = formState_1.createObjectState({
801
+ const a = objectField_1.createObjectState({
731
802
  books: {
732
803
  type: "list",
733
804
  config: {
734
- title: { type: "value", rules: [formState_1.required] },
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 = formState_1.createObjectState({
820
+ const a = objectField_1.createObjectState({
750
821
  books: {
751
822
  type: "list",
752
823
  config: {
753
- title: { type: "value", rules: [formState_1.required] },
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 = formState_1.createObjectState({
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 = formState_1.createObjectState({
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 = formState_1.createObjectState({
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 = formState_1.createObjectState({
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 = formState_1.createObjectState(authorWithBooksConfig, {
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 = formState_1.createObjectState(authorWithAddressConfig, {
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 = formState_1.createObjectState(authorWithAddressConfig, {
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 = formState_1.createObjectState(authorWithAddressConfig, {
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 = formState_1.createObjectState(authorWithBooksConfig, {
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 = formState_1.createObjectState({
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: [formState_1.required] },
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 = formState_1.createObjectState({
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: [formState_1.required] },
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 = formState_1.createObjectState({
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: [formState_1.required] },
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 = formState_1.createObjectState(authorWithBooksConfig, { firstName: "f" });
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 = formState_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
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 = formState_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
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 = formState_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
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 = formState_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
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 = formState_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
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 = formState_1.createObjectState({
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 = formState_1.createObjectState({
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 = formState_1.createObjectState(authorWithAddressConfig, {
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 = formState_1.createObjectState({
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 = formState_1.createObjectState(authorWithBooksConfig, {
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: [formState_1.required] },
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 formState_1.createObjectState(authorWithBooksConfig, input, { maybeAutoSave });
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: [formState_1.required] },
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: [formState_1.required] },
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: [formState_1.required] },
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 formState_1.createObjectState(authorWithAddressConfig, input);
1380
+ return objectField_1.createObjectState(authorWithAddressConfig, input);
1310
1381
  }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,7 @@
1
- export { createObjectState, FieldState, ListFieldState, ObjectConfig, ObjectState, required, Rule, } from "./formState";
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 formState_1 = require("./formState");
5
- Object.defineProperty(exports, "createObjectState", { enumerable: true, get: function () { return formState_1.createObjectState; } });
6
- Object.defineProperty(exports, "required", { enumerable: true, get: function () { return formState_1.required; } });
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");
@@ -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
+ });
@@ -1,4 +1,5 @@
1
- import { ObjectConfig, ObjectState } from "./formState";
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>;
@@ -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 formState_1 = require("./formState");
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 = formState_1.createObjectState(config, value, { maybeAutoSave });
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