@homebound/form-state 2.20.1 → 2.21.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 +10 -10
- package/dist/FormStateApp.js +9 -11
- package/dist/config.d.ts +4 -8
- package/dist/configBuilders.d.ts +60 -0
- package/dist/configBuilders.js +102 -0
- package/dist/fields/listField.d.ts +1 -1
- package/dist/fields/valueField.d.ts +2 -0
- package/dist/formState.test.d.ts +11 -1
- package/dist/formState.test.js +58 -80
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/mobx-behavior.test.d.ts +1 -0
- package/dist/mobx-behavior.test.js +51 -0
- package/dist/stackoverlfow.d.ts +17 -0
- package/dist/stackoverlfow.js +17 -0
- package/dist/utils.d.ts +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
form-state is a headless form state management library, built on top of mobx.
|
|
7
7
|
|
|
8
|
-
It acts as a buffer between the canonical data (i.e. the server-side data, or your app's GraphQL/Redux/etc. global store) and the user's WIP data that is being actively mutated in form fields (which is "too chatty"/WIP to
|
|
8
|
+
It acts as a buffer between the canonical data (i.e. the server-side data, or your app's GraphQL/Redux/etc. global store) and the user's WIP data that is being actively mutated in form fields (which is "too chatty"/WIP to push back into global stores).
|
|
9
9
|
|
|
10
10
|
It also keeps track of low-level form UX details like:
|
|
11
11
|
|
|
@@ -21,7 +21,7 @@ It also keeps track of low-level form UX details like:
|
|
|
21
21
|
- Building a wire payload that has only changed fields
|
|
22
22
|
- `form.changedValue` will return the entity `id` + only changed fields to faciliate doing partial update APIs
|
|
23
23
|
- Supports collections of children, i.e. a `author: { books: [...} }` will include only changed books if necessary
|
|
24
|
-
- Child collections can be either
|
|
24
|
+
- Child collections can be either exhaustive (if any child changes, submit them all) or incremental (only include changed children), to match the backend endpoint's semantics
|
|
25
25
|
|
|
26
26
|
# Main Features
|
|
27
27
|
|
|
@@ -37,16 +37,17 @@ The core abstraction that `form-state` provides is a `FieldState` interface that
|
|
|
37
37
|
```ts
|
|
38
38
|
// Very simplified example
|
|
39
39
|
interface FieldState {
|
|
40
|
-
value
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
// Can be used to read & write the value bound into the form field
|
|
41
|
+
value: V;
|
|
42
|
+
errors: string[];
|
|
43
|
+
valid: boolean;
|
|
44
|
+
touched: boolean;
|
|
44
45
|
}
|
|
45
46
|
```
|
|
46
47
|
|
|
47
48
|
Which combines all the logical aspects of "a single form field" into a single object/prop.
|
|
48
49
|
|
|
49
|
-
This facilitates the "gold standard" of form
|
|
50
|
+
This facilitates the "gold standard" of form DX, which is "one line per form field", i.e:
|
|
50
51
|
|
|
51
52
|
```tsx
|
|
52
53
|
function AuthorEditorComponent() {
|
|
@@ -124,14 +125,14 @@ type AuthorFragment = { firstName: string; miscOtherData: {} };
|
|
|
124
125
|
// For your page's form state, add-in the "extra data"
|
|
125
126
|
type AuthorForm = AuthorInput & {
|
|
126
127
|
// The `Fragment` type tells form-state this is not a regular form field
|
|
127
|
-
data: Fragment<AuthorFragment
|
|
128
|
+
data: Fragment<AuthorFragment>;
|
|
128
129
|
};
|
|
129
130
|
|
|
130
131
|
// Tell the form config the "fragment" is not a real field
|
|
131
132
|
const config: ObjectConfig<AuthorForm> = {
|
|
132
133
|
firstName: { type: "value", rules: [require] },
|
|
133
134
|
data: { type: "fragment" },
|
|
134
|
-
}
|
|
135
|
+
};
|
|
135
136
|
|
|
136
137
|
// Now in the component...
|
|
137
138
|
const data = useGraphQLQuery();
|
|
@@ -147,7 +148,6 @@ const form = useFormState({
|
|
|
147
148
|
});
|
|
148
149
|
```
|
|
149
150
|
|
|
150
|
-
|
|
151
151
|
# Internal Implementation Notes
|
|
152
152
|
|
|
153
153
|
form-state keeps the "actual data" (basically a POJO of your form data) separate from the "mobx proxies that track reactivity" (the `ObjectState` interface with `.get` / `.set` / `.errors` other methods).
|
package/dist/FormStateApp.js
CHANGED
|
@@ -33,17 +33,15 @@ function FormStateApp() {
|
|
|
33
33
|
}
|
|
34
34
|
exports.FormStateApp = FormStateApp;
|
|
35
35
|
// Configure the fields/behavior for AuthorInput's fields
|
|
36
|
-
const formConfig = {
|
|
37
|
-
firstName:
|
|
38
|
-
lastName:
|
|
39
|
-
books:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
},
|
|
46
|
-
};
|
|
36
|
+
const formConfig = index_1.f.config({
|
|
37
|
+
firstName: index_1.f.value().req(),
|
|
38
|
+
lastName: index_1.f.value().readOnly(),
|
|
39
|
+
books: index_1.f
|
|
40
|
+
.list({
|
|
41
|
+
title: index_1.f.value().req(),
|
|
42
|
+
})
|
|
43
|
+
.rule(({ value }) => (value.length === 0 ? "Empty" : undefined)),
|
|
44
|
+
});
|
|
47
45
|
function TextField(props) {
|
|
48
46
|
const { field } = props;
|
|
49
47
|
// Somewhat odd: input won't update unless we use <Observer>, even though our
|
package/dist/config.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment, ObjectState } from "./fields/objectField";
|
|
2
2
|
import { Rule } from "./rules";
|
|
3
|
-
import { Builtin } from "./utils";
|
|
3
|
+
import { Builtin, OmitIf } from "./utils";
|
|
4
4
|
/**
|
|
5
5
|
* Config rules for each field in `T` that we're editing in a form.
|
|
6
6
|
*
|
|
@@ -8,17 +8,14 @@ import { Builtin } from "./utils";
|
|
|
8
8
|
* the caller define field-specific behavior, i.e. validation rules.
|
|
9
9
|
*/
|
|
10
10
|
export type ObjectConfig<T> = {
|
|
11
|
-
[P in keyof OmitIf<T, Function>]: T[P] extends Fragment<infer V> ? FragmentFieldConfig : T[P] extends Array<infer U> | null | undefined ? U extends Builtin ? ValueFieldConfig<T
|
|
11
|
+
[P in keyof OmitIf<T, Function>]: T[P] extends Fragment<infer V> ? FragmentFieldConfig : T[P] extends Array<infer U> | null | undefined ? U extends Builtin ? ValueFieldConfig<T[P]> : ListFieldConfig<U> : ValueFieldConfig<T[P]> | ObjectFieldConfig<T[P]>;
|
|
12
12
|
};
|
|
13
|
-
type OmitIf<Base, Condition> = Pick<Base, {
|
|
14
|
-
[Key in keyof Base]: Base[Key] extends Condition ? never : Key;
|
|
15
|
-
}[keyof Base]>;
|
|
16
13
|
/** Field configuration for an opaque value that we don't actually want to include. */
|
|
17
14
|
export type FragmentFieldConfig = {
|
|
18
15
|
type: "fragment";
|
|
19
16
|
};
|
|
20
17
|
/** Field configuration for primitive values, i.e. strings/numbers/Dates/user-defined types. */
|
|
21
|
-
export type ValueFieldConfig<
|
|
18
|
+
export type ValueFieldConfig<V> = {
|
|
22
19
|
type: "value";
|
|
23
20
|
rules?: Rule<V | null | undefined>[];
|
|
24
21
|
/**
|
|
@@ -45,7 +42,7 @@ export type ValueFieldConfig<T, V> = {
|
|
|
45
42
|
strictOrder?: false;
|
|
46
43
|
};
|
|
47
44
|
/** Field configuration for list values, i.e. `U` is `Book` in a form with `books: Book[]`. */
|
|
48
|
-
export type ListFieldConfig<
|
|
45
|
+
export type ListFieldConfig<U> = {
|
|
49
46
|
type: "list";
|
|
50
47
|
/** Rules that can run on the full list of children. */
|
|
51
48
|
rules?: Rule<readonly ObjectState<U>[]>[];
|
|
@@ -71,4 +68,3 @@ export type ObjectFieldConfig<U> = {
|
|
|
71
68
|
/** Config for the child's form state, i.e. each book. */
|
|
72
69
|
config: ObjectConfig<U>;
|
|
73
70
|
};
|
|
74
|
-
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { FragmentFieldConfig, ListFieldConfig, ObjectConfig, ValueFieldConfig } from "./config";
|
|
2
|
+
import { Fragment, ObjectState } from "./fields/objectField";
|
|
3
|
+
import { Rule } from "./rules";
|
|
4
|
+
import { Builtin, OmitIf } from "./utils";
|
|
5
|
+
/**
|
|
6
|
+
* Provides a Zod-ish API for building form configs.
|
|
7
|
+
*/
|
|
8
|
+
export declare const f: {
|
|
9
|
+
/** Creates the top-level config for a form, like an author or book. */
|
|
10
|
+
config<T>(fields: ObjectConfigBuilderFields<T>): ObjectConfig<T>;
|
|
11
|
+
/** Creates the config DSL for an object, like an author or book. */
|
|
12
|
+
object<T_1>(fields: ObjectConfigBuilderFields<T_1>): ObjectConfigBuilder<T_1>;
|
|
13
|
+
/** Creates the config DSL for an list of objects, like an author's list of books. */
|
|
14
|
+
list<U>(fields: ObjectConfigBuilderFields<U>): ListFieldConfigBuilder<U>;
|
|
15
|
+
/** Creates the config DSL for a primitive value, like a first name or phone number. */
|
|
16
|
+
value<V>(): ValueFieldConfigBuilder<V>;
|
|
17
|
+
/** A shorthand for creating a computed value. */
|
|
18
|
+
computed<V_1>(): ValueFieldConfigBuilder<V_1>;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Defines the fields that should be passed into `f.object({ ... })` for fhe form type `T`.
|
|
22
|
+
*
|
|
23
|
+
* This is basically a map of `key` in `T` to `f.value()`, `f.list()`, or `f.object()`
|
|
24
|
+
* DSL objects that define the runtime structure/fields of the form.
|
|
25
|
+
*/
|
|
26
|
+
export type ObjectConfigBuilderFields<T> = {
|
|
27
|
+
[P in keyof OmitIf<T, Function>]: T[P] extends Fragment<infer V> ? FragmentFieldConfigBuilder : T[P] extends Array<infer U> | null | undefined ? U extends Builtin ? ValueFieldConfigBuilder<T[P]> : ListFieldConfigBuilder<U> : ValueFieldConfigBuilder<T[P]> | ObjectConfigBuilder<T[P]>;
|
|
28
|
+
};
|
|
29
|
+
export interface FragmentFieldConfigBuilder {
|
|
30
|
+
build(): FragmentFieldConfig;
|
|
31
|
+
}
|
|
32
|
+
/** Provides a fluent DSL for building up an object's config. */
|
|
33
|
+
export declare class ObjectConfigBuilder<T> {
|
|
34
|
+
private config;
|
|
35
|
+
constructor(fields: ObjectConfigBuilderFields<T>);
|
|
36
|
+
build(): ObjectConfig<T>;
|
|
37
|
+
}
|
|
38
|
+
/** Provides a fluent DSL for building up a value's config. */
|
|
39
|
+
export declare class ValueFieldConfigBuilder<V> {
|
|
40
|
+
private config;
|
|
41
|
+
/** Marks the field as required. */
|
|
42
|
+
req(): this;
|
|
43
|
+
/** Marks the field as read only. */
|
|
44
|
+
readOnly(): this;
|
|
45
|
+
/** Appends `rules` to the field's validation rules. */
|
|
46
|
+
rules(rules: Rule<V | null | undefined>[]): this;
|
|
47
|
+
/** Marks the field as computed. */
|
|
48
|
+
computed(): this;
|
|
49
|
+
build(): ValueFieldConfig<V>;
|
|
50
|
+
}
|
|
51
|
+
/** Provides a fluent DSL for building up a list's config. */
|
|
52
|
+
export declare class ListFieldConfigBuilder<U> {
|
|
53
|
+
private config;
|
|
54
|
+
constructor(fields: ObjectConfigBuilderFields<U>);
|
|
55
|
+
/** Appends `rule` to the field's validation rules. */
|
|
56
|
+
rule(rule: Rule<readonly ObjectState<U>[]>): this;
|
|
57
|
+
/** Appends `rules` to the field's validation rules. */
|
|
58
|
+
rules(rules: Rule<readonly ObjectState<U>[]>[]): this;
|
|
59
|
+
build(): ListFieldConfig<U>;
|
|
60
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ListFieldConfigBuilder = exports.ValueFieldConfigBuilder = exports.ObjectConfigBuilder = exports.f = void 0;
|
|
4
|
+
const rules_1 = require("./rules");
|
|
5
|
+
/**
|
|
6
|
+
* Provides a Zod-ish API for building form configs.
|
|
7
|
+
*/
|
|
8
|
+
exports.f = {
|
|
9
|
+
/** Creates the top-level config for a form, like an author or book. */
|
|
10
|
+
config(fields) {
|
|
11
|
+
return new ObjectConfigBuilder(fields).build();
|
|
12
|
+
},
|
|
13
|
+
/** Creates the config DSL for an object, like an author or book. */
|
|
14
|
+
object(fields) {
|
|
15
|
+
return new ObjectConfigBuilder(fields);
|
|
16
|
+
},
|
|
17
|
+
/** Creates the config DSL for an list of objects, like an author's list of books. */
|
|
18
|
+
list(fields) {
|
|
19
|
+
return new ListFieldConfigBuilder(fields);
|
|
20
|
+
},
|
|
21
|
+
/** Creates the config DSL for a primitive value, like a first name or phone number. */
|
|
22
|
+
value() {
|
|
23
|
+
return new ValueFieldConfigBuilder();
|
|
24
|
+
},
|
|
25
|
+
/** A shorthand for creating a computed value. */
|
|
26
|
+
computed() {
|
|
27
|
+
return this.value().computed();
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
/** Provides a fluent DSL for building up an object's config. */
|
|
31
|
+
class ObjectConfigBuilder {
|
|
32
|
+
constructor(fields) {
|
|
33
|
+
this.config = {};
|
|
34
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
35
|
+
this.config[key] = value.build();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
build() {
|
|
39
|
+
return this.config;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.ObjectConfigBuilder = ObjectConfigBuilder;
|
|
43
|
+
/** Provides a fluent DSL for building up a value's config. */
|
|
44
|
+
class ValueFieldConfigBuilder {
|
|
45
|
+
constructor() {
|
|
46
|
+
this.config = { type: "value", rules: [] };
|
|
47
|
+
}
|
|
48
|
+
/** Marks the field as required. */
|
|
49
|
+
req() {
|
|
50
|
+
this.config.rules = [rules_1.required];
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
/** Marks the field as read only. */
|
|
54
|
+
readOnly() {
|
|
55
|
+
this.config.readOnly = true;
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
/** Appends `rules` to the field's validation rules. */
|
|
59
|
+
rules(rules) {
|
|
60
|
+
var _a;
|
|
61
|
+
var _b;
|
|
62
|
+
((_a = (_b = this.config).rules) !== null && _a !== void 0 ? _a : (_b.rules = [])).push(...rules);
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
/** Marks the field as computed. */
|
|
66
|
+
computed() {
|
|
67
|
+
this.config.computed = true;
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
build() {
|
|
71
|
+
return this.config;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.ValueFieldConfigBuilder = ValueFieldConfigBuilder;
|
|
75
|
+
/** Provides a fluent DSL for building up a list's config. */
|
|
76
|
+
class ListFieldConfigBuilder {
|
|
77
|
+
constructor(fields) {
|
|
78
|
+
const config = {};
|
|
79
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
80
|
+
config[key] = value.build();
|
|
81
|
+
}
|
|
82
|
+
this.config = { type: "list", rules: [], config };
|
|
83
|
+
}
|
|
84
|
+
/** Appends `rule` to the field's validation rules. */
|
|
85
|
+
rule(rule) {
|
|
86
|
+
var _a;
|
|
87
|
+
var _b;
|
|
88
|
+
((_a = (_b = this.config).rules) !== null && _a !== void 0 ? _a : (_b.rules = [])).push(rule);
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
/** Appends `rules` to the field's validation rules. */
|
|
92
|
+
rules(rules) {
|
|
93
|
+
var _a;
|
|
94
|
+
var _b;
|
|
95
|
+
((_a = (_b = this.config).rules) !== null && _a !== void 0 ? _a : (_b.rules = [])).push(...rules);
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
build() {
|
|
99
|
+
return this.config;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.ListFieldConfigBuilder = ListFieldConfigBuilder;
|
|
@@ -8,4 +8,4 @@ export interface ListFieldState<U> extends Omit<FieldState<U[]>, "originalValue"
|
|
|
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<
|
|
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<U>, config: ObjectConfig<U>, strictOrder: boolean, maybeAutoSave: () => void): ListFieldState<U>;
|
|
@@ -11,7 +11,9 @@ import { Rule } from "../rules";
|
|
|
11
11
|
* i.e. text boxes, can always be cleared out/deleted.
|
|
12
12
|
*/
|
|
13
13
|
export interface FieldState<V> {
|
|
14
|
+
/** The key in the parent object, i.e. `firstName` in `author: { firstName: string }`. */
|
|
14
15
|
readonly key: string;
|
|
16
|
+
/** The current value of the field. */
|
|
15
17
|
value: V;
|
|
16
18
|
readonly originalValue: V;
|
|
17
19
|
touched: boolean;
|
package/dist/formState.test.d.ts
CHANGED
|
@@ -1 +1,11 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export declare class ObservableObject {
|
|
2
|
+
firstName: string | undefined;
|
|
3
|
+
lastName: string | undefined;
|
|
4
|
+
age?: number | undefined;
|
|
5
|
+
constructor();
|
|
6
|
+
get fullName(): string;
|
|
7
|
+
set fullName(fullName: string);
|
|
8
|
+
toInput(): {
|
|
9
|
+
firstName: string | undefined;
|
|
10
|
+
};
|
|
11
|
+
}
|
package/dist/formState.test.js
CHANGED
|
@@ -1,55 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ObservableObject = void 0;
|
|
3
4
|
const mobx_1 = require("mobx");
|
|
5
|
+
const configBuilders_1 = require("./configBuilders");
|
|
4
6
|
const objectField_1 = require("./fields/objectField");
|
|
5
7
|
const formStateDomain_1 = require("./formStateDomain");
|
|
6
8
|
const rules_1 = require("./rules");
|
|
7
|
-
describe("mobx behavior", () => {
|
|
8
|
-
it("mobx batches if in actions", () => {
|
|
9
|
-
const a = (0, mobx_1.observable)({ name: "a1" });
|
|
10
|
-
const b = (0, mobx_1.observable)({ name: "b1" });
|
|
11
|
-
const map = new mobx_1.ObservableMap();
|
|
12
|
-
map.set("a", a);
|
|
13
|
-
map.set("b", b);
|
|
14
|
-
let runs = 0;
|
|
15
|
-
(0, mobx_1.autorun)(() => {
|
|
16
|
-
noop([...map.values()].filter((v) => v.name.includes("1")));
|
|
17
|
-
runs++;
|
|
18
|
-
});
|
|
19
|
-
expect(runs).toBe(1);
|
|
20
|
-
// When we change both a and b
|
|
21
|
-
(0, mobx_1.runInAction)(() => {
|
|
22
|
-
a.name = "a11";
|
|
23
|
-
b.name = "b2";
|
|
24
|
-
});
|
|
25
|
-
// Then the reaction waited and only ran once
|
|
26
|
-
expect(runs).toBe(2);
|
|
27
|
-
});
|
|
28
|
-
it("mobx can watch undefined keys", () => {
|
|
29
|
-
const a = new ObservableObject();
|
|
30
|
-
const b = new ObservableObject();
|
|
31
|
-
const map = new mobx_1.ObservableMap();
|
|
32
|
-
map.set("a", a);
|
|
33
|
-
map.set("b", b);
|
|
34
|
-
let runs = 0;
|
|
35
|
-
(0, mobx_1.autorun)(() => {
|
|
36
|
-
noop([...map.values()].map((a) => a.age));
|
|
37
|
-
runs++;
|
|
38
|
-
});
|
|
39
|
-
expect(runs).toBe(1);
|
|
40
|
-
b.age = 1;
|
|
41
|
-
expect(runs).toBe(2);
|
|
42
|
-
});
|
|
43
|
-
it("mobx lists maintain observable identity", () => {
|
|
44
|
-
// given a parent observable
|
|
45
|
-
const a = (0, mobx_1.observable)({ list: [] });
|
|
46
|
-
// if we observable-ize a value being pushing it on the list
|
|
47
|
-
const c1 = (0, mobx_1.observable)({});
|
|
48
|
-
a.list.push(c1);
|
|
49
|
-
// then we get identify equality on the list lookups
|
|
50
|
-
expect(a.list[0] === c1).toEqual(true);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
9
|
describe("formState", () => {
|
|
54
10
|
it("can create a simple object", () => {
|
|
55
11
|
const a = (0, objectField_1.createObjectState)({ title: { type: "value", rules: [rules_1.required] } }, {});
|
|
@@ -968,6 +924,15 @@ describe("formState", () => {
|
|
|
968
924
|
// Throw away assertion, test is making sure ^ line compiles
|
|
969
925
|
expect(config).toBeDefined();
|
|
970
926
|
});
|
|
927
|
+
it("supports observable objects with helper methods in the config DSL", () => {
|
|
928
|
+
const config = configBuilders_1.f.config({
|
|
929
|
+
firstName: configBuilders_1.f.value(),
|
|
930
|
+
lastName: configBuilders_1.f.value(),
|
|
931
|
+
fullName: configBuilders_1.f.computed(),
|
|
932
|
+
});
|
|
933
|
+
// Throw away assertion, test is making sure ^ line compiles
|
|
934
|
+
expect(config).toBeDefined();
|
|
935
|
+
});
|
|
971
936
|
it("can reset observable objects with computeds", () => {
|
|
972
937
|
const formState = (0, objectField_1.createObjectState)({
|
|
973
938
|
firstName: { type: "value" },
|
|
@@ -1512,19 +1477,17 @@ class ObservableObject {
|
|
|
1512
1477
|
return { firstName: this.firstName };
|
|
1513
1478
|
}
|
|
1514
1479
|
}
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
},
|
|
1527
|
-
};
|
|
1480
|
+
exports.ObservableObject = ObservableObject;
|
|
1481
|
+
const authorWithBooksConfig = configBuilders_1.f.config({
|
|
1482
|
+
id: configBuilders_1.f.value(),
|
|
1483
|
+
firstName: configBuilders_1.f.value(),
|
|
1484
|
+
lastName: configBuilders_1.f.value(),
|
|
1485
|
+
books: configBuilders_1.f.list({
|
|
1486
|
+
id: configBuilders_1.f.value(),
|
|
1487
|
+
title: configBuilders_1.f.value().req(),
|
|
1488
|
+
classification: configBuilders_1.f.value(),
|
|
1489
|
+
}),
|
|
1490
|
+
});
|
|
1528
1491
|
function createAuthorInputState(input, maybeAutoSave) {
|
|
1529
1492
|
return (0, objectField_1.createObjectState)(authorWithBooksConfig, input, { maybeAutoSave });
|
|
1530
1493
|
}
|
|
@@ -1540,27 +1503,42 @@ const authorWithAddressConfig = {
|
|
|
1540
1503
|
},
|
|
1541
1504
|
},
|
|
1542
1505
|
};
|
|
1543
|
-
const authorWithAddressAndBooksConfig = {
|
|
1544
|
-
id:
|
|
1545
|
-
firstName:
|
|
1546
|
-
lastName:
|
|
1547
|
-
address: {
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1506
|
+
const authorWithAddressAndBooksConfig = configBuilders_1.f.config({
|
|
1507
|
+
id: configBuilders_1.f.value(),
|
|
1508
|
+
firstName: configBuilders_1.f.value(),
|
|
1509
|
+
lastName: configBuilders_1.f.value(),
|
|
1510
|
+
address: configBuilders_1.f.object({
|
|
1511
|
+
id: configBuilders_1.f.value(),
|
|
1512
|
+
street: configBuilders_1.f.value().rules([rules_1.required]),
|
|
1513
|
+
city: configBuilders_1.f.value(),
|
|
1514
|
+
}),
|
|
1515
|
+
books: configBuilders_1.f.list({
|
|
1516
|
+
id: configBuilders_1.f.value(),
|
|
1517
|
+
title: configBuilders_1.f.value(),
|
|
1518
|
+
classification: configBuilders_1.f.value(),
|
|
1519
|
+
}),
|
|
1520
|
+
});
|
|
1521
|
+
// const authorWithAddressAndBooksConfig: ObjectConfig<AuthorInput> = {
|
|
1522
|
+
// id: { type: "value" },
|
|
1523
|
+
// firstName: { type: "value" },
|
|
1524
|
+
// lastName: { type: "value" },
|
|
1525
|
+
// address: {
|
|
1526
|
+
// type: "object",
|
|
1527
|
+
// config: {
|
|
1528
|
+
// id: { type: "value" },
|
|
1529
|
+
// street: { type: "value", rules: [required] },
|
|
1530
|
+
// city: { type: "value" },
|
|
1531
|
+
// },
|
|
1532
|
+
// },
|
|
1533
|
+
// books: {
|
|
1534
|
+
// type: "list",
|
|
1535
|
+
// config: {
|
|
1536
|
+
// id: { type: "value" },
|
|
1537
|
+
// title: { type: "value", rules: [required] },
|
|
1538
|
+
// classification: { type: "value" },
|
|
1539
|
+
// },
|
|
1540
|
+
// },
|
|
1541
|
+
// };
|
|
1564
1542
|
function createAuthorWithAddressInputState(input) {
|
|
1565
1543
|
return (0, objectField_1.createObjectState)(authorWithAddressConfig, input);
|
|
1566
1544
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { ObjectConfig } from "./config";
|
|
2
|
+
export { f } from "./configBuilders";
|
|
2
3
|
export { ListFieldState } from "./fields/listField";
|
|
3
4
|
export { ObjectState, createObjectState } from "./fields/objectField";
|
|
4
5
|
export { FieldState, ValueAdapter } from "./fields/valueField";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.useFormStates = exports.useFormState = exports.required = exports.createObjectState = void 0;
|
|
3
|
+
exports.useFormStates = exports.useFormState = exports.required = exports.createObjectState = exports.f = void 0;
|
|
4
|
+
var configBuilders_1 = require("./configBuilders");
|
|
5
|
+
Object.defineProperty(exports, "f", { enumerable: true, get: function () { return configBuilders_1.f; } });
|
|
4
6
|
var objectField_1 = require("./fields/objectField");
|
|
5
7
|
Object.defineProperty(exports, "createObjectState", { enumerable: true, get: function () { return objectField_1.createObjectState; } });
|
|
6
8
|
var rules_1 = require("./rules");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const mobx_1 = require("mobx");
|
|
4
|
+
const formState_test_1 = require("./formState.test");
|
|
5
|
+
describe("mobx behavior", () => {
|
|
6
|
+
it("mobx batches if in actions", () => {
|
|
7
|
+
const a = (0, mobx_1.observable)({ name: "a1" });
|
|
8
|
+
const b = (0, mobx_1.observable)({ name: "b1" });
|
|
9
|
+
const map = new mobx_1.ObservableMap();
|
|
10
|
+
map.set("a", a);
|
|
11
|
+
map.set("b", b);
|
|
12
|
+
let runs = 0;
|
|
13
|
+
(0, mobx_1.autorun)(() => {
|
|
14
|
+
noop([...map.values()].filter((v) => v.name.includes("1")));
|
|
15
|
+
runs++;
|
|
16
|
+
});
|
|
17
|
+
expect(runs).toBe(1);
|
|
18
|
+
// When we change both a and b
|
|
19
|
+
(0, mobx_1.runInAction)(() => {
|
|
20
|
+
a.name = "a11";
|
|
21
|
+
b.name = "b2";
|
|
22
|
+
});
|
|
23
|
+
// Then the reaction waited and only ran once
|
|
24
|
+
expect(runs).toBe(2);
|
|
25
|
+
});
|
|
26
|
+
it("mobx can watch undefined keys", () => {
|
|
27
|
+
const a = new formState_test_1.ObservableObject();
|
|
28
|
+
const b = new formState_test_1.ObservableObject();
|
|
29
|
+
const map = new mobx_1.ObservableMap();
|
|
30
|
+
map.set("a", a);
|
|
31
|
+
map.set("b", b);
|
|
32
|
+
let runs = 0;
|
|
33
|
+
(0, mobx_1.autorun)(() => {
|
|
34
|
+
noop([...map.values()].map((a) => a.age));
|
|
35
|
+
runs++;
|
|
36
|
+
});
|
|
37
|
+
expect(runs).toBe(1);
|
|
38
|
+
b.age = 1;
|
|
39
|
+
expect(runs).toBe(2);
|
|
40
|
+
});
|
|
41
|
+
it("mobx lists maintain observable identity", () => {
|
|
42
|
+
// given a parent observable
|
|
43
|
+
const a = (0, mobx_1.observable)({ list: [] });
|
|
44
|
+
// if we observable-ize a value being pushing it on the list
|
|
45
|
+
const c1 = (0, mobx_1.observable)({});
|
|
46
|
+
a.list.push(c1);
|
|
47
|
+
// then we get identify equality on the list lookups
|
|
48
|
+
expect(a.list[0] === c1).toEqual(true);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
function noop(t) { }
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
declare const c: {
|
|
2
|
+
value<V>(): ValueConfig<V>;
|
|
3
|
+
};
|
|
4
|
+
type Config<T> = {
|
|
5
|
+
[P in keyof T]: ValueConfig<T[P]>;
|
|
6
|
+
};
|
|
7
|
+
declare function config<T>(object: Config<T>): void;
|
|
8
|
+
interface ValueConfig<V> {
|
|
9
|
+
req(): ValueConfig<V>;
|
|
10
|
+
readOnly(): ValueConfig<V>;
|
|
11
|
+
validate(fn: (value: V) => boolean): ValueConfig<V>;
|
|
12
|
+
}
|
|
13
|
+
type Author = {
|
|
14
|
+
name: string;
|
|
15
|
+
address: string;
|
|
16
|
+
city: string;
|
|
17
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Create a Zod-style fluent DSL for building config
|
|
3
|
+
const c = {
|
|
4
|
+
value() {
|
|
5
|
+
return null;
|
|
6
|
+
},
|
|
7
|
+
};
|
|
8
|
+
function config(object) { }
|
|
9
|
+
// Try and configure the author
|
|
10
|
+
config({
|
|
11
|
+
// correctly inferred as c.value<string> --> ValueConfig<string>
|
|
12
|
+
name: c.value(),
|
|
13
|
+
// inferred only as c.value<unknown> --> ValueConfig<unknown>
|
|
14
|
+
address: c.value().req(),
|
|
15
|
+
// compile error, v is implicitly typed as any
|
|
16
|
+
city: c.value().validate((v) => v.length > 0),
|
|
17
|
+
});
|
package/dist/utils.d.ts
CHANGED
|
@@ -8,6 +8,9 @@ export type Primitive = undefined | null | boolean | string | number | Function
|
|
|
8
8
|
export type DeepRequired<T> = T extends Primitive ? T : {
|
|
9
9
|
[P in keyof Required<T>]: T[P] extends Array<infer U> ? Array<DeepRequired<U>> : T[P] extends ReadonlyArray<infer U2> ? ReadonlyArray<DeepRequired<U2>> : DeepRequired<T[P]>;
|
|
10
10
|
};
|
|
11
|
+
export type OmitIf<Base, Condition> = Pick<Base, {
|
|
12
|
+
[Key in keyof Base]: Base[Key] extends Condition ? never : Key;
|
|
13
|
+
}[keyof Base]>;
|
|
11
14
|
export declare function fail(message?: string): never;
|
|
12
15
|
export declare function assertNever(x: never): never;
|
|
13
16
|
/** Introspects the `init` prop to see if has a `map` function/etc. and returns the form value. */
|