@homebound/form-state 2.10.0 → 2.10.1
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 +205 -0
- package/dist/fields/objectField.d.ts +54 -0
- package/dist/fields/objectField.js +180 -0
- package/dist/fields/valueField.d.ts +58 -0
- package/dist/fields/valueField.js +149 -0
- package/dist/formState.test.js +61 -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 +2 -2
- package/dist/useFormState.test.js +4 -4
- 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 +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 -559
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.newValueFieldState = void 0;
|
|
4
|
+
const is_plain_object_1 = require("is-plain-object");
|
|
5
|
+
const mobx_1 = require("mobx");
|
|
6
|
+
const rules_1 = require("../rules");
|
|
7
|
+
const utils_1 = require("../utils");
|
|
8
|
+
function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, isDeleteKey, isReadOnlyKey, computed, readOnly, maybeAutoSave) {
|
|
9
|
+
// keep a copy here for reference equality
|
|
10
|
+
const value = parentInstance[key];
|
|
11
|
+
let _originalValue = value === null ? undefined : is_plain_object_1.isPlainObject(value) ? mobx_1.toJS(value) : value;
|
|
12
|
+
// Because we read/write the value directly back into parentInstance[key],
|
|
13
|
+
// which itself is not a proxy, we use this as our "value changed" trigger.
|
|
14
|
+
const _tick = mobx_1.observable({ value: 1 });
|
|
15
|
+
const _originalValueTick = mobx_1.observable({ value: 1 });
|
|
16
|
+
const field = {
|
|
17
|
+
key,
|
|
18
|
+
touched: false,
|
|
19
|
+
/** Current readOnly value. */
|
|
20
|
+
_readOnly: readOnly || false,
|
|
21
|
+
_focused: false,
|
|
22
|
+
_isIdKey: isIdKey,
|
|
23
|
+
_isDeleteKey: isDeleteKey,
|
|
24
|
+
_isReadOnlyKey: isReadOnlyKey,
|
|
25
|
+
rules,
|
|
26
|
+
get value() {
|
|
27
|
+
const value = _tick.value > 0 ? parentInstance[key] : utils_1.fail();
|
|
28
|
+
// Re-create the `keepNull` logic on sets but for our initial read where our
|
|
29
|
+
// originalValue is null (empty) but we want to expose it as undefined for
|
|
30
|
+
// consistency of "empty-ness" to our UI components.
|
|
31
|
+
return value === null && utils_1.isEmpty(_originalValue) ? undefined : value;
|
|
32
|
+
},
|
|
33
|
+
set value(v) {
|
|
34
|
+
this.set(v);
|
|
35
|
+
},
|
|
36
|
+
get dirty() {
|
|
37
|
+
return !utils_1.areEqual(this.originalValue, this.value);
|
|
38
|
+
},
|
|
39
|
+
/** Returns whether this field is readOnly, although if our parent is readOnly then it trumps. */
|
|
40
|
+
get readOnly() {
|
|
41
|
+
return parentState().readOnly || this._readOnly;
|
|
42
|
+
},
|
|
43
|
+
/** Sets the field readOnly, but `v` will still be or-d with the parent's readOnly state. */
|
|
44
|
+
set readOnly(v) {
|
|
45
|
+
this._readOnly = v;
|
|
46
|
+
},
|
|
47
|
+
// For primitive fields, the changed value is just the value.
|
|
48
|
+
get changedValue() {
|
|
49
|
+
return this.value;
|
|
50
|
+
},
|
|
51
|
+
get valid() {
|
|
52
|
+
const opts = { value: this.value, key: key, originalValue: this.originalValue, object: parentState() };
|
|
53
|
+
return this.rules.every((r) => r(opts) === undefined);
|
|
54
|
+
},
|
|
55
|
+
get errors() {
|
|
56
|
+
const opts = { value: this.value, key: key, originalValue: this.originalValue, object: parentState() };
|
|
57
|
+
return this.rules.map((r) => r(opts)).filter(utils_1.isNotUndefined);
|
|
58
|
+
},
|
|
59
|
+
get required() {
|
|
60
|
+
return this.rules.some((rule) => rule === rules_1.required);
|
|
61
|
+
},
|
|
62
|
+
get isNewEntity() {
|
|
63
|
+
return parentState().isNewEntity;
|
|
64
|
+
},
|
|
65
|
+
focus() {
|
|
66
|
+
this._focused = true;
|
|
67
|
+
},
|
|
68
|
+
blur() {
|
|
69
|
+
this.maybeAutoSave();
|
|
70
|
+
this._focused = false;
|
|
71
|
+
},
|
|
72
|
+
maybeAutoSave() {
|
|
73
|
+
// Now that the user is done editing the field (note maybe w/o blurring, i.e. if they hit enter), we sneak in some trim logic
|
|
74
|
+
this.maybeTrim();
|
|
75
|
+
// touched is readonly, but we're allowed to change it
|
|
76
|
+
this.touched = true;
|
|
77
|
+
maybeAutoSave();
|
|
78
|
+
},
|
|
79
|
+
set(value, opts = {}) {
|
|
80
|
+
if (this.readOnly && !opts.resetting && !opts.refreshing) {
|
|
81
|
+
throw new Error(`${key} is currently readOnly`);
|
|
82
|
+
}
|
|
83
|
+
if (opts.refreshing && this.dirty && this.value !== value) {
|
|
84
|
+
// Ignore incoming values if we have changes (this.dirty) unless our latest change (this.value)
|
|
85
|
+
// matches the incoming value (value), b/c if it does we should accept it and reset originalValue
|
|
86
|
+
// so that we're not longer dirty.
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
else if (computed && (opts.resetting || opts.refreshing)) {
|
|
90
|
+
// Computeds can't be either reset or refreshed
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// If the user has deleted/emptied a value that was originally set, keep it as `null`
|
|
94
|
+
// so that our partial update to the backend correctly unsets it.
|
|
95
|
+
const keepNull = !utils_1.isEmpty(this.originalValue) && utils_1.isEmpty(value);
|
|
96
|
+
// If a list of primitives was originally undefined, coerce `[]` to `undefined`
|
|
97
|
+
const coerceEmptyList = value && value instanceof Array && value.length === 0 && utils_1.isEmpty(this.originalValue);
|
|
98
|
+
const newValue = keepNull ? null : utils_1.isEmpty(value) || coerceEmptyList ? undefined : value;
|
|
99
|
+
// Set the value on our parent object
|
|
100
|
+
const changed = !utils_1.areEqual(newValue, this.value);
|
|
101
|
+
parentInstance[key] = newValue;
|
|
102
|
+
_tick.value++;
|
|
103
|
+
if (opts.refreshing) {
|
|
104
|
+
this.originalValue = newValue;
|
|
105
|
+
}
|
|
106
|
+
// If we're being set programmatically, i.e. we don't currently have focus,
|
|
107
|
+
// call blur to trigger any auto-saves.
|
|
108
|
+
if (!this._focused && !opts.refreshing && !opts.resetting && this.dirty && changed && opts.autoSave !== false) {
|
|
109
|
+
this.maybeAutoSave();
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
reset() {
|
|
113
|
+
if (!computed) {
|
|
114
|
+
this.set(this.originalValue, { resetting: true });
|
|
115
|
+
}
|
|
116
|
+
this.touched = false;
|
|
117
|
+
},
|
|
118
|
+
save() {
|
|
119
|
+
if (is_plain_object_1.isPlainObject(this.originalValue)) {
|
|
120
|
+
this.originalValue = mobx_1.toJS(this.value);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
this.originalValue = this.value;
|
|
124
|
+
}
|
|
125
|
+
this.touched = false;
|
|
126
|
+
},
|
|
127
|
+
get originalValue() {
|
|
128
|
+
// A dummy check to for reactivity around our non-proxy value
|
|
129
|
+
return _originalValueTick.value > -1 ? _originalValue : _originalValue;
|
|
130
|
+
},
|
|
131
|
+
set originalValue(v) {
|
|
132
|
+
_originalValue = v;
|
|
133
|
+
_originalValueTick.value++;
|
|
134
|
+
},
|
|
135
|
+
maybeTrim() {
|
|
136
|
+
if (typeof this.value === "string") {
|
|
137
|
+
let newValue = this.value.trim();
|
|
138
|
+
if (newValue === "") {
|
|
139
|
+
newValue = undefined;
|
|
140
|
+
}
|
|
141
|
+
if (newValue !== this.value) {
|
|
142
|
+
this.set(newValue);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
return field;
|
|
148
|
+
}
|
|
149
|
+
exports.newValueFieldState = newValueFieldState;
|
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
|
});
|
|
@@ -723,15 +724,15 @@ describe("formState", () => {
|
|
|
723
724
|
expect(a1.firstName.readOnly).toBeTruthy();
|
|
724
725
|
});
|
|
725
726
|
it("canSave returns dirty and touches", () => {
|
|
726
|
-
const a1 =
|
|
727
|
-
firstName: { type: "value", rules: [
|
|
727
|
+
const a1 = objectField_1.createObjectState({
|
|
728
|
+
firstName: { type: "value", rules: [rules_1.required] },
|
|
728
729
|
}, {});
|
|
729
730
|
expect(a1.firstName.touched).toBeFalsy();
|
|
730
731
|
expect(a1.canSave()).toBeFalsy();
|
|
731
732
|
expect(a1.firstName.touched).toBeTruthy();
|
|
732
733
|
});
|
|
733
734
|
it("uses toJSON if available for dirty checks", () => {
|
|
734
|
-
const a1 =
|
|
735
|
+
const a1 = objectField_1.createObjectState({
|
|
735
736
|
birthday: { type: "value" },
|
|
736
737
|
}, { birthday: new formStateDomain_1.DateOnly(formStateDomain_1.jan1) });
|
|
737
738
|
a1.birthday.set(new formStateDomain_1.DateOnly(formStateDomain_1.jan1));
|
|
@@ -746,10 +747,10 @@ describe("formState", () => {
|
|
|
746
747
|
expect(a.valid).toBeTruthy();
|
|
747
748
|
});
|
|
748
749
|
it("can validate a list input", () => {
|
|
749
|
-
const a =
|
|
750
|
+
const a = objectField_1.createObjectState({
|
|
750
751
|
books: {
|
|
751
752
|
type: "list",
|
|
752
|
-
config: { title: { type: "value", rules: [
|
|
753
|
+
config: { title: { type: "value", rules: [rules_1.required] } },
|
|
753
754
|
rules: [({ value }) => (value.find((b) => b.title.value === "t1") ? "Cannot have t1" : undefined)],
|
|
754
755
|
},
|
|
755
756
|
}, {});
|
|
@@ -763,11 +764,11 @@ describe("formState", () => {
|
|
|
763
764
|
expect(lastErrors).toEqual("");
|
|
764
765
|
});
|
|
765
766
|
it("can ignore a deleted list entry", () => {
|
|
766
|
-
const a =
|
|
767
|
+
const a = objectField_1.createObjectState({
|
|
767
768
|
books: {
|
|
768
769
|
type: "list",
|
|
769
770
|
config: {
|
|
770
|
-
title: { type: "value", rules: [
|
|
771
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
771
772
|
delete: { type: "value", isDeleteKey: true },
|
|
772
773
|
},
|
|
773
774
|
},
|
|
@@ -782,11 +783,11 @@ describe("formState", () => {
|
|
|
782
783
|
expect(a.books.valid).toBeTruthy();
|
|
783
784
|
});
|
|
784
785
|
it("can treat a list entry as read-only", () => {
|
|
785
|
-
const a =
|
|
786
|
+
const a = objectField_1.createObjectState({
|
|
786
787
|
books: {
|
|
787
788
|
type: "list",
|
|
788
789
|
config: {
|
|
789
|
-
title: { type: "value", rules: [
|
|
790
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
790
791
|
isPublished: { type: "value", isReadOnlyKey: true },
|
|
791
792
|
},
|
|
792
793
|
},
|
|
@@ -850,7 +851,7 @@ describe("formState", () => {
|
|
|
850
851
|
expect(config).toBeDefined();
|
|
851
852
|
});
|
|
852
853
|
it("can reset observable objects with computeds", () => {
|
|
853
|
-
const formState =
|
|
854
|
+
const formState = objectField_1.createObjectState({
|
|
854
855
|
firstName: { type: "value" },
|
|
855
856
|
lastName: { type: "value" },
|
|
856
857
|
fullName: { type: "value", computed: true },
|
|
@@ -863,7 +864,7 @@ describe("formState", () => {
|
|
|
863
864
|
expect(formState.firstName.value).toEqual("first");
|
|
864
865
|
});
|
|
865
866
|
it("can set computeds that have setters", () => {
|
|
866
|
-
const formState =
|
|
867
|
+
const formState = objectField_1.createObjectState({
|
|
867
868
|
firstName: { type: "value" },
|
|
868
869
|
lastName: { type: "value" },
|
|
869
870
|
fullName: { type: "value", computed: true },
|
|
@@ -876,7 +877,7 @@ describe("formState", () => {
|
|
|
876
877
|
expect(formState.firstName.value).toEqual("first");
|
|
877
878
|
});
|
|
878
879
|
it("can mark fields as read only", () => {
|
|
879
|
-
const formState =
|
|
880
|
+
const formState = objectField_1.createObjectState({
|
|
880
881
|
firstName: { type: "value" },
|
|
881
882
|
lastName: { type: "value" },
|
|
882
883
|
fullName: { type: "value", readOnly: true },
|
|
@@ -889,7 +890,7 @@ describe("formState", () => {
|
|
|
889
890
|
expect(formState.firstName.value).toEqual("first");
|
|
890
891
|
});
|
|
891
892
|
it("lets rules validate against other fields", () => {
|
|
892
|
-
const formState =
|
|
893
|
+
const formState = objectField_1.createObjectState({
|
|
893
894
|
firstName: { type: "value", rules: [] },
|
|
894
895
|
lastName: {
|
|
895
896
|
type: "value",
|
|
@@ -908,7 +909,7 @@ describe("formState", () => {
|
|
|
908
909
|
});
|
|
909
910
|
it("can return only changed primitive fields", () => {
|
|
910
911
|
// Given an author
|
|
911
|
-
const formState =
|
|
912
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, {
|
|
912
913
|
id: "a:1",
|
|
913
914
|
firstName: "f",
|
|
914
915
|
lastName: "l",
|
|
@@ -926,7 +927,7 @@ describe("formState", () => {
|
|
|
926
927
|
});
|
|
927
928
|
it("can return only changed object fields", () => {
|
|
928
929
|
// Given an author with an address object
|
|
929
|
-
const formState =
|
|
930
|
+
const formState = objectField_1.createObjectState(authorWithAddressConfig, {
|
|
930
931
|
id: "a:1",
|
|
931
932
|
firstName: "f",
|
|
932
933
|
address: { city: "c1", street: "s1" },
|
|
@@ -943,7 +944,7 @@ describe("formState", () => {
|
|
|
943
944
|
});
|
|
944
945
|
it("changedValue skips empty nested fields", () => {
|
|
945
946
|
// Given a new author with an address object
|
|
946
|
-
const formState =
|
|
947
|
+
const formState = objectField_1.createObjectState(authorWithAddressConfig, {
|
|
947
948
|
firstName: "f",
|
|
948
949
|
// And nothing is in the address
|
|
949
950
|
address: {},
|
|
@@ -954,7 +955,7 @@ describe("formState", () => {
|
|
|
954
955
|
});
|
|
955
956
|
it("changedValue skips effectively empty nested fields", () => {
|
|
956
957
|
// Given a new author with an address object
|
|
957
|
-
const formState =
|
|
958
|
+
const formState = objectField_1.createObjectState(authorWithAddressConfig, {
|
|
958
959
|
firstName: "f",
|
|
959
960
|
// And only undefined keys are in the address
|
|
960
961
|
address: { city: undefined },
|
|
@@ -965,7 +966,7 @@ describe("formState", () => {
|
|
|
965
966
|
});
|
|
966
967
|
it("can return only changed list fields", () => {
|
|
967
968
|
// Given an author with some books
|
|
968
|
-
const formState =
|
|
969
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, {
|
|
969
970
|
id: "a:1",
|
|
970
971
|
firstName: "f",
|
|
971
972
|
books: [
|
|
@@ -986,7 +987,7 @@ describe("formState", () => {
|
|
|
986
987
|
});
|
|
987
988
|
it("can return only changed but incremental list fields", () => {
|
|
988
989
|
// Given an author
|
|
989
|
-
const formState =
|
|
990
|
+
const formState = objectField_1.createObjectState({
|
|
990
991
|
id: { type: "value" },
|
|
991
992
|
// And the books collection is marked as incremental (i.e. line items)
|
|
992
993
|
books: {
|
|
@@ -994,7 +995,7 @@ describe("formState", () => {
|
|
|
994
995
|
update: "incremental",
|
|
995
996
|
config: {
|
|
996
997
|
id: { type: "value" },
|
|
997
|
-
title: { type: "value", rules: [
|
|
998
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
998
999
|
},
|
|
999
1000
|
},
|
|
1000
1001
|
}, {
|
|
@@ -1017,14 +1018,14 @@ describe("formState", () => {
|
|
|
1017
1018
|
});
|
|
1018
1019
|
it("uses the op key as a hint to use incremental list behavior", () => {
|
|
1019
1020
|
// Given an author
|
|
1020
|
-
const formState =
|
|
1021
|
+
const formState = objectField_1.createObjectState({
|
|
1021
1022
|
id: { type: "value" },
|
|
1022
1023
|
// And the books collection is not explicitly marked as incremental
|
|
1023
1024
|
books: {
|
|
1024
1025
|
type: "list",
|
|
1025
1026
|
config: {
|
|
1026
1027
|
id: { type: "value" },
|
|
1027
|
-
title: { type: "value", rules: [
|
|
1028
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
1028
1029
|
// But it does have an op key
|
|
1029
1030
|
op: { type: "value" },
|
|
1030
1031
|
},
|
|
@@ -1050,14 +1051,14 @@ describe("formState", () => {
|
|
|
1050
1051
|
});
|
|
1051
1052
|
it("defaults the incremental op key to included when not set", () => {
|
|
1052
1053
|
// Given an author
|
|
1053
|
-
const formState =
|
|
1054
|
+
const formState = objectField_1.createObjectState({
|
|
1054
1055
|
id: { type: "value" },
|
|
1055
1056
|
// And the books collection is not explicitly marked as incremental
|
|
1056
1057
|
books: {
|
|
1057
1058
|
type: "list",
|
|
1058
1059
|
config: {
|
|
1059
1060
|
id: { type: "value" },
|
|
1060
|
-
title: { type: "value", rules: [
|
|
1061
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
1061
1062
|
// But it does have an op key
|
|
1062
1063
|
op: { type: "value" },
|
|
1063
1064
|
},
|
|
@@ -1091,7 +1092,7 @@ describe("formState", () => {
|
|
|
1091
1092
|
});
|
|
1092
1093
|
});
|
|
1093
1094
|
it("can observe value changes", () => {
|
|
1094
|
-
const formState =
|
|
1095
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f" });
|
|
1095
1096
|
let ticks = 0;
|
|
1096
1097
|
mobx_1.reaction(() => formState.value, () => ticks++, { equals: () => false });
|
|
1097
1098
|
expect(ticks).toEqual(0);
|
|
@@ -1101,7 +1102,7 @@ describe("formState", () => {
|
|
|
1101
1102
|
expect(ticks).toEqual(1);
|
|
1102
1103
|
});
|
|
1103
1104
|
it("does not override a focused, changed field", () => {
|
|
1104
|
-
const formState =
|
|
1105
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1105
1106
|
// Given first name is focused
|
|
1106
1107
|
formState.firstName.focus();
|
|
1107
1108
|
// And they have wip edits
|
|
@@ -1116,7 +1117,7 @@ describe("formState", () => {
|
|
|
1116
1117
|
expect(formState.firstName.value).toEqual("fff");
|
|
1117
1118
|
});
|
|
1118
1119
|
it("does update a focused, unchanged field", () => {
|
|
1119
|
-
const formState =
|
|
1120
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1120
1121
|
// Given first name is focused, but has not changed
|
|
1121
1122
|
formState.firstName.focus();
|
|
1122
1123
|
// When a mutation result sets both firstName and lastName
|
|
@@ -1125,7 +1126,7 @@ describe("formState", () => {
|
|
|
1125
1126
|
expect(formState.firstName.value).toEqual("f2");
|
|
1126
1127
|
});
|
|
1127
1128
|
it("trims string values on blur", () => {
|
|
1128
|
-
const formState =
|
|
1129
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1129
1130
|
// Given the user is typing with spaces
|
|
1130
1131
|
formState.firstName.focus();
|
|
1131
1132
|
formState.firstName.set("f ");
|
|
@@ -1137,7 +1138,7 @@ describe("formState", () => {
|
|
|
1137
1138
|
expect(formState.firstName.value).toEqual("f");
|
|
1138
1139
|
});
|
|
1139
1140
|
it("trims string values on maybeAutoSave", () => {
|
|
1140
|
-
const formState =
|
|
1141
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1141
1142
|
// Given the user is typing with spaces
|
|
1142
1143
|
formState.firstName.focus();
|
|
1143
1144
|
formState.firstName.set("f ");
|
|
@@ -1151,7 +1152,7 @@ describe("formState", () => {
|
|
|
1151
1152
|
expect(formState.firstName.touched).toEqual(true);
|
|
1152
1153
|
});
|
|
1153
1154
|
it("trims empty values to undefined on blur", () => {
|
|
1154
|
-
const formState =
|
|
1155
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1155
1156
|
// Given the user is typing with only spaces
|
|
1156
1157
|
formState.firstName.focus();
|
|
1157
1158
|
formState.firstName.set(" ");
|
|
@@ -1169,7 +1170,7 @@ describe("formState", () => {
|
|
|
1169
1170
|
});
|
|
1170
1171
|
it("can work with both required inputs and optional fields", () => {
|
|
1171
1172
|
// And we drop it in a form.
|
|
1172
|
-
const form =
|
|
1173
|
+
const form = objectField_1.createObjectState({
|
|
1173
1174
|
id: { type: "value" },
|
|
1174
1175
|
firstName: { type: "value" },
|
|
1175
1176
|
}, { id: "a:1" });
|
|
@@ -1190,7 +1191,7 @@ describe("formState", () => {
|
|
|
1190
1191
|
});
|
|
1191
1192
|
it("can have child object states passed in as field states", () => {
|
|
1192
1193
|
// Given an author
|
|
1193
|
-
const a =
|
|
1194
|
+
const a = objectField_1.createObjectState({
|
|
1194
1195
|
id: { type: "value" },
|
|
1195
1196
|
// And address is modeled as a value object
|
|
1196
1197
|
address: { type: "value" },
|
|
@@ -1207,7 +1208,7 @@ describe("formState", () => {
|
|
|
1207
1208
|
});
|
|
1208
1209
|
it("provides isNewEntity", () => {
|
|
1209
1210
|
// Given an author without an id
|
|
1210
|
-
const formState =
|
|
1211
|
+
const formState = objectField_1.createObjectState(authorWithAddressConfig, {
|
|
1211
1212
|
firstName: "f",
|
|
1212
1213
|
address: { city: "c1", street: "s1" },
|
|
1213
1214
|
});
|
|
@@ -1222,7 +1223,7 @@ describe("formState", () => {
|
|
|
1222
1223
|
});
|
|
1223
1224
|
it("isNewEntity is false if there is no id field", () => {
|
|
1224
1225
|
// Given an author without an id field defined
|
|
1225
|
-
const formState =
|
|
1226
|
+
const formState = objectField_1.createObjectState({
|
|
1226
1227
|
firstName: { type: "value" },
|
|
1227
1228
|
address: { type: "object", config: { street: { type: "value" } } },
|
|
1228
1229
|
}, {});
|
|
@@ -1232,7 +1233,7 @@ describe("formState", () => {
|
|
|
1232
1233
|
});
|
|
1233
1234
|
it("provides isNewEntity for lists", () => {
|
|
1234
1235
|
// Given an author & books without an id
|
|
1235
|
-
const formState =
|
|
1236
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, {
|
|
1236
1237
|
books: [{ title: "t1" }, { title: "t2" }],
|
|
1237
1238
|
});
|
|
1238
1239
|
// Then both books know they are new
|
|
@@ -1300,13 +1301,13 @@ const authorWithBooksConfig = {
|
|
|
1300
1301
|
type: "list",
|
|
1301
1302
|
config: {
|
|
1302
1303
|
id: { type: "value" },
|
|
1303
|
-
title: { type: "value", rules: [
|
|
1304
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
1304
1305
|
classification: { type: "value" },
|
|
1305
1306
|
},
|
|
1306
1307
|
},
|
|
1307
1308
|
};
|
|
1308
1309
|
function createAuthorInputState(input, maybeAutoSave) {
|
|
1309
|
-
return
|
|
1310
|
+
return objectField_1.createObjectState(authorWithBooksConfig, input, { maybeAutoSave });
|
|
1310
1311
|
}
|
|
1311
1312
|
const authorWithAddressConfig = {
|
|
1312
1313
|
id: { type: "value" },
|
|
@@ -1315,7 +1316,7 @@ const authorWithAddressConfig = {
|
|
|
1315
1316
|
address: {
|
|
1316
1317
|
type: "object",
|
|
1317
1318
|
config: {
|
|
1318
|
-
street: { type: "value", rules: [
|
|
1319
|
+
street: { type: "value", rules: [rules_1.required] },
|
|
1319
1320
|
city: { type: "value" },
|
|
1320
1321
|
},
|
|
1321
1322
|
},
|
|
@@ -1328,7 +1329,7 @@ const authorWithAddressAndBooksConfig = {
|
|
|
1328
1329
|
type: "object",
|
|
1329
1330
|
config: {
|
|
1330
1331
|
id: { type: "value" },
|
|
1331
|
-
street: { type: "value", rules: [
|
|
1332
|
+
street: { type: "value", rules: [rules_1.required] },
|
|
1332
1333
|
city: { type: "value" },
|
|
1333
1334
|
},
|
|
1334
1335
|
},
|
|
@@ -1336,11 +1337,11 @@ const authorWithAddressAndBooksConfig = {
|
|
|
1336
1337
|
type: "list",
|
|
1337
1338
|
config: {
|
|
1338
1339
|
id: { type: "value" },
|
|
1339
|
-
title: { type: "value", rules: [
|
|
1340
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
1340
1341
|
classification: { type: "value" },
|
|
1341
1342
|
},
|
|
1342
1343
|
},
|
|
1343
1344
|
};
|
|
1344
1345
|
function createAuthorWithAddressInputState(input) {
|
|
1345
|
-
return
|
|
1346
|
+
return objectField_1.createObjectState(authorWithAddressConfig, input);
|
|
1346
1347
|
}
|
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 {};
|