@homebound/form-state 2.10.0 → 2.12.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.
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useFormState = void 0;
4
4
  const is_plain_object_1 = require("is-plain-object");
5
5
  const react_1 = require("react");
6
- const formState_1 = require("./formState");
6
+ const objectField_1 = require("./fields/objectField");
7
7
  const utils_1 = require("./utils");
8
8
  // If the user's autoSave hook makes some last-minute `.set` calls to sneak
9
9
  // in some business logic right before their GraphQL mutation call, ignore it
@@ -16,7 +16,7 @@ let pendingAutoSave = false;
16
16
  * Creates a formState instance for editing in a form.
17
17
  */
18
18
  function useFormState(opts) {
19
- const { config, init, addRules, readOnly = false, autoSave } = opts;
19
+ const { config, init, addRules, readOnly = false, loading, autoSave } = opts;
20
20
  // Use a ref so our memo'ized `onBlur` always see the latest value
21
21
  const autoSaveRef = react_1.useRef(autoSave);
22
22
  autoSaveRef.current = autoSave;
@@ -25,7 +25,7 @@ function useFormState(opts) {
25
25
  const [firstInitValue] = react_1.useState(() => utils_1.initValue(config, init));
26
26
  const isWrappingMobxProxy = !is_plain_object_1.isPlainObject(firstInitValue);
27
27
  // If they're using init.input, useMemo on it (and it might be an array), otherwise allow the identity of init be unstable
28
- const dep = init && "input" in init && "map" in init ? (Array.isArray(init.input) ? init.input : [init.input]) : [];
28
+ const dep = utils_1.isInput(init) ? makeArray(init.input) : utils_1.isQuery(init) ? [init.query.data, init.query.loading] : [];
29
29
  const form = react_1.useMemo(() => {
30
30
  function maybeAutoSave() {
31
31
  if (isAutoSaving === "in-flight") {
@@ -60,8 +60,9 @@ function useFormState(opts) {
60
60
  }
61
61
  }
62
62
  const value = firstRunRef.current ? firstInitValue : utils_1.initValue(config, init);
63
- const form = formState_1.createObjectState(config, value, { maybeAutoSave });
63
+ const form = objectField_1.createObjectState(config, value, { maybeAutoSave });
64
64
  form.readOnly = readOnly;
65
+ setLoading(form, opts);
65
66
  // The identity of `addRules` is not stable, but assume that it is for better UX.
66
67
  (addRules || (() => { }))(form);
67
68
  firstRunRef.current = true;
@@ -80,12 +81,34 @@ function useFormState(opts) {
80
81
  firstRunRef.current = false;
81
82
  return;
82
83
  }
84
+ setLoading(form, opts);
83
85
  form.set(utils_1.initValue(config, init), { refreshing: true });
84
86
  }, [form, ...dep]);
85
87
  // Use useEffect so that we don't touch the form.init proxy during a render
86
88
  react_1.useEffect(() => {
87
89
  form.readOnly = readOnly;
88
- }, [form, readOnly]);
90
+ if (loading !== undefined) {
91
+ form.loading = loading;
92
+ }
93
+ }, [form, readOnly, loading]);
89
94
  return form;
90
95
  }
91
96
  exports.useFormState = useFormState;
97
+ function setLoading(form, opts) {
98
+ const { loading, init } = opts;
99
+ if (loading !== undefined) {
100
+ // Prefer the explicit/top-level opts.loading if it's set
101
+ form.loading = loading;
102
+ }
103
+ else if (utils_1.isInput(init) && !init.ifUndefined) {
104
+ // Otherwise, check for `init.input`
105
+ form.loading = init.input === undefined;
106
+ }
107
+ else if (utils_1.isQuery(init)) {
108
+ // Or `query.loading`
109
+ form.loading = init.query.loading;
110
+ }
111
+ }
112
+ function makeArray(input) {
113
+ return Array.isArray(input) ? input : [input];
114
+ }
@@ -7,7 +7,7 @@ const mobx_1 = require("mobx");
7
7
  const mobx_react_1 = require("mobx-react");
8
8
  const react_2 = require("react");
