@homebound/form-state 2.17.0 → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config.d.ts
CHANGED
|
@@ -63,6 +63,8 @@ export declare type ListFieldConfig<T, U> = {
|
|
|
63
63
|
* default behavior.
|
|
64
64
|
*/
|
|
65
65
|
update?: "exhaustive" | "incremental";
|
|
66
|
+
/** Set to not consider the order of the list when evaluating changed/dirty states. */
|
|
67
|
+
strictOrder?: false;
|
|
66
68
|
};
|
|
67
69
|
export declare type ObjectFieldConfig<U> = {
|
|
68
70
|
type: "object";
|
|
@@ -3,9 +3,9 @@ import { ObjectState } from "./objectField";
|
|
|
3
3
|
import { FieldState } from "./valueField";
|
|
4
4
|
import { Rule } from "../rules";
|
|
5
5
|
/** Form state for list of children, i.e. `U` is a `Book` in a form with a `books: Book[]`. */
|
|
6
|
-
export interface ListFieldState<
|
|
6
|
+
export interface ListFieldState<U> extends Omit<FieldState<U[]>, "originalValue"> {
|
|
7
7
|
readonly rows: ReadonlyArray<ObjectState<U>>;
|
|
8
8
|
add(value: U, index?: number): void;
|
|
9
9
|
remove(indexOrValue: number | U): void;
|
|
10
10
|
}
|
|
11
|
-
export declare function newListFieldState<T, K extends keyof T, U>(parentInstance: T, parentState: () => ObjectState<T>, key: K, rules: Rule<readonly ObjectState<U>[]>[], listConfig: ListFieldConfig<T, U>, config: ObjectConfig<U>, maybeAutoSave: () => void): ListFieldState<
|
|
11
|
+
export declare function newListFieldState<T, K extends keyof T, U>(parentInstance: T, parentState: () => ObjectState<T>, key: K, rules: Rule<readonly ObjectState<U>[]>[], listConfig: ListFieldConfig<T, U>, config: ObjectConfig<U>, strictOrder: boolean, maybeAutoSave: () => void): ListFieldState<U>;
|
package/dist/fields/listField.js
CHANGED
|
@@ -5,7 +5,7 @@ const mobx_1 = require("mobx");
|
|
|
5
5
|
const objectField_1 = require("./objectField");
|
|
6
6
|
const rules_1 = require("../rules");
|
|
7
7
|
const utils_1 = require("../utils");
|
|
8
|
-
function newListFieldState(parentInstance, parentState, key, rules, listConfig, config, maybeAutoSave) {
|
|
8
|
+
function newListFieldState(parentInstance, parentState, key, rules, listConfig, config, strictOrder, maybeAutoSave) {
|
|
9
9
|
// Keep a map of "item in the parent list" -> "that item's ObjectState"
|
|
10
10
|
const rowMap = new Map();
|
|
11
11
|
const _tick = mobx_1.observable({ value: 1 });
|
|
@@ -37,7 +37,7 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
37
37
|
this.set(v);
|
|
38
38
|
},
|
|
39
39
|
get dirty() {
|
|
40
|
-
return this.rows.some((r) => r.dirty) || this.
|
|
40
|
+
return this.rows.some((r) => r.dirty) || this.hasChanged();
|
|
41
41
|
},
|
|
42
42
|
get required() {
|
|
43
43
|
return this.rules.some((rule) => rule === rules_1.required);
|
|
@@ -46,21 +46,22 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
46
46
|
return parentState().isNewEntity;
|
|
47
47
|
},
|
|
48
48
|
// private
|
|
49
|
-
|
|
49
|
+
hasChanged() {
|
|
50
50
|
const [current, original] = [this.value || [], originalCopy || []];
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
if (current.length !== original.length) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
if (strictOrder) {
|
|
55
|
+
return original.some((e, idx) => {
|
|
56
|
+
const originalState = rowMap.get(e);
|
|
57
|
+
const currentState = rowMap.get(current[idx]);
|
|
58
|
+
return originalState !== currentState;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return !original.every((e) => {
|
|
59
62
|
const state = rowMap.get(e);
|
|
60
63
|
return current.some((e) => rowMap.get(e) === state);
|
|
61
64
|
});
|
|
62
|
-
const isSame = a && b;
|
|
63
|
-
return !isSame;
|
|
64
65
|
},
|
|
65
66
|
// And we can derive each value's ObjectState wrapper as needed from the rowMap cache
|
|
66
67
|
get rows() {
|
|
@@ -82,7 +83,7 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
82
83
|
},
|
|
83
84
|
// TODO Should this be true when all rows are touched?
|
|
84
85
|
get touched() {
|
|
85
|
-
return this.rows.some((r) => r.touched) || this.
|
|
86
|
+
return this.rows.some((r) => r.touched) || this.hasChanged();
|
|
86
87
|
},
|
|
87
88
|
set touched(touched) {
|
|
88
89
|
this.rows.forEach((r) => (r.touched = touched));
|
|
@@ -14,7 +14,7 @@ import { Builtin } from "../utils";
|
|
|
14
14
|
* have exactly the on-the-wire type `T` that they need to submit to the backend, without doing the
|
|
15
15
|
* manual mapping of "data that was in the form controls" into "data that the backend wants".
|
|
16
16
|
*
|
|
17
|
-
* Note that this can be hierarchical by having
|
|
17
|
+
* Note that this can be hierarchical by having a field of `ListFieldState` that
|
|
18
18
|
* themselves each wrap an `ObjectState`, i.e.:
|
|
19
19
|
*
|
|
20
20
|
* ```
|
|
@@ -28,14 +28,14 @@ import { Builtin } from "../utils";
|
|
|
28
28
|
* - title: FieldState
|
|
29
29
|
* ```
|
|
30
30
|
*/
|
|
31
|
-
export declare type ObjectState<T
|
|
31
|
+
export declare type ObjectState<T> = FieldStates<T> & FieldState<T> & {
|
|
32
32
|
/** Sets the state of fields in `state`. */
|
|
33
33
|
set(state: Partial<T>, opts?: SetOpts): void;
|
|
34
34
|
/** Returns whether the object can be saved, i.e. is valid, but also as a side-effect marks touched. */
|
|
35
35
|
canSave(): boolean;
|
|
36
36
|
};
|
|
37
|
-
export declare type ObjectStateInternal<T
|
|
38
|
-
set(value:
|
|
37
|
+
export declare type ObjectStateInternal<T> = ObjectState<T> & {
|
|
38
|
+
set(value: T, opts?: InternalSetOpts): void;
|
|
39
39
|
};
|
|
40
40
|
declare const fragmentSym: unique symbol;
|
|
41
41
|
export declare type Fragment<V> = V & {
|
|
@@ -44,7 +44,7 @@ export declare type Fragment<V> = V & {
|
|
|
44
44
|
export declare function fragment<V>(value: V): Fragment<V>;
|
|
45
45
|
/** For a given input type `T`, decorate each field into the "field state" type that holds our form-relevant state, i.e. valid/touched/etc. */
|
|
46
46
|
declare type FieldStates<T> = {
|
|
47
|
-
[K in keyof T]-?: T[K] extends Fragment<infer V> ? FragmentField<V> : T[K] extends Array<infer U> | null | undefined ? [U] extends [Builtin] ? FieldState<T[K]> : ListFieldState<
|
|
47
|
+
[K in keyof T]-?: T[K] extends Fragment<infer V> ? FragmentField<V> : T[K] extends Array<infer U> | null | undefined ? [U] extends [Builtin] ? FieldState<T[K]> : ListFieldState<U> : T[K] extends Builtin | null | undefined ? FieldState<T[K]> : ObjectState<T[K]>;
|
|
48
48
|
};
|
|
49
49
|
/**
|
|
50
50
|
* Creates a new `ObjectState` for a given form object `T` given config rules in `config`.
|
|
@@ -56,5 +56,5 @@ declare type FieldStates<T> = {
|
|
|
56
56
|
export declare function createObjectState<T>(config: ObjectConfig<T>, instance: T, opts?: {
|
|
57
57
|
maybeAutoSave?: () => void;
|
|
58
58
|
}): ObjectState<T>;
|
|
59
|
-
export declare function newObjectState<T, P = any>(config: ObjectConfig<T>, parentState: (() => ObjectState<P>) | undefined, parentListState: FieldState<any> | undefined, instance: T, key: keyof T | undefined, maybeAutoSave: () => void): ObjectState<T
|
|
59
|
+
export declare function newObjectState<T, P = any>(config: ObjectConfig<T>, parentState: (() => ObjectState<P>) | undefined, parentListState: FieldState<any> | undefined, instance: T, key: keyof T | undefined, maybeAutoSave: () => void): ObjectState<T>;
|
|
60
60
|
export {};
|
|
@@ -35,7 +35,7 @@ function newObjectState(config, parentState, parentListState, instance, key, may
|
|
|
35
35
|
return proxy;
|
|
36
36
|
}
|
|
37
37
|
const fieldStates = Object.entries(config).map(([_key, _config]) => {
|
|
38
|
-
var _a;
|
|
38
|
+
var _a, _b;
|
|
39
39
|
const key = _key;
|
|
40
40
|
const config = _config;
|
|
41
41
|
let field;
|
|
@@ -43,7 +43,7 @@ function newObjectState(config, parentState, parentListState, instance, key, may
|
|
|
43
43
|
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);
|
|
44
44
|
}
|
|
45
45
|
else if (config.type === "list") {
|
|
46
|
-
field = listField_1.newListFieldState(instance, getObjectState, key, config.rules || [], config, config.config, maybeAutoSave);
|
|
46
|
+
field = listField_1.newListFieldState(instance, getObjectState, key, config.rules || [], config, config.config, (_b = config.strictOrder) !== null && _b !== void 0 ? _b : true, maybeAutoSave);
|
|
47
47
|
}
|
|
48
48
|
else if (config.type === "object") {
|
|
49
49
|
if (!instance[key]) {
|
package/dist/formState.test.js
CHANGED
|
@@ -1320,6 +1320,41 @@ describe("formState", () => {
|
|
|
1320
1320
|
// The list is again dirty
|
|
1321
1321
|
expect(a1.books.dirty).toBeTruthy();
|
|
1322
1322
|
});
|
|
1323
|
+
it("can properly evaluate dirty lists when order changes", () => {
|
|
1324
|
+
// Given a list state in a specific order
|
|
1325
|
+
const a = createAuthorInputState({ books: [{ title: "t1" }, { title: "t2" }] });
|
|
1326
|
+
expect(a.books.dirty).toBeFalsy();
|
|
1327
|
+
// When changing that order
|
|
1328
|
+
const book1 = a.books.rows[0].value;
|
|
1329
|
+
a.books.remove(book1);
|
|
1330
|
+
a.books.add(book1);
|
|
1331
|
+
// Then the list state is considered dirty.
|
|
1332
|
+
expect(a.books.dirty).toBeTruthy();
|
|
1333
|
+
// When putting the books back in the original order
|
|
1334
|
+
a.books.remove(book1);
|
|
1335
|
+
a.books.add(book1, 0);
|
|
1336
|
+
// Then the list state is no longer dirty
|
|
1337
|
+
expect(a.books.dirty).toBeFalsy();
|
|
1338
|
+
});
|
|
1339
|
+
it("can optionally ignore the order of list state rows when determining dirty state", () => {
|
|
1340
|
+
// Given a list state in a specific order, and the `strictOrder` option set to `false`
|
|
1341
|
+
const a = objectField_1.createObjectState({
|
|
1342
|
+
books: {
|
|
1343
|
+
type: "list",
|
|
1344
|
+
strictOrder: false,
|
|
1345
|
+
config: {
|
|
1346
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
1347
|
+
},
|
|
1348
|
+
},
|
|
1349
|
+
}, { books: [{ title: "t1" }, { title: "t2" }] });
|
|
1350
|
+
expect(a.books.dirty).toBeFalsy();
|
|
1351
|
+
// When changing that order
|
|
1352
|
+
const book1 = a.books.rows[0].value;
|
|
1353
|
+
a.books.remove(book1);
|
|
1354
|
+
a.books.add(book1);
|
|
1355
|
+
// Then the list state is still not considered dirty.
|
|
1356
|
+
expect(a.books.dirty).toBeFalsy();
|
|
1357
|
+
});
|
|
1323
1358
|
describe("fragments", () => {
|
|
1324
1359
|
it("ignores changes within fragments", () => {
|
|
1325
1360
|
const a = objectField_1.createObjectState({ title: { type: "value" }, data: { type: "fragment" } }, { title: "b1", data: objectField_1.fragment({ foo: "1" }) });
|