@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 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 store in the global store).
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 exhausive (if any child changes, submit them all) or incremental (only include changed children), to match the backend endpoint's semantics
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: V
41
- errors: string[]
42
- valid: boolean
43
- touched: boolean
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 libraries, which is "one line per form field", i.e:
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).
@@ -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: { type: "value", rules: [index_1.required] },
38
- lastName: { type: "value", rules: [index_1.required], readOnly: true },
39
- books: {
40
- type: "list",
41
- rules: [({ value: list }) => ((list || []).length === 0 ? "Empty" : undefined)],
42
- config: {
43
- title: { type: "value", rules: [index_1.required] },
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, T[P]> : ListFieldConfig<T, U> : ValueFieldConfig<T, T[P]> | ObjectFieldConfig<T[P]>;
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<T, V> = {
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<T, U> = {
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<T, U>, config: ObjectConfig<U>, strictOrder: boolean, maybeAutoSave: () => void): ListFieldState<U>;
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;
@@ -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
+ }
@@ -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
- const authorWithBooksConfig = {
1516
- id: { type: "value" },
1517
- firstName: { type: "value" },
1518
- lastName: { type: "value" },
1519
- books: {
1520
- type: "list",
1521
- config: {
1522
- id: { type: "value" },
1523
- title: { type: "value", rules: [rules_1.required] },
1524
- classification: { type: "value" },
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: { type: "value" },
1545
- firstName: { type: "value" },
1546
- lastName: { type: "value" },
1547
- address: {
1548
- type: "object",
1549
- config: {
1550
- id: { type: "value" },
1551
- street: { type: "value", rules: [rules_1.required] },
1552
- city: { type: "value" },
1553
- },
1554
- },
1555
- books: {
1556
- type: "list",
1557
- config: {
1558
- id: { type: "value" },
1559
- title: { type: "value", rules: [rules_1.required] },
1560
- classification: { type: "value" },
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. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/form-state",
3
- "version": "2.20.1",
3
+ "version": "2.21.0",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",