@homebound/form-state 2.13.0 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -4
- package/dist/config.d.ts +2 -0
- package/dist/fields/objectField.js +2 -1
- package/dist/fields/valueField.d.ts +1 -1
- package/dist/fields/valueField.js +3 -3
- package/dist/formState.test.js +18 -0
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +7 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+

|
|
2
|
+
[](https://circleci.com/gh/homebound-team/form-state)
|
|
3
|
+
|
|
1
4
|
# form-state
|
|
2
5
|
|
|
3
6
|
form-state is a headless form state management library, built on top of mobx.
|
|
@@ -7,14 +10,18 @@ It acts as a buffer between the canonical data/entity (i.e. the server-side data
|
|
|
7
10
|
It also keeps track of low-level form UX details like:
|
|
8
11
|
|
|
9
12
|
* Which form fields are dirty
|
|
10
|
-
* Which
|
|
11
|
-
* Which
|
|
13
|
+
* Which form fields are valid/invalid
|
|
14
|
+
* Which form fields are touched (i.e. don't show validation errors for untouched fields)
|
|
12
15
|
* Enabling/disabling buttons/form UX based on the overall form-wide state
|
|
13
16
|
* Submitting the form should touch (validate) all fields
|
|
14
17
|
* Auto-saving the form when appropriate (i.e. not on keystroke, but after blur/leaving the field)
|
|
15
|
-
*
|
|
18
|
+
* Queueing auto-saves if one is already in-flight
|
|
19
|
+
* Auto-saves in a table with per-row forms will serialize to avoid cross-child write conflicts on the backend
|
|
20
|
+
* Not over-writing the user's WIP/actively-focused field when auto-saved data refreshes
|
|
16
21
|
* Building a wire payload that has only changed fields
|
|
17
|
-
*
|
|
22
|
+
* `form.changedValue` will return the entity `id` + only changed fields to faciliate doing partial update APIs
|
|
23
|
+
* Supports collections of children, i.e. a `author: { books: [...} }` will include only changed books if necessary
|
|
24
|
+
* Child collections can be either exhausive (if any child changes, submit them all) or incremental (only include changed children), to match the backend endpoint's semantics
|
|
18
25
|
|
|
19
26
|
# The Three Type/Shapes Mental Model
|
|
20
27
|
|
package/dist/config.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export declare type ValueFieldConfig<T, V> = {
|
|
|
37
37
|
computed?: boolean;
|
|
38
38
|
/** Marks a field as being initially read-only, i.e. `field.readOnly = true/false` can change this default. */
|
|
39
39
|
readOnly?: boolean;
|
|
40
|
+
/** Marks an array field to be order agnostic when determining changed/dirty states */
|
|
41
|
+
strictOrder?: false;
|
|
40
42
|
};
|
|
41
43
|
/** Field configuration for list values, i.e. `U` is `Book` in a form with `books: Book[]`. */
|
|
42
44
|
export declare type ListFieldConfig<T, U> = {
|
|
@@ -29,11 +29,12 @@ function newObjectState(config, parentState, parentListState, instance, key, may
|
|
|
29
29
|
return proxy;
|
|
30
30
|
}
|
|
31
31
|
const fieldStates = Object.entries(config).map(([_key, _config]) => {
|
|
32
|
+
var _a;
|
|
32
33
|
const key = _key;
|
|
33
34
|
const config = _config;
|
|
34
35
|
let field;
|
|
35
36
|
if (config.type === "value") {
|
|
36
|
-
field = valueField_1.newValueFieldState(instance, getObjectState, key, config.rules || [], config.isIdKey || key === "id", config.isDeleteKey || false, config.isReadOnlyKey || false, config.computed || false, config.readOnly || false, maybeAutoSave);
|
|
37
|
+
field = valueField_1.newValueFieldState(instance, getObjectState, key, config.rules || [], config.isIdKey || key === "id", config.isDeleteKey || false, config.isReadOnlyKey || false, config.computed || false, config.readOnly || false, (_a = config.strictOrder) !== null && _a !== void 0 ? _a : true, maybeAutoSave);
|
|
37
38
|
}
|
|
38
39
|
else if (config.type === "list") {
|
|
39
40
|
field = listField_1.newListFieldState(instance, getObjectState, key, config.rules || [], config, config.config, maybeAutoSave);
|
|
@@ -56,4 +56,4 @@ export interface FieldStateInternal<T, V> extends FieldState<T, V> {
|
|
|
56
56
|
_isReadOnlyKey: boolean;
|
|
57
57
|
_focused: boolean;
|
|
58
58
|
}
|
|
59
|
-
export declare function newValueFieldState<T, K extends keyof T>(parentInstance: T, parentState: () => ObjectState<T>, key: K, rules: Rule<T, T[K] | null | undefined>[], isIdKey: boolean, isDeleteKey: boolean, isReadOnlyKey: boolean, computed: boolean, readOnly: boolean, maybeAutoSave: () => void): FieldState<T, T[K] | null | undefined>;
|
|
59
|
+
export declare function newValueFieldState<T, K extends keyof T>(parentInstance: T, parentState: () => ObjectState<T>, key: K, rules: Rule<T, T[K] | null | undefined>[], isIdKey: boolean, isDeleteKey: boolean, isReadOnlyKey: boolean, computed: boolean, readOnly: boolean, strictOrder: boolean, maybeAutoSave: () => void): FieldState<T, T[K] | null | undefined>;
|
|
@@ -5,7 +5,7 @@ const is_plain_object_1 = require("is-plain-object");
|
|
|
5
5
|
const mobx_1 = require("mobx");
|
|
6
6
|
const rules_1 = require("../rules");
|
|
7
7
|
const utils_1 = require("../utils");
|
|
8
|
-
function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, isDeleteKey, isReadOnlyKey, computed, readOnly, maybeAutoSave) {
|
|
8
|
+
function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, isDeleteKey, isReadOnlyKey, computed, readOnly, strictOrder, maybeAutoSave) {
|
|
9
9
|
// keep a copy here for reference equality
|
|
10
10
|
const value = parentInstance[key];
|
|
11
11
|
let _originalValue = value === null ? undefined : is_plain_object_1.isPlainObject(value) ? mobx_1.toJS(value) : value;
|
|
@@ -35,7 +35,7 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
|
|
|
35
35
|
this.set(v);
|
|
36
36
|
},
|
|
37
37
|
get dirty() {
|
|
38
|
-
return !utils_1.areEqual(this.originalValue, this.value);
|
|
38
|
+
return !utils_1.areEqual(this.originalValue, this.value, strictOrder);
|
|
39
39
|
},
|
|
40
40
|
/** Returns whether this field is readOnly, although if our parent is readOnly then it trumps. */
|
|
41
41
|
get readOnly() {
|
|
@@ -106,7 +106,7 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
|
|
|
106
106
|
const coerceEmptyList = value && value instanceof Array && value.length === 0 && utils_1.isEmpty(this.originalValue);
|
|
107
107
|
const newValue = keepNull ? null : utils_1.isEmpty(value) || coerceEmptyList ? undefined : value;
|
|
108
108
|
// Set the value on our parent object
|
|
109
|
-
const changed = !utils_1.areEqual(newValue, this.value);
|
|
109
|
+
const changed = !utils_1.areEqual(newValue, this.value, strictOrder);
|
|
110
110
|
parentInstance[key] = newValue;
|
|
111
111
|
_tick.value++;
|
|
112
112
|
if (opts.refreshing) {
|
package/dist/formState.test.js
CHANGED
|
@@ -487,6 +487,24 @@ describe("formState", () => {
|
|
|
487
487
|
a1.favoriteColors.set([]);
|
|
488
488
|
expect(a1.dirty).toBeFalsy();
|
|
489
489
|
});
|
|
490
|
+
it("can evaluate arrays to be equal even if order of options are different from original when strictOrder = false", () => {
|
|
491
|
+
// Given an array field value where we define `strictOrder` as false
|
|
492
|
+
const a1 = objectField_1.createObjectState({ favoriteColors: { type: "value", strictOrder: false } }, { favoriteColors: [formStateDomain_1.Color.Red, formStateDomain_1.Color.Blue] });
|
|
493
|
+
expect(a1.favoriteColors.dirty).toBeFalsy();
|
|
494
|
+
// When changing the order of the values
|
|
495
|
+
a1.favoriteColors.set([formStateDomain_1.Color.Blue, formStateDomain_1.Color.Red]);
|
|
496
|
+
// Then expect the field to not be dirty
|
|
497
|
+
expect(a1.dirty).toBeFalsy();
|
|
498
|
+
});
|
|
499
|
+
it("evaluates arrays to be equal based on order by default", () => {
|
|
500
|
+
// Given an array field value
|
|
501
|
+
const a1 = objectField_1.createObjectState({ favoriteColors: { type: "value" } }, { favoriteColors: [formStateDomain_1.Color.Red, formStateDomain_1.Color.Blue] });
|
|
502
|
+
expect(a1.favoriteColors.dirty).toBeFalsy();
|
|
503
|
+
// When we change the order of the values
|
|
504
|
+
a1.favoriteColors.set([formStateDomain_1.Color.Blue, formStateDomain_1.Color.Red]);
|
|
505
|
+
// Then the field should be dirty
|
|
506
|
+
expect(a1.dirty).toBeTruthy();
|
|
507
|
+
});
|
|
490
508
|
it("knows originally unset fields are dirty", () => {
|
|
491
509
|
// Given firstName is purposefully not set when originally initialized
|
|
492
510
|
const a1 = createAuthorInputState({});
|
package/dist/utils.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export declare function isEmpty(value: any): boolean;
|
|
|
37
37
|
* - objects that implement `toJSON`
|
|
38
38
|
*
|
|
39
39
|
*/
|
|
40
|
-
export declare function areEqual<T>(a?: T, b?: T): boolean;
|
|
40
|
+
export declare function areEqual<T>(a?: T, b?: T, strictOrder?: boolean): boolean;
|
|
41
41
|
export declare function hasToJSON(o?: unknown): o is {
|
|
42
42
|
toJSON(): void;
|
|
43
43
|
};
|
package/dist/utils.js
CHANGED
|
@@ -102,7 +102,7 @@ exports.isEmpty = isEmpty;
|
|
|
102
102
|
* - objects that implement `toJSON`
|
|
103
103
|
*
|
|
104
104
|
*/
|
|
105
|
-
function areEqual(a, b) {
|
|
105
|
+
function areEqual(a, b, strictOrder) {
|
|
106
106
|
if (is_plain_object_1.isPlainObject(a)) {
|
|
107
107
|
return fast_deep_equal_1.default(mobx_1.toJS(a), mobx_1.toJS(b));
|
|
108
108
|
}
|
|
@@ -112,7 +112,12 @@ function areEqual(a, b) {
|
|
|
112
112
|
return fast_deep_equal_1.default(a1, b1);
|
|
113
113
|
}
|
|
114
114
|
if (a && b && a instanceof Array && b instanceof Array) {
|
|
115
|
-
|
|
115
|
+
if (strictOrder !== false) {
|
|
116
|
+
return fast_deep_equal_1.default(a, b);
|
|
117
|
+
}
|
|
118
|
+
if (a.length !== b.length)
|
|
119
|
+
return false;
|
|
120
|
+
return a.every((a1) => b.some((b1) => fast_deep_equal_1.default(a1, b1)));
|
|
116
121
|
}
|
|
117
122
|
return a === b;
|
|
118
123
|
}
|