9
9
  const FormStateApp_1 = require("./FormStateApp");
10
- const formState_1 = require("./formState");
10
+ const rules_1 = require("./rules");
11
11
  const useFormState_1 = require("./useFormState");
12
12
  describe("useFormState", () => {
13
13
  it("calls init.map if init.input is defined", async () => {
@@ -15,10 +15,10 @@ describe("useFormState", () => {
15
15
  function TestComponent() {
16
16
  const config = { firstName: { type: "value" } };
17
17
  // And we have query data that may or may not be defined
18
- const data = Math.random() >= 0 ? { firstName: "bob" } : undefined;
19
- // Then the lambda is passed the "de-undefined" data
18
+ const data = { firstName: "bob" };
20
19
  const form = useFormState_1.useFormState({
21
20
  config,
21
+ // Then the lambda is passed the "de-undefined" data, i.e. `d.firstName` is not a compile error
22
22
  init: { input: data, map: (d) => ({ firstName: d.firstName }) },
23
23
  });
24
24
  return jsx_runtime_1.jsx("div", { children: form.firstName.value }, void 0);
@@ -454,6 +454,51 @@ describe("useFormState", () => {
454
454
  expect(autoSaveStub).toBeCalledTimes(1);
455
455
  expect(autoSaveStub).toBeCalledWith({ id: "a:1", firstName: "first", lastName: "first" });
456
456
  });
457
+ it("sets loading if opts.loading is true", async () => {
458
+ const config = { firstName: { type: "value" } };
459
+ function TestComponent({ loading }) {
460
+ const form = useFormState_1.useFormState({ config, loading });
461
+ return jsx_runtime_1.jsx(mobx_react_1.Observer, { children: () => jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "loading" }, { children: String(form.loading) }), void 0) }, void 0);
462
+ }
463
+ // And we initially pass in `init.query.loading: true`
464
+ const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, { loading: true }, void 0));
465
+ // Then the form is marked as loading
466
+ expect(r.loading()).toHaveTextContent("true");
467
+ // And when the query is not loading
468
+ await r.rerender(jsx_runtime_1.jsx(TestComponent, { loading: false }, void 0));
469
+ // Then the form is marked as not loading
470
+ expect(r.loading()).toHaveTextContent("false");
471
+ });
472
+ it("sets loading if input.data is undefined", async () => {
473
+ const config = { firstName: { type: "value" } };
474
+ function TestComponent({ data }) {
475
+ const form = useFormState_1.useFormState({ config, init: { input: data, map: (d) => d } });
476
+ return jsx_runtime_1.jsx(mobx_react_1.Observer, { children: () => jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "loading" }, { children: String(form.loading) }), void 0) }, void 0);
477
+ }
478
+ // And we initially pass in `init.input: undefined`
479
+ const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, { data: undefined }, void 0));
480
+ // Then the form is marked as loading
481
+ expect(r.loading()).toHaveTextContent("true");
482
+ // And when the data is no longer undefined
483
+ await r.rerender(jsx_runtime_1.jsx(TestComponent, { data: { firstName: "first" } }, void 0));
484
+ // Then the form is marked as not loading
485
+ expect(r.loading()).toHaveTextContent("false");
486
+ });
487
+ it("sets loading if query.loading is true", async () => {
488
+ const config = { firstName: { type: "value" } };
489
+ function TestComponent({ loading, data }) {
490
+ const form = useFormState_1.useFormState({ config, init: { query: { data, loading, error: null }, map: (d) => d } });
491
+ return jsx_runtime_1.jsx(mobx_react_1.Observer, { children: () => jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "loading" }, { children: String(form.loading) }), void 0) }, void 0);
492
+ }
493
+ // And we initially pass in `init.query.loading: true`
494
+ const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, { loading: true, data: undefined }, void 0));
495
+ // Then the form is marked as loading
496
+ expect(r.loading()).toHaveTextContent("true");
497
+ // And when the query is not loading
498
+ await r.rerender(jsx_runtime_1.jsx(TestComponent, { loading: false, data: { firstName: "first" } }, void 0));
499
+ // Then the form is marked as not loading
500
+ expect(r.loading()).toHaveTextContent("false");
501
+ });
457
502
  });
