@homebound/form-state 2.9.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 +97 -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 +11 -3
- package/dist/useFormStates.js +33 -28
- package/dist/useFormStates.test.js +48 -10
- package/dist/utils.d.ts +24 -1
- package/dist/utils.js +41 -1
- package/dist/utils.test.js +3 -3
- package/package.json +1 -1
- package/dist/formState.d.ts +0 -192
- package/dist/formState.js +0 -573
|
@@ -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,20 +683,56 @@ describe("formState", () => {
|
|
|
682
683
|
});
|
|
683
684
|
it("maintain field readOnly state when form is readOnly", () => {
|
|
684
685
|
// Given a formState
|
|
685
|
-
const formState =
|
|
686
|
+
const formState = objectField_1.createObjectState({ title: { type: "value", rules: [rules_1.required], readOnly: true } }, {});
|
|
686
687
|
// Then expect form
|
|
687
688
|
expect(formState.title.readOnly).toBeTruthy();
|
|
688
689
|
});
|
|
690
|
+
it("has an object-level readOnly=true override field-level readOnly=false", () => {
|
|
691
|
+
// Given a top-level object
|
|
692
|
+
const a1 = createAuthorInputState({
|
|
693
|
+
firstName: "a1",
|
|
694
|
+
lastName: "aL1",
|
|
695
|
+
books: [{ title: "b1", classification: formStateDomain_1.dd100 }],
|
|
696
|
+
});
|
|
697
|
+
// And it is read-only
|
|
698
|
+
a1.readOnly = true;
|
|
699
|
+
// Then it's 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 it's 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
|
+
});
|
|
689
726
|
it("canSave returns dirty and touches", () => {
|
|
690
|
-
const a1 =
|
|
691
|
-
firstName: { type: "value", rules: [
|
|
727
|
+
const a1 = objectField_1.createObjectState({
|
|
728
|
+
firstName: { type: "value", rules: [rules_1.required] },
|
|
692
729
|
}, {});
|
|
693
730
|
expect(a1.firstName.touched).toBeFalsy();
|
|
694
731
|
expect(a1.canSave()).toBeFalsy();
|
|
695
732
|
expect(a1.firstName.touched).toBeTruthy();
|
|
696
733
|
});
|
|
697
734
|
it("uses toJSON if available for dirty checks", () => {
|
|
698
|
-
const a1 =
|
|
735
|
+
const a1 = objectField_1.createObjectState({
|
|
699
736
|
birthday: { type: "value" },
|
|
700
737
|
}, { birthday: new formStateDomain_1.DateOnly(formStateDomain_1.jan1) });
|
|
701
738
|
a1.birthday.set(new formStateDomain_1.DateOnly(formStateDomain_1.jan1));
|
|
@@ -710,10 +747,10 @@ describe("formState", () => {
|
|
|
710
747
|
expect(a.valid).toBeTruthy();
|
|
711
748
|
});
|
|
712
749
|
it("can validate a list input", () => {
|
|
713
|
-
const a =
|
|
750
|
+
const a = objectField_1.createObjectState({
|
|
714
751
|
books: {
|
|
715
752
|
type: "list",
|
|
716
|
-
config: { title: { type: "value", rules: [
|
|
753
|
+
config: { title: { type: "value", rules: [rules_1.required] } },
|
|
717
754
|
rules: [({ value }) => (value.find((b) => b.title.value === "t1") ? "Cannot have t1" : undefined)],
|
|
718
755
|
},
|
|
719
756
|
}, {});
|
|
@@ -727,11 +764,11 @@ describe("formState", () => {
|
|
|
727
764
|
expect(lastErrors).toEqual("");
|
|
728
765
|
});
|
|
729
766
|
it("can ignore a deleted list entry", () => {
|
|
730
|
-
const a =
|
|
767
|
+
const a = objectField_1.createObjectState({
|
|
731
768
|
books: {
|
|
732
769
|
type: "list",
|
|
733
770
|
config: {
|
|
734
|
-
title: { type: "value", rules: [
|
|
771
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
735
772
|
delete: { type: "value", isDeleteKey: true },
|
|
736
773
|
},
|
|
737
774
|
},
|
|
@@ -746,11 +783,11 @@ describe("formState", () => {
|
|
|
746
783
|
expect(a.books.valid).toBeTruthy();
|
|
747
784
|
});
|
|
748
785
|
it("can treat a list entry as read-only", () => {
|
|
749
|
-
const a =
|
|
786
|
+
const a = objectField_1.createObjectState({
|
|
750
787
|
books: {
|
|
751
788
|
type: "list",
|
|
752
789
|
config: {
|
|
753
|
-
title: { type: "value", rules: [
|
|
790
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
754
791
|
isPublished: { type: "value", isReadOnlyKey: true },
|
|
755
792
|
},
|
|
756
793
|
},
|
|
@@ -814,7 +851,7 @@ describe("formState", () => {
|
|
|
814
851
|
expect(config).toBeDefined();
|
|
815
852
|
});
|
|
816
853
|
it("can reset observable objects with computeds", () => {
|
|
817
|
-
const formState =
|
|
854
|
+
const formState = objectField_1.createObjectState({
|
|
818
855
|
firstName: { type: "value" },
|
|
819
856
|
lastName: { type: "value" },
|
|
820
857
|
fullName: { type: "value", computed: true },
|
|
@@ -827,7 +864,7 @@ describe("formState", () => {
|
|
|
827
864
|
expect(formState.firstName.value).toEqual("first");
|
|
828
865
|
});
|
|
829
866
|
it("can set computeds that have setters", () => {
|
|
830
|
-
const formState =
|
|
867
|
+
const formState = objectField_1.createObjectState({
|
|
831
868
|
firstName: { type: "value" },
|
|
832
869
|
lastName: { type: "value" },
|
|
833
870
|
fullName: { type: "value", computed: true },
|
|
@@ -840,7 +877,7 @@ describe("formState", () => {
|
|
|
840
877
|
expect(formState.firstName.value).toEqual("first");
|
|
841
878
|
});
|
|
842
879
|
it("can mark fields as read only", () => {
|
|
843
|
-
const formState =
|
|
880
|
+
const formState = objectField_1.createObjectState({
|
|
844
881
|
firstName: { type: "value" },
|
|
845
882
|
lastName: { type: "value" },
|
|
846
883
|
fullName: { type: "value", readOnly: true },
|
|
@@ -853,7 +890,7 @@ describe("formState", () => {
|
|
|
853
890
|
expect(formState.firstName.value).toEqual("first");
|
|
854
891
|
});
|
|
855
892
|
it("lets rules validate against other fields", () => {
|
|
856
|
-
const formState =
|
|
893
|
+
const formState = objectField_1.createObjectState({
|
|
857
894
|
firstName: { type: "value", rules: [] },
|
|
858
895
|
lastName: {
|
|
859
896
|
type: "value",
|
|
@@ -872,7 +909,7 @@ describe("formState", () => {
|
|
|
872
909
|
});
|
|
873
910
|
it("can return only changed primitive fields", () => {
|
|
874
911
|
// Given an author
|
|
875
|
-
const formState =
|
|
912
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, {
|
|
876
913
|
id: "a:1",
|
|
877
914
|
firstName: "f",
|
|
878
915
|
lastName: "l",
|
|
@@ -890,7 +927,7 @@ describe("formState", () => {
|
|
|
890
927
|
});
|
|
891
928
|
it("can return only changed object fields", () => {
|
|
892
929
|
// Given an author with an address object
|
|
893
|
-
const formState =
|
|
930
|
+
const formState = objectField_1.createObjectState(authorWithAddressConfig, {
|
|
894
931
|
id: "a:1",
|
|
895
932
|
firstName: "f",
|
|
896
933
|
address: { city: "c1", street: "s1" },
|
|
@@ -907,7 +944,7 @@ describe("formState", () => {
|
|
|
907
944
|
});
|
|
908
945
|
it("changedValue skips empty nested fields", () => {
|
|
909
946
|
// Given a new author with an address object
|
|
910
|
-
const formState =
|
|
947
|
+
const formState = objectField_1.createObjectState(authorWithAddressConfig, {
|
|
911
948
|
firstName: "f",
|
|
912
949
|
// And nothing is in the address
|
|
913
950
|
address: {},
|
|
@@ -918,7 +955,7 @@ describe("formState", () => {
|
|
|
918
955
|
});
|
|
919
956
|
it("changedValue skips effectively empty nested fields", () => {
|
|
920
957
|
// Given a new author with an address object
|
|
921
|
-
const formState =
|
|
958
|
+
const formState = objectField_1.createObjectState(authorWithAddressConfig, {
|
|
922
959
|
firstName: "f",
|
|
923
960
|
// And only undefined keys are in the address
|
|
924
961
|
address: { city: undefined },
|
|
@@ -929,7 +966,7 @@ describe("formState", () => {
|
|
|
929
966
|
});
|
|
930
967
|
it("can return only changed list fields", () => {
|
|
931
968
|
// Given an author with some books
|
|
932
|
-
const formState =
|
|
969
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, {
|
|
933
970
|
id: "a:1",
|
|
934
971
|
firstName: "f",
|
|
935
972
|
books: [
|
|
@@ -950,7 +987,7 @@ describe("formState", () => {
|
|
|
950
987
|
});
|
|
951
988
|
it("can return only changed but incremental list fields", () => {
|
|
952
989
|
// Given an author
|
|
953
|
-
const formState =
|
|
990
|
+
const formState = objectField_1.createObjectState({
|
|
954
991
|
id: { type: "value" },
|
|
955
992
|
// And the books collection is marked as incremental (i.e. line items)
|
|
956
993
|
books: {
|
|
@@ -958,7 +995,7 @@ describe("formState", () => {
|
|
|
958
995
|
update: "incremental",
|
|
959
996
|
config: {
|
|
960
997
|
id: { type: "value" },
|
|
961
|
-
title: { type: "value", rules: [
|
|
998
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
962
999
|
},
|
|
963
1000
|
},
|
|
964
1001
|
}, {
|
|
@@ -981,14 +1018,14 @@ describe("formState", () => {
|
|
|
981
1018
|
});
|
|
982
1019
|
it("uses the op key as a hint to use incremental list behavior", () => {
|
|
983
1020
|
// Given an author
|
|
984
|
-
const formState =
|
|
1021
|
+
const formState = objectField_1.createObjectState({
|
|
985
1022
|
id: { type: "value" },
|
|
986
1023
|
// And the books collection is not explicitly marked as incremental
|
|
987
1024
|
books: {
|
|
988
1025
|
type: "list",
|
|
989
1026
|
config: {
|
|
990
1027
|
id: { type: "value" },
|
|
991
|
-
title: { type: "value", rules: [
|
|
1028
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
992
1029
|
// But it does have an op key
|
|
993
1030
|
op: { type: "value" },
|
|
994
1031
|
},
|
|
@@ -1014,14 +1051,14 @@ describe("formState", () => {
|
|
|
1014
1051
|
});
|
|
1015
1052
|
it("defaults the incremental op key to included when not set", () => {
|
|
1016
1053
|
// Given an author
|
|
1017
|
-
const formState =
|
|
1054
|
+
const formState = objectField_1.createObjectState({
|
|
1018
1055
|
id: { type: "value" },
|
|
1019
1056
|
// And the books collection is not explicitly marked as incremental
|
|
1020
1057
|
books: {
|
|
1021
1058
|
type: "list",
|
|
1022
1059
|
config: {
|
|
1023
1060
|
id: { type: "value" },
|
|
1024
|
-
title: { type: "value", rules: [
|
|
1061
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
1025
1062
|
// But it does have an op key
|
|
1026
1063
|
op: { type: "value" },
|
|
1027
1064
|
},
|
|
@@ -1055,7 +1092,7 @@ describe("formState", () => {
|
|
|
1055
1092
|
});
|
|
1056
1093
|
});
|
|
1057
1094
|
it("can observe value changes", () => {
|
|
1058
|
-
const formState =
|
|
1095
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f" });
|
|
1059
1096
|
let ticks = 0;
|
|
1060
1097
|
mobx_1.reaction(() => formState.value, () => ticks++, { equals: () => false });
|
|
1061
1098
|
expect(ticks).toEqual(0);
|
|
@@ -1065,7 +1102,7 @@ describe("formState", () => {
|
|
|
1065
1102
|
expect(ticks).toEqual(1);
|
|
1066
1103
|
});
|
|
1067
1104
|
it("does not override a focused, changed field", () => {
|
|
1068
|
-
const formState =
|
|
1105
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1069
1106
|
// Given first name is focused
|
|
1070
1107
|
formState.firstName.focus();
|
|
1071
1108
|
// And they have wip edits
|
|
@@ -1080,7 +1117,7 @@ describe("formState", () => {
|
|
|
1080
1117
|
expect(formState.firstName.value).toEqual("fff");
|
|
1081
1118
|
});
|
|
1082
1119
|
it("does update a focused, unchanged field", () => {
|
|
1083
|
-
const formState =
|
|
1120
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1084
1121
|
// Given first name is focused, but has not changed
|
|
1085
1122
|
formState.firstName.focus();
|
|
1086
1123
|
// When a mutation result sets both firstName and lastName
|
|
@@ -1089,7 +1126,7 @@ describe("formState", () => {
|
|
|
1089
1126
|
expect(formState.firstName.value).toEqual("f2");
|
|
1090
1127
|
});
|
|
1091
1128
|
it("trims string values on blur", () => {
|
|
1092
|
-
const formState =
|
|
1129
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1093
1130
|
// Given the user is typing with spaces
|
|
1094
1131
|
formState.firstName.focus();
|
|
1095
1132
|
formState.firstName.set("f ");
|
|
@@ -1101,7 +1138,7 @@ describe("formState", () => {
|
|
|
1101
1138
|
expect(formState.firstName.value).toEqual("f");
|
|
1102
1139
|
});
|
|
1103
1140
|
it("trims string values on maybeAutoSave", () => {
|
|
1104
|
-
const formState =
|
|
1141
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1105
1142
|
// Given the user is typing with spaces
|
|
1106
1143
|
formState.firstName.focus();
|
|
1107
1144
|
formState.firstName.set("f ");
|
|
@@ -1115,7 +1152,7 @@ describe("formState", () => {
|
|
|
1115
1152
|
expect(formState.firstName.touched).toEqual(true);
|
|
1116
1153
|
});
|
|
1117
1154
|
it("trims empty values to undefined on blur", () => {
|
|
1118
|
-
const formState =
|
|
1155
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
1119
1156
|
// Given the user is typing with only spaces
|
|
1120
1157
|
formState.firstName.focus();
|
|
1121
1158
|
formState.firstName.set(" ");
|
|
@@ -1133,7 +1170,7 @@ describe("formState", () => {
|
|
|
1133
1170
|
});
|
|
1134
1171
|
it("can work with both required inputs and optional fields", () => {
|
|
1135
1172
|
// And we drop it in a form.
|
|
1136
|
-
const form =
|
|
1173
|
+
const form = objectField_1.createObjectState({
|
|
1137
1174
|
id: { type: "value" },
|
|
1138
1175
|
firstName: { type: "value" },
|
|
1139
1176
|
}, { id: "a:1" });
|
|
@@ -1154,7 +1191,7 @@ describe("formState", () => {
|
|
|
1154
1191
|
});
|
|
1155
1192
|
it("can have child object states passed in as field states", () => {
|
|
1156
1193
|
// Given an author
|
|
1157
|
-
const a =
|
|
1194
|
+
const a = objectField_1.createObjectState({
|
|
1158
1195
|
id: { type: "value" },
|
|
1159
1196
|
// And address is modeled as a value object
|
|
1160
1197
|
address: { type: "value" },
|
|
@@ -1171,7 +1208,7 @@ describe("formState", () => {
|
|
|
1171
1208
|
});
|
|
1172
1209
|
it("provides isNewEntity", () => {
|
|
1173
1210
|
// Given an author without an id
|
|
1174
|
-
const formState =
|
|
1211
|
+
const formState = objectField_1.createObjectState(authorWithAddressConfig, {
|
|
1175
1212
|
firstName: "f",
|
|
1176
1213
|
address: { city: "c1", street: "s1" },
|
|
1177
1214
|
});
|
|
@@ -1186,7 +1223,7 @@ describe("formState", () => {
|
|
|
1186
1223
|
});
|
|
1187
1224
|
it("isNewEntity is false if there is no id field", () => {
|
|
1188
1225
|
// Given an author without an id field defined
|
|
1189
|
-
const formState =
|
|
1226
|
+
const formState = objectField_1.createObjectState({
|
|
1190
1227
|
firstName: { type: "value" },
|
|
1191
1228
|
address: { type: "object", config: { street: { type: "value" } } },
|
|
1192
1229
|
}, {});
|
|
@@ -1196,7 +1233,7 @@ describe("formState", () => {
|
|
|
1196
1233
|
});
|
|
1197
1234
|
it("provides isNewEntity for lists", () => {
|
|
1198
1235
|
// Given an author & books without an id
|
|
1199
|
-
const formState =
|
|
1236
|
+
const formState = objectField_1.createObjectState(authorWithBooksConfig, {
|
|
1200
1237
|
books: [{ title: "t1" }, { title: "t2" }],
|
|
1201
1238
|
});
|
|
1202
1239
|
// Then both books know they are new
|
|
@@ -1264,13 +1301,13 @@ const authorWithBooksConfig = {
|
|
|
1264
1301
|
type: "list",
|
|
1265
1302
|
config: {
|
|
1266
1303
|
id: { type: "value" },
|
|
1267
|
-
title: { type: "value", rules: [
|
|
1304
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
1268
1305
|
classification: { type: "value" },
|
|
1269
1306
|
},
|
|
1270
1307
|
},
|
|
1271
1308
|
};
|
|
1272
1309
|
function createAuthorInputState(input, maybeAutoSave) {
|
|
1273
|
-
return
|
|
1310
|
+
return objectField_1.createObjectState(authorWithBooksConfig, input, { maybeAutoSave });
|
|
1274
1311
|
}
|
|
1275
1312
|
const authorWithAddressConfig = {
|
|
1276
1313
|
id: { type: "value" },
|
|
@@ -1279,7 +1316,7 @@ const authorWithAddressConfig = {
|
|
|
1279
1316
|
address: {
|
|
1280
1317
|
type: "object",
|
|
1281
1318
|
config: {
|
|
1282
|
-
street: { type: "value", rules: [
|
|
1319
|
+
street: { type: "value", rules: [rules_1.required] },
|
|
1283
1320
|
city: { type: "value" },
|
|
1284
1321
|
},
|
|
1285
1322
|
},
|
|
@@ -1292,7 +1329,7 @@ const authorWithAddressAndBooksConfig = {
|
|
|
1292
1329
|
type: "object",
|
|
1293
1330
|
config: {
|
|
1294
1331
|
id: { type: "value" },
|
|
1295
|
-
street: { type: "value", rules: [
|
|
1332
|
+
street: { type: "value", rules: [rules_1.required] },
|
|
1296
1333
|
city: { type: "value" },
|
|
1297
1334
|
},
|
|
1298
1335
|
},
|
|
@@ -1300,11 +1337,11 @@ const authorWithAddressAndBooksConfig = {
|
|
|
1300
1337
|
type: "list",
|
|
1301
1338
|
config: {
|
|
1302
1339
|
id: { type: "value" },
|
|
1303
|
-
title: { type: "value", rules: [
|
|
1340
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
1304
1341
|
classification: { type: "value" },
|
|
1305
1342
|
},
|
|
1306
1343
|
},
|
|
1307
1344
|
};
|
|
1308
1345
|
function createAuthorWithAddressInputState(input) {
|
|
1309
|
-
return
|
|
1346
|
+
return objectField_1.createObjectState(authorWithAddressConfig, input);
|
|
1310
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";
|