458
503
  const authorConfig = {
459
504
  id: { type: "value" },
@@ -468,7 +513,7 @@ const authorWithBooksConfig = {
468
513
  type: "list",
469
514
  config: {
470
515
  id: { type: "value" },
471
- title: { type: "value", rules: [formState_1.required] },
516
+ title: { type: "value", rules: [rules_1.required] },
472
517
  classification: { type: "value" },
473
518
  },
474
519
  },
@@ -481,7 +526,7 @@ const authorWithAddressAndBooksConfig = {
481
526
  type: "object",
482
527
  config: {
483
528
  id: { type: "value" },
484
- street: { type: "value", rules: [formState_1.required] },
529
+ street: { type: "value", rules: [rules_1.required] },
485
530
  city: { type: "value" },
486
531
  },
487
532
  },
@@ -489,7 +534,7 @@ const authorWithAddressAndBooksConfig = {
489
534
  type: "list",
490
535
  config: {
491
536
  id: { type: "value" },
492
- title: { type: "value", rules: [formState_1.required] },
537
+ title: { type: "value", rules: [rules_1.required] },
493
538
  classification: { type: "value" },
494
539
  },
495
540
  },
@@ -1,4 +1,5 @@
1
- import { ObjectConfig, ObjectState } from "./formState";
1
+ import { ObjectConfig } from "./config";
2
+ import { ObjectState } from "./fields/objectField";
2
3
  export declare type ObjectStateCache<T, I> = Record<string, [ObjectState<T>, I]>;
3
4
  declare type UseFormStatesOpts<T, I> = {
4
5
  /**
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useFormStates = void 0;
4
4
  const react_1 = require("react");
5
- const formState_1 = require("./formState");
5
+ const objectField_1 = require("./fields/objectField");
6
6
  const utils_1 = require("./utils");
7
7
  function useFormStates(opts) {
8
8
  const { config, autoSave, getId, map, addRules, readOnly = false } = opts;
@@ -49,7 +49,7 @@ function useFormStates(opts) {
49
49
  }
50
50
  // If it didn't exist, then add to the cache.
51
51
  if (!form) {
52
- form = formState_1.createObjectState(config, utils_1.initValue(config, map ? { map, input } : input), {
52
+ form = objectField_1.createObjectState(config, utils_1.initValue(config, map ? { map, input } : input), {
53
53
  maybeAutoSave: () => maybeAutoSave(form),
54
54
  });
55
55
  if (addRules) {
@@ -4,8 +4,8 @@ const jsx_runtime_1 = require("react/jsx-runtime");
4
4
  const rtl_utils_1 = require("@homebound/rtl-utils");
5
5
  const mobx_1 = require("mobx");
6
6
  const react_1 = require("react");
7
- const formState_1 = require("./formState");
8
7
  const FormStateApp_1 = require("./FormStateApp");
8
+ const rules_1 = require("./rules");
9
9
  const useFormStates_1 = require("./useFormStates");
10
10
  describe("useFormStates", () => {
11
11
  it("can lazily create form states", async () => {
@@ -114,7 +114,7 @@ describe("useFormStates", () => {
114
114
  const originalConfig = { id: { type: "value" }, firstName: { type: "value" } };
115
115
  const updatedConfig = {
116
116
  id: { type: "value" },
117
- firstName: { type: "value", rules: [formState_1.required] },
117
+ firstName: { type: "value", rules: [rules_1.required] },
118
118
  };
119
119
  function TestComponent() {
120
120
  const [config, setConfig] = react_1.useState(originalConfig);
@@ -148,7 +148,7 @@ describe("useFormStates", () => {
148
148
  jsx_runtime_1.jsx(ChildComponent, { os: getFormState({ id: "a:1", firstName: "Brandon" }) }, void 0)] }, void 0));
149
149
  }
150
150
  // When we render
151
- const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
151
+ await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
152
152
  // Then addRules was only called once
153
153
  expect(addRules).toHaveBeenCalledTimes(1);
154
154
  });
package/dist/utils.d.ts CHANGED
@@ -1,8 +1,19 @@
1
- import { ObjectConfig } from "./formState";
1
+ import { ObjectConfig } from "./config";
2
+ import { InputAndMap, QueryAndMap, UseFormStateOpts } from "./useFormState";
3
+ export declare type Builtin = Date | Function | Uint8Array | string | number | boolean;
4
+ export declare type Primitive = undefined | null | boolean | string | number | Function | Date | {
5
+ toJSON(): any;
6
+ };
7
+ /** Makes the keys in `T` required while keeping the values undefined. */
8
+ export declare type DeepRequired<T> = T extends Primitive ? T : {
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
+ };
2
11
  export declare function fail(message?: string): never;
3
12
  export declare function assertNever(x: never): never;
4
13
  /** Introspects the `init` prop to see if has a `map` function/etc. and returns the form value. */
5
14
  export declare function initValue<T>(config: ObjectConfig<T>, init: any): T;
15
+ export declare function isInput<T, I>(init: UseFormStateOpts<T, I>["init"]): init is InputAndMap<T, I>;
16
+ export declare function isQuery<T, I>(init: UseFormStateOpts<T, I>["init"]): init is QueryAndMap<T, I>;
6
17
  /**
7
18
  * Picks ony fields out of `instance` that are in the `formConfig`.
8
19
  *
@@ -15,3 +26,18 @@ export declare function initValue<T>(config: ObjectConfig<T>, init: any): T;
15
26
  export declare function pickFields<T, I>(formConfig: ObjectConfig<T>, instance: I): {
16
27
  [K in keyof T]: K extends keyof I ? I[K] : never;
17
28
  };
29
+ export declare function isNotUndefined<T>(value: T | undefined): value is T;
30
+ export declare function isEmpty(value: any): boolean;
31
+ /**
32
+ * An equals that does deep-ish equality.
33
+ *
34
+ * We only do non-identity equals for:
35
+ *
36
+ * - "plain" objects that have no custom prototype/i.e. are object literals
37
+ * - objects that implement `toJSON`
38
+ *
39
+ */
40
+ export declare function areEqual<T>(a?: T, b?: T): boolean;
41
+ export declare function hasToJSON(o?: unknown): o is {
42
+ toJSON(): void;
43
+ };
package/dist/utils.js CHANGED
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.pickFields = exports.initValue = exports.assertNever = exports.fail = void 0;
6
+ exports.hasToJSON = exports.areEqual = exports.isEmpty = exports.isNotUndefined = exports.pickFields = exports.isQuery = exports.isInput = exports.initValue = exports.assertNever = exports.fail = void 0;
7
+ const fast_deep_equal_1 = __importDefault(require("fast-deep-equal"));
4
8
  const is_plain_object_1 = require("is-plain-object");
5
9
  const mobx_1 = require("mobx");
6
10
  function fail(message) {
@@ -13,14 +17,27 @@ function assertNever(x) {
13
17
  exports.assertNever = assertNever;
14
18
  /** Introspects the `init` prop to see if has a `map` function/etc. and returns the form value. */
15
19
  function initValue(config, init) {
16
- const initValue = init && "input" in init && "map" in init
17
- ? init.input
18
- ? init.map(init.input)
19
- : init.ifUndefined || {}
20
- : init || {};
21
- return pickFields(config, initValue);
20
+ let value;
21
+ if (isInput(init)) {
22
+ value = init.input ? init.map(init.input) : init.ifUndefined;
23
+ }
24
+ else if (isQuery(init)) {
25
+ value = init.query.data ? init.map(init.query.data) : init.ifUndefined;
26
+ }
27
+ else {
28
+ value = init;
29
+ }
30
+ return pickFields(config, value !== null && value !== void 0 ? value : {});
22
31
  }
23
32
  exports.initValue = initValue;
33
+ function isInput(init) {
34
+ return !!init && "input" in init && "map" in init;
35
+ }
36
+ exports.isInput = isInput;
37
+ function isQuery(init) {
38
+ return !!init && "query" in init && "map" in init;
39
+ }
40
+ exports.isQuery = isQuery;
24
41
  /**
25
42
  * Picks ony fields out of `instance` that are in the `formConfig`.
26
43
  *
@@ -68,3 +85,39 @@ function pickFields(formConfig, instance) {
68
85
  }));
69
86
  }
70
87
  exports.pickFields = pickFields;
88
+ function isNotUndefined(value) {
89
+ return value !== undefined;
90
+ }
91
+ exports.isNotUndefined = isNotUndefined;
92
+ function isEmpty(value) {
93
+ return value === undefined || value === null || value === "";
94
+ }
95
+ exports.isEmpty = isEmpty;
96
+ /**
97
+ * An equals that does deep-ish equality.
98
+ *
99
+ * We only do non-identity equals for:
100
+ *
101
+ * - "plain" objects that have no custom prototype/i.e. are object literals
102
+ * - objects that implement `toJSON`
103
+ *
104
+ */
105
+ function areEqual(a, b) {
106
+ if (is_plain_object_1.isPlainObject(a)) {
107
+ return fast_deep_equal_1.default(mobx_1.toJS(a), mobx_1.toJS(b));
108
+ }
109
+ if (hasToJSON(a) || hasToJSON(b)) {
110
+ const a1 = hasToJSON(a) ? a.toJSON() : a;
111
+ const b1 = hasToJSON(b) ? b.toJSON() : b;
112
+ return fast_deep_equal_1.default(a1, b1);
113
+ }
114
+ if (a && b && a instanceof Array && b instanceof Array) {
115
+ return fast_deep_equal_1.default(a, b);
116
+ }
117
+ return a === b;
118
+ }
119
+ exports.areEqual = areEqual;
120
+ function hasToJSON(o) {
121
+ return !!(o && typeof o === "object" && "toJSON" in o);
122
+ }
123
+ exports.hasToJSON = hasToJSON;
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const mobx_1 = require("mobx");
4
- const formState_1 = require("./formState");
4
+ const rules_1 = require("./rules");
5
5
  const utils_1 = require("./utils");
6
6
  describe("utils", () => {
7
7
  describe("pickFields", () => {
@@ -82,7 +82,7 @@ const authorWithAddressConfig = {
82
82
  address: {
83
83
  type: "object",
84
84
  config: {
85
- street: { type: "value", rules: [formState_1.required] },
85
+ street: { type: "value", rules: [rules_1.required] },
86
86
  city: { type: "value" },
87
87
  },
88
88
  },
@@ -95,7 +95,7 @@ const authorWithBooksConfig = {
95
95
  type: "list",
96
96
  config: {
97
97
  id: { type: "value" },
98
- title: { type: "value", rules: [formState_1.required] },
98
+ title: { type: "value", rules: [rules_1.required] },
99
99
  classification: { type: "value" },
100
100
  },
101
101
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/form-state",
3
- "version": "2.10.0",
3
+ "version": "2.12.0",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -1,192 +0,0 @@
1
- /**
2
- * Wraps a given input/on-the-wire type `T` for editing in a form.
3
- *
4
- * We basically mimic every field in `T` (i.e. `firstName`, `lastName`, etc.) but decorate them
5
- * with form-specific state like `touched`, `dirty`, `errors`, etc.
6
- *
7
- * The intent is that, after ensuring all fields are `valid`/etc., callers can take the
8
- * result of this `objectState.value` (or `objectState.originalValue` for the non-proxy version) and
9
- * have exactly the on-the-wire type `T` that they need to submit to the backend, without doing the
10
- * manual mapping of "data that was in the form controls" into "data that the backend wants".
11
- *
12
- * Note that this can be hierarchical by having by having a field of `ListFieldState` that
13
- * themselves each wrap an `ObjectState`, i.e.:
14
- *
15
- * ```
16
- * ObjectState for author
17
- * - firstName: FieldState
18
- * - lastName: FieldState
19
- * - rows: ListFieldState
20
- * - [0]: ObjectState for book 1
21
- * - title: FieldState
22
- * - [1]: ObjectState for book 2
23
- * - title: FieldState
24
- * ```
25
- */
26
- export declare type ObjectState<T, P = any> = FieldStates<T> & FieldState<P, T> & {
27
- /** Sets the state of fields in `state`. */
28
- set(state: Partial<T>, opts?: SetOpts): void;
29
- /** Returns whether the object can be saved, i.e. is valid, but also as a side-effect marks touched. */
30
- canSave(): boolean;
31
- };
32
- export declare type Builtin = Date | Function | Uint8Array | string | number | boolean;
33
- /** 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. */
34
- declare type FieldStates<T> = {
35
- [K in keyof T]-?: T[K] extends Array<infer U> | null | undefined ? [U] extends [Builtin] ? FieldState<T, T[K]> : ListFieldState<T, U> : T[K] extends Builtin | null | undefined ? FieldState<T, T[K]> : ObjectState<T[K], T>;
36
- };
37
- declare type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N;
38
- /** A validation rule, given the value and name, return the error string if valid, or undefined if valid. */
39
- export declare type Rule<T, V> = (opts: {
40
- value: V;
41
- key: string;
42
- originalValue: V;
43
- object: IfAny<T, any, ObjectState<T>>;
44
- }) => string | undefined;
45
- /** A rule that validates `value` is not `undefined`, `null`, or empty string. */
46
- export declare const required: <V>({ value: v }: {
47
- value: V;
48
- }) => string | undefined;
49
- /**
50
- * Form state for a primitive field in the form, i.e. its value but also touched/validation/etc. state.
51
- *
52
- * This API also provides hooks for form elements to call into, i.e. `blur()` and `set(...)` that will
53
- * update the field state and re-render, i.e. when including in an `ObjectState`-typed literal that is
54
- * an mobx `useLocalObservable`/observable.
55
- *
56
- * Note that `V` will always have `null | undefined` added to it by `FieldStates`, b/c most form fields
57
- * i.e. text boxes, can always be cleared out/deleted.
58
- */
59
- export interface FieldState<T, V> {
60
- readonly key: string;
61
- value: V;
62
- readonly originalValue: V;
63
- touched: boolean;
64
- readOnly: boolean;
65
- readonly required: boolean;
66
- readonly dirty: boolean;
67
- readonly valid: boolean;
68
- readonly isNewEntity: boolean;
69
- rules: Rule<T, V>[];
70
- readonly errors: string[];
71
- /** Returns a subset of V with only the changed values. Currently not observable. */
72
- readonly changedValue: V;
73
- /** Focuses the field. Disables changes from `ObjectState.set` calls. */
74
- focus(): void;
75
- /** Blur marks the field as touched and triggers an auto-save. */
76
- blur(): void;
77
- /** Triggers an auto-save; the caller (i.e. useFormState) should still dirty & valid check. */
78
- maybeAutoSave(): void;
79
- set(value: V, opts?: SetOpts): void;
80
- /** Reverts back to the original value and resets dirty/touched. */
81
- reset(): void;
82
- /** Accepts the current changed value (if any) as the original and resets dirty/touched. */
83
- save(): void;
84
- }
85
- /** Public options for our `set` command. */
86
- export interface SetOpts {
87
- /** Whether this `set` should trigger an auto-save; defaults to true. */
88
- autoSave?: boolean;
89
- }
90
- /** Internal `.set` opts for conditionally that form-state internally percolates. */
91
- interface InternalSetOpts extends SetOpts {
92
- /** `resetting` is the code calling `.reset()` to revert to original values. */
93
- resetting?: boolean;
94
- /** `refreshing` is when `useFormState` sees a new value. */
95
- refreshing?: boolean;
96
- }
97
- export interface FieldStateInternal<T, V> extends FieldState<T, V> {
98
- set(value: V, opts?: InternalSetOpts): void;
99
- _isIdKey: boolean;
100
- _isDeleteKey: boolean;
101
- _isReadOnlyKey: boolean;
102
- _focused: boolean;
103
- }
104
- export declare type ObjectStateInternal<T, P = any> = ObjectState<T, P> & {
105
- set(value: P, opts?: InternalSetOpts): void;
106
- };
107
- /** Form state for list of children, i.e. `U` is a `Book` in a form with a `books: Book[]`. */
108
- export interface ListFieldState<T, U> extends Omit<FieldState<T, U[]>, "originalValue"> {
109
- readonly rows: ReadonlyArray<ObjectState<U>>;
110
- add(value: U, index?: number): void;
111
- remove(indexOrValue: number | U): void;
112
- }
113
- /**
114
- * Config rules for each field in `T` that we're editing in a form.
115
- *
116
- * Basically every field is either a value/primitive or a list, and this `ObjectConfig` lets
117
- * the caller define field-specific behavior, i.e. validation rules.
118
- */
119
- export declare type ObjectConfig<T> = {
120
- [P in keyof OmitIf<T, Function>]: 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]>;
121
- };
122
- declare type OmitIf<Base, Condition> = Pick<Base, {
123
- [Key in keyof Base]: Base[Key] extends Condition ? never : Key;
124
- }[keyof Base]>;
125
- /** Field configuration for primitive values, i.e. strings/numbers/Dates/user-defined types. */
126
- export declare type ValueFieldConfig<T, V> = {
127
- type: "value";
128
- rules?: Rule<T, V | null | undefined>[];
129
- /**
130
- * If true, marks this field as the id, which will be used for things like "always include in changedValue".
131
- *
132
- * Defaults to true for fields named `id`.
133
- */
134
- isIdKey?: boolean;
135
- /** If true, and this value is used on an entity in a list, the entity won't count towards the list validity. */
136
- isDeleteKey?: boolean;
137
- /** If true, the entity that contains this value will be treated as read only. */
138
- isReadOnlyKey?: boolean;
139
- /**
140
- * Marks a field as being backed by a mobx class computed field.
141
- *
142
- * Note that it might still be settable (some computed have setters), but we do
143
- * exclude from the `reset` operation, i.e. we assume resetting other non-computed fields
144
- * will effectively reset this field as well.
145
- */
146
- computed?: boolean;
147
- /** Marks a field as being initiallyread-only, i.e. `field.readOnly = true/false` can change this default. */
148
- readOnly?: boolean;
149
- };
150
- /** Field configuration for list values, i.e. `U` is `Book` in a form with `books: Book[]`. */
151
- export declare type ListFieldConfig<T, U> = {
152
- type: "list";
153
- /** Rules that can run on the full list of children. */
154
- rules?: Rule<T, readonly ObjectState<U>[]>[];
155
- /** Config for each child's form state, i.e. each book. */
156
- config: ObjectConfig<U>;
157
- /**
158
- * What the server-side update behavior is for this collection.
159
- *
160
- * When exhaustive, we include all rows in `changedValue` so that we
161
- * don't orhan unchanged rows.
162
- *
163
- * When incremental, we only include changed rows in `changedValue`.
164
- *
165
- * Defaults to `exhaustive` b/c that is the safest and also Joist's
166
- * default behavior.
167
- */
168
- update?: "exhaustive" | "incremental";
169
- };
170
- export declare type ObjectFieldConfig<U> = {
171
- type: "object";
172
- /** Config for the child's form state, i.e. each book. */
173
- config: ObjectConfig<U>;
174
- };
175
- /**
176
- * Creates a new `ObjectState` for a given form object `T` given config rules in `config`.
177
- *
178
- * The returned `ObjectState` can be used in a mobx `useLocalObservable` to driven an
179
- * interactive form that tracks the current valid/touched/etc. state of both each
180
- * individual fields as well as the top-level form/object itself.
181
- */
182
- export declare function createObjectState<T>(config: ObjectConfig<T>, instance: T, opts?: {
183
- maybeAutoSave?: () => void;
184
- }): ObjectState<T>;
185
- declare type Primitive = undefined | null | boolean | string | number | Function | Date | {
186
- toJSON(): any;
187
- };
188
- /** Makes the keys in `T` required while keeping the values undefined. */
189
- export declare type DeepRequired<T> = T extends Primitive ? T : {
190
- [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]>;
191
- };
192
- export {};