@homebound/form-state 2.9.1 → 2.11.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.
@@ -0,0 +1,66 @@
1
+ import { ObjectState } from "./fields/objectField";
2
+ import { Rule } from "./rules";
3
+ import { Builtin } from "./utils";
4
+ /**
5
+ * Config rules for each field in `T` that we're editing in a form.
6
+ *
7
+ * Basically every field is either a value/primitive or a list, and this `ObjectConfig` lets
8
+ * the caller define field-specific behavior, i.e. validation rules.
9
+ */
10
+ export declare type ObjectConfig<T> = {
11
+ [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]>;
12
+ };
13
+ declare type OmitIf<Base, Condition> = Pick<Base, {
14
+ [Key in keyof Base]: Base[Key] extends Condition ? never : Key;
15
+ }[keyof Base]>;
16
+ /** Field configuration for primitive values, i.e. strings/numbers/Dates/user-defined types. */
17
+ export declare type ValueFieldConfig<T, V> = {
18
+ type: "value";
19
+ rules?: Rule<T, V | null | undefined>[];
20
+ /**
21
+ * If true, marks this field as the id, which will be used for things like "always include in changedValue".
22
+ *
23
+ * Defaults to true for fields named `id`.
24
+ */
25
+ isIdKey?: boolean;
26
+ /** If true, and this value is used on an entity in a list, the entity won't count towards the list validity. */
27
+ isDeleteKey?: boolean;
28
+ /** If true, the entity that contains this value will be treated as read only. */
29
+ isReadOnlyKey?: boolean;
30
+ /**
31
+ * Marks a field as being backed by a mobx class computed field.
32
+ *
33
+ * Note that it might still be settable (some computed have setters), but we do
34
+ * exclude from the `reset` operation, i.e. we assume resetting other non-computed fields
35
+ * will effectively reset this field as well.
36
+ */
37
+ computed?: boolean;
38
+ /** Marks a field as being initially read-only, i.e. `field.readOnly = true/false` can change this default. */
39
+ readOnly?: boolean;
40
+ };
41
+ /** Field configuration for list values, i.e. `U` is `Book` in a form with `books: Book[]`. */
42
+ export declare type ListFieldConfig<T, U> = {
43
+ type: "list";
44
+ /** Rules that can run on the full list of children. */
45
+ rules?: Rule<T, readonly ObjectState<U>[]>[];
46
+ /** Config for each child's form state, i.e. each book. */
47
+ config: ObjectConfig<U>;
48
+ /**
49
+ * What the server-side update behavior is for this collection.
50
+ *
51
+ * When exhaustive, we include all rows in `changedValue` so that we
52
+ * don't orhan unchanged rows.
53
+ *
54
+ * When incremental, we only include changed rows in `changedValue`.
55
+ *
56
+ * Defaults to `exhaustive` b/c that is the safest and also Joist's
57
+ * default behavior.
58
+ */
59
+ update?: "exhaustive" | "incremental";
60
+ };
61
+ export declare type ObjectFieldConfig<U> = {
62
+ type: "object";
63
+ /** Config for the child's form state, i.e. each book. */
64
+ config: ObjectConfig<U>;
65
+ };
66
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,11 @@
1
+ import { ListFieldConfig, ObjectConfig } from "../config";
2
+ import { ObjectState } from "./objectField";
3
+ import { FieldState } from "./valueField";
4
+ import { Rule } from "../rules";
5
+ /** Form state for list of children, i.e. `U` is a `Book` in a form with a `books: Book[]`. */
6
+ export interface ListFieldState<T, U> extends Omit<FieldState<T, U[]>, "originalValue"> {
7
+ readonly rows: ReadonlyArray<ObjectState<U>>;
8
+ add(value: U, index?: number): void;
9
+ remove(indexOrValue: number | U): void;
10
+ }
11
+ export declare function newListFieldState<T, K extends keyof T, U>(parentInstance: T, parentState: () => ObjectState<T>, key: K, rules: Rule<T, readonly ObjectState<U>[]>[], listConfig: ListFieldConfig<T, U>, config: ObjectConfig<U>, maybeAutoSave: () => void): ListFieldState<T, U>;
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.newListFieldState = void 0;
4
+ const mobx_1 = require("mobx");
5
+ const objectField_1 = require("./objectField");
6
+ const rules_1 = require("../rules");
7
+ const utils_1 = require("../utils");
8
+ function newListFieldState(parentInstance, parentState, key, rules, listConfig, config, maybeAutoSave) {
9
+ // Keep a map of "item in the parent list" -> "that item's ObjectState"
10
+ const rowMap = new Map();
11
+ const _tick = mobx_1.observable({ value: 1 });
12
+ // this is for dirty checking, not object identity
13
+ let originalCopy = [...(parentInstance[key] || [])];
14
+ const list = {
15
+ key: key,
16
+ // Our fundamental state of wrapped Us
17
+ get value() {
18
+ return _tick.value > 0 ? parentInstance[key] : utils_1.fail();
19
+ },
20
+ _focused: false,
21
+ _readOnly: false,
22
+ _loading: false,
23
+ get readOnly() {
24
+ return this._readOnly || parentState().readOnly;
25
+ },
26
+ set readOnly(readOnly) {
27
+ this._readOnly = readOnly;
28
+ },
29
+ get loading() {
30
+ return this._loading || parentState().loading;
31
+ },
32
+ set loading(loading) {
33
+ this._loading = loading;
34
+ },
35
+ set value(v) {
36
+ this.set(v);
37
+ },
38
+ get dirty() {
39
+ return this.rows.some((r) => r.dirty) || this.hasNewItems();
40
+ },
41
+ get required() {
42
+ return this.rules.some((rule) => rule === rules_1.required);
43
+ },
44
+ get isNewEntity() {
45
+ return parentState().isNewEntity;
46
+ },
47
+ // private
48
+ hasNewItems() {
49
+ const [current, original] = [this.value || [], originalCopy || []];
50
+ // Instead of relying on just object identities, we look up each child's state
51
+ // in rowMap, because we already dedup/check object identity (i.e. look for id fields)
52
+ // when create object states.
53
+ const a = current.every((e) => {
54
+ const state = rowMap.get(e);
55
+ return original.some((e) => rowMap.get(e) === state);
56
+ });
57
+ const b = original.every((e) => {
58
+ const state = rowMap.get(e);
59
+ return current.some((e) => rowMap.get(e) === state);
60
+ });
61
+ const isSame = a && b;
62
+ return !isSame;
63
+ },
64
+ // And we can derive each value's ObjectState wrapper as needed from the rowMap cache
65
+ get rows() {
66
+ // It's unclear why we need to access _tick.value here, b/c calling `this.value` should
67
+ // transitively register us as a dependency on it
68
+ if (_tick.value < 0)
69
+ utils_1.fail();
70
+ return (this.value || []).map((child) => {
71
+ // Because we're reading from this.value, child will be the proxy version
72
+ let childState = rowMap.get(child);
73
+ if (!childState) {
74
+ childState = objectField_1.newObjectState(config, parentState, list, child, undefined, maybeAutoSave);
75
+ rowMap.set(child, childState);
76
+ }
77
+ return childState;
78
+ });
79
+ },
80
+ // TODO Should this be true when all rows are touched?
81
+ get touched() {
82
+ return this.rows.some((r) => r.touched) || this.hasNewItems();
83
+ },
84
+ set touched(touched) {
85
+ this.rows.forEach((r) => (r.touched = touched));
86
+ },
87
+ rules,
88
+ get valid() {
89
+ const value = this.rows;
90
+ // TODO Passing `originalCopy || []` is probably not 100% right
91
+ const opts = { value, key: key, originalValue: originalCopy || [], object: parentState() };
92
+ const collectionValid = this.rules.every((r) => r(opts) === undefined);
93
+ const entriesValid = this.rows.filter((r) => !r._considerDeleted()).every((r) => r.valid);
94
+ return collectionValid && entriesValid;
95
+ },
96
+ get errors() {
97
+ if (_tick.value < 0)
98
+ utils_1.fail();
99
+ const opts = { value: this.rows, key: key, originalValue: originalCopy || [], object: parentState() };
100
+ return this.rules.map((r) => r(opts)).filter(utils_1.isNotUndefined);
101
+ },
102
+ get changedValue() {
103
+ const result = [];
104
+ const hasOpKey = Object.keys(listConfig.config).includes("op");
105
+ const hasLegacyOpKey = Object.keys(listConfig.config).some((key) => key === "delete" || key === "remove");
106
+ const incremental = listConfig.update === "incremental" ||
107
+ // Implicitly enable incremental mode if we see an op key
108
+ (listConfig.update === undefined && (hasOpKey || hasLegacyOpKey));
109
+ const exhaustive = !incremental;
110
+ this.rows.forEach((r) => {
111
+ var _a;
112
+ var _b;
113
+ if (exhaustive || r.dirty || r.isNewEntity) {
114
+ const changed = r.changedValue;
115
+ // Ensure we have an `op: include` key, following https://joist-orm.io/docs/features/partial-update-apis
116
+ if (incremental && hasOpKey) {
117
+ (_a = (_b = changed).op) !== null && _a !== void 0 ? _a : (_b.op = "include");
118
+ }
119
+ result.push(changed);
120
+ }
121
+ });
122
+ return result;
123
+ },
124
+ focus() {
125
+ this._focused = true;
126
+ },
127
+ blur() {
128
+ this._focused = false;
129
+ this.maybeAutoSave();
130
+ },
131
+ maybeAutoSave() {
132
+ this.touched = true;
133
+ maybeAutoSave();
134
+ },
135
+ set(values, opts = {}) {
136
+ if (this.readOnly && !opts.resetting && !opts.refreshing) {
137
+ throw new Error(`${key} is currently readOnly`);
138
+ }
139
+ // We should be passed values that are non-proxies.
140
+ parentInstance[key] = values.map((value) => {
141
+ let childState = rowMap.get(value);
142
+ if (!childState) {
143
+ // Look for an existing child (requires having an id key configured)
144
+ for (const [, otherState] of rowMap.entries()) {
145
+ if (otherState.isSameEntity(value)) {
146
+ otherState.set(value, opts);
147
+ rowMap.set(value, otherState);
148
+ return otherState.value;
149
+ }
150
+ }
151
+ // If we didn't have an existing child, just make a new object state
152
+ childState = objectField_1.createObjectState(config, value, { maybeAutoSave });
153
+ rowMap.set(value, childState);
154
+ }
155
+ // Return the already-observable'd value so that our `parent.value[key] = values` doesn't re-proxy things
156
+ return childState.value;
157
+ });
158
+ // Reset originalCopy so that our dirty checks have the right # of rows.
159
+ if (opts.refreshing) {
160
+ originalCopy = [...(parentInstance[key] || [])];
161
+ }
162
+ _tick.value++;
163
+ },
164
+ add(value, spliceIndex) {
165
+ // This is called by the user, so value should be a non-proxy value we should keep
166
+ const childState = objectField_1.createObjectState(config, value, { maybeAutoSave });
167
+ rowMap.set(value, childState);
168
+ this.ensureSet();
169
+ this.value.splice(typeof spliceIndex === "number" ? spliceIndex : this.value.length, 0, childState.value);
170
+ _tick.value++;
171
+ maybeAutoSave();
172
+ },
173
+ remove(indexOrValue) {
174
+ this.ensureSet();
175
+ if (typeof indexOrValue === "number") {
176
+ this.value.splice(indexOrValue, 1);
177
+ }
178
+ else {
179
+ const index = this.value.findIndex((v) => v === indexOrValue);
180
+ if (index > -1) {
181
+ this.value.splice(index, 1);
182
+ }
183
+ }
184
+ _tick.value++;
185
+ maybeAutoSave();
186
+ },
187
+ reset() {
188
+ if (originalCopy) {
189
+ this.set(originalCopy, { resetting: true });
190
+ this.rows.forEach((r) => r.reset());
191
+ }
192
+ },
193
+ save() {
194
+ this.rows.forEach((r) => {
195
+ r.save();
196
+ });
197
+ originalCopy = [...(parentInstance[key] || [])];
198
+ _tick.value++;
199
+ },
200
+ ensureSet() {
201
+ if (!parentInstance[key]) {
202
+ parentInstance[key] = [];
203
+ }
204
+ _tick.value++;
205
+ },
206
+ };
207
+ return mobx_1.makeAutoObservable(list, {
208
+ // See other makeAutoObservable comment
209
+ value: mobx_1.computed({ equals: () => false }),
210
+ });
211
+ }
212
+ exports.newListFieldState = newListFieldState;
@@ -0,0 +1,54 @@
1
+ import { ObjectConfig } from "../config";
2
+ import { ListFieldState } from "./listField";
3
+ import { FieldState, InternalSetOpts, SetOpts } from "./valueField";
4
+ import { Builtin } from "../utils";
5
+ /**
6
+ * Wraps a given input/on-the-wire type `T` for editing in a form.
7
+ *
8
+ * We basically mimic every field in `T` (i.e. `firstName`, `lastName`, etc.) but decorate them
9
+ * with form-specific state like `touched`, `dirty`, `errors`, etc.
10
+ *
11
+ * The intent is that, after ensuring all fields are `valid`/etc., callers can take the
12
+ * result of this `objectState.value` (or `objectState.originalValue` for the non-proxy version) and
13
+ * have exactly the on-the-wire type `T` that they need to submit to the backend, without doing the
14
+ * manual mapping of "data that was in the form controls" into "data that the backend wants".
15
+ *
16
+ * Note that this can be hierarchical by having by having a field of `ListFieldState` that
17
+ * themselves each wrap an `ObjectState`, i.e.:
18
+ *
19
+ * ```
20
+ * ObjectState for author
21
+ * - firstName: FieldState
22
+ * - lastName: FieldState
23
+ * - rows: ListFieldState
24
+ * - [0]: ObjectState for book 1
25
+ * - title: FieldState
26
+ * - [1]: ObjectState for book 2
27
+ * - title: FieldState
28
+ * ```
29
+ */
30
+ export declare type ObjectState<T, P = any> = FieldStates<T> & FieldState<P, T> & {
31
+ /** Sets the state of fields in `state`. */
32
+ set(state: Partial<T>, opts?: SetOpts): void;
33
+ /** Returns whether the object can be saved, i.e. is valid, but also as a side-effect marks touched. */
34
+ canSave(): boolean;
35
+ };
36
+ export declare type ObjectStateInternal<T, P = any> = ObjectState<T, P> & {
37
+ set(value: P, opts?: InternalSetOpts): void;
38
+ };
39
+ /** 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. */
40
+ declare type FieldStates<T> = {
41
+ [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>;
42
+ };
43
+ /**
44
+ * Creates a new `ObjectState` for a given form object `T` given config rules in `config`.
45
+ *
46
+ * The returned `ObjectState` can be used in a mobx `useLocalObservable` to drive an
47
+ * interactive form that tracks the current valid/touched/etc. state of both each
48
+ * individual fields as well as the top-level form/object itself.
49
+ */
50
+ export declare function createObjectState<T>(config: ObjectConfig<T>, instance: T, opts?: {
51
+ maybeAutoSave?: () => void;
52
+ }): ObjectState<T>;
53
+ export declare function newObjectState<T, P = any>(config: ObjectConfig<T>, parentState: (() => ObjectState<P>) | undefined, parentListState: FieldState<any, any> | undefined, instance: T, key: keyof T | undefined, maybeAutoSave: () => void): ObjectState<T, P>;
54
+ export {};
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.newObjectState = exports.createObjectState = void 0;
4
+ const mobx_1 = require("mobx");
5
+ const listField_1 = require("./listField");
6
+ const valueField_1 = require("./valueField");
7
+ const utils_1 = require("../utils");
8
+ /**
9
+ * Creates a new `ObjectState` for a given form object `T` given config rules in `config`.
10
+ *
11
+ * The returned `ObjectState` can be used in a mobx `useLocalObservable` to drive an
12
+ * interactive form that tracks the current valid/touched/etc. state of both each
13
+ * individual fields as well as the top-level form/object itself.
14
+ */
15
+ function createObjectState(config, instance, opts = {}) {
16
+ const noop = () => { };
17
+ return newObjectState(config, undefined, undefined, instance, undefined, opts.maybeAutoSave || noop);
18
+ }
19
+ exports.createObjectState = createObjectState;
20
+ function newObjectState(config, parentState, parentListState, instance, key, maybeAutoSave) {
21
+ // This is what we return, but we only know it's value until we call `observable`, so
22
+ // we create a mutable variable to capture it so that we can create fields/call their
23
+ // constructors and give them a way to access it later.
24
+ let proxy = undefined;
25
+ function getObjectState() {
26
+ if (!proxy) {
27
+ throw new Error("Race condition");
28
+ }
29
+ return proxy;
30
+ }
31
+ const fieldStates = Object.entries(config).map(([_key, _config]) => {
32
+ const key = _key;
33
+ const config = _config;
34
+ let field;
35
+ if (config.type === "value") {
36
+ field = valueField_1.newValueFieldState(instance, getObjectState, key, config.rules || [], config.isIdKey || key === "id", config.isDeleteKey || false, config.isReadOnlyKey || false, config.computed || false, config.readOnly || false, maybeAutoSave);
37
+ }
38
+ else if (config.type === "list") {
39
+ field = listField_1.newListFieldState(instance, getObjectState, key, config.rules || [], config, config.config, maybeAutoSave);
40
+ }
41
+ else if (config.type === "object") {
42
+ if (!instance[key]) {
43
+ instance[key] = {};
44
+ }
45
+ field = newObjectState(config.config, getObjectState, undefined, instance[key], key, maybeAutoSave);
46
+ }
47
+ else {
48
+ throw new Error("Unsupported");
49
+ }
50
+ return [key, field];
51
+ });
52
+ // We always return the same `instance` field from our `value` method, but
53
+ // we want to pretend that it's observable, so use a tick to force it.
54
+ const _tick = mobx_1.observable({ value: 1 });
55
+ const fieldNames = Object.keys(config);
56
+ function getFields(proxyThis) {
57
+ return fieldNames.map((name) => proxyThis[name]);
58
+ }
59
+ const obj = {
60
+ ...Object.fromEntries(fieldStates),
61
+ key,
62
+ get value() {
63
+ _tick.value > 0 || utils_1.fail();
64
+ return instance;
65
+ },
66
+ set value(value) {
67
+ this.set(value);
68
+ },
69
+ // private
70
+ _readOnly: false,
71
+ _loading: false,
72
+ _considerDeleted() {
73
+ const deleteField = getFields(this).find((f) => f._isDeleteKey);
74
+ return !!(deleteField === null || deleteField === void 0 ? void 0 : deleteField.value);
75
+ },
76
+ _considerReadOnly() {
77
+ const readOnlyField = getFields(this).find((f) => f._isReadOnlyKey);
78
+ return !!(readOnlyField === null || readOnlyField === void 0 ? void 0 : readOnlyField.value);
79
+ },
80
+ get touched() {
81
+ return getFields(this).some((f) => f.touched);
82
+ },
83
+ set touched(touched) {
84
+ getFields(this).forEach((f) => (f.touched = touched));
85
+ },
86
+ get readOnly() {
87
+ return (this._readOnly ||
88
+ this._considerReadOnly() ||
89
+ (parentState && parentState().readOnly) ||
90
+ !!(parentListState === null || parentListState === void 0 ? void 0 : parentListState.readOnly));
91
+ },
92
+ set readOnly(readOnly) {
93
+ this._readOnly = readOnly;
94
+ },
95
+ get loading() {
96
+ return this._loading || (parentState && parentState().loading) || !!(parentListState === null || parentListState === void 0 ? void 0 : parentListState.loading);
97
+ },
98
+ set loading(loading) {
99
+ this._loading = loading;
100
+ },
101
+ get valid() {
102
+ return getFields(this).every((f) => f.valid);
103
+ },
104
+ get errors() {
105
+ return getFields(this).flatMap((f) => f.errors.map((e) => `${f.key}: ${e}`));
106
+ },
107
+ get dirty() {
108
+ return getFields(this).some((f) => f.dirty);
109
+ },
110
+ get isNewEntity() {
111
+ const idField = getFields(this).find((f) => f._isIdKey);
112
+ // If we're a line item w/o an immediate id field, look in our parent
113
+ if (!idField && parentState) {
114
+ return parentState().isNewEntity;
115
+ }
116
+ // If there is no id field, assume we're not new
117
+ return idField !== undefined && (idField.value === null || idField.value === undefined);
118
+ },
119
+ canSave() {
120
+ this.touched = true;
121
+ return this.valid;
122
+ },
123
+ // Accepts new values in bulk, i.e. when setting the form initial state from the backend.
124
+ set(value, opts = {}) {
125
+ if (this.readOnly && !opts.resetting && !opts.refreshing) {
126
+ throw new Error(`${key || "formState"} is currently readOnly`);
127
+ }
128
+ getFields(this).forEach((field) => {
129
+ if (field.key in value) {
130
+ field.set(value[field.key], opts);
131
+ }
132
+ });
133
+ },
134
+ // Resets all fields back to their original values
135
+ reset() {
136
+ getFields(this).forEach((f) => f.reset());
137
+ },
138
+ // Saves all current values into _originalValue
139
+ save() {
140
+ getFields(this).forEach((f) => f.save());
141
+ },
142
+ // Create a result that is only populated with changed keys
143
+ get changedValue() {
144
+ const result = {};
145
+ getFields(this).forEach((f) => {
146
+ if (f.dirty ||
147
+ // If the caller used useFormState.ifUndefined to provide some default values, then those keys may not
148
+ // look dirty, but if we're new we should include them anyway.
149
+ (this.isNewEntity &&
150
+ // Unless they're undefined anyway
151
+ f.value !== undefined &&
152
+ // And unless they're empty sub-objects
153
+ !(f.value instanceof Object && Object.entries(f.changedValue).length === 0))) {
154
+ result[f.key] = f.changedValue;
155
+ }
156
+ });
157
+ // Ensure we always have the id for updates to work
158
+ const idField = getFields(this).find((f) => f._isIdKey);
159
+ if (idField && idField.value !== undefined) {
160
+ result[idField.key] = idField.value;
161
+ }
162
+ return result;
163
+ },
164
+ get originalValue() {
165
+ return instance;
166
+ },
167
+ // An internal helper method to see if `other` is for "the same entity" as our current row
168
+ isSameEntity(other) {
169
+ const idField = getFields(this).find((f) => f._isIdKey);
170
+ if (!idField) {
171
+ return false;
172
+ }
173
+ return this[idField.key].value === other[idField.key];
174
+ },
175
+ };
176
+ proxy = mobx_1.makeAutoObservable(obj, {
177
+ // Use custom equality on `value` that is _never_ equals. This sounds weird, but
178
+ // because our `value` is always the same `instance` that was passed to `newObjectState`,
179
+ // to mobx this looks like the value never changes, and it will never invoke observers
180
+ // even with our tick-based hacks.
181
+ value: mobx_1.computed({ equals: () => false }),
182
+ });
183
+ // Any time a field changes, percolate that change up to us
184
+ mobx_1.reaction(() => getFields(proxy).map((f) => f.value), () => _tick.value++);
185
+ return proxy;
186
+ }
187
+ exports.newObjectState = newObjectState;
@@ -0,0 +1,59 @@
1
+ import { ObjectState } from "./objectField";
2
+ import { Rule } from "../rules";
3
+ /**
4
+ * Form state for a primitive field in the form, i.e. its value but also touched/validation/etc. state.
5
+ *
6
+ * This API also provides hooks for form elements to call into, i.e. `blur()` and `set(...)` that will
7
+ * update the field state and re-render, i.e. when including in an `ObjectState`-typed literal that is
8
+ * an mobx `useLocalObservable`/observable.
9
+ *
10
+ * Note that `V` will always have `null | undefined` added to it by `FieldStates`, b/c most form fields
11
+ * i.e. text boxes, can always be cleared out/deleted.
12
+ */
13
+ export interface FieldState<T, V> {
14
+ readonly key: string;
15
+ value: V;
16
+ readonly originalValue: V;
17
+ touched: boolean;
18
+ readOnly: boolean;
19
+ loading: boolean;
20
+ readonly required: boolean;
21
+ readonly dirty: boolean;
22
+ readonly valid: boolean;
23
+ readonly isNewEntity: boolean;
24
+ rules: Rule<T, V>[];
25
+ readonly errors: string[];
26
+ /** Returns a subset of V with only the changed values. Currently not observable. */
27
+ readonly changedValue: V;
28
+ /** Focuses the field. Disables changes from `ObjectState.set` calls. */
29
+ focus(): void;
30
+ /** Blur marks the field as touched and triggers an auto-save. */
31
+ blur(): void;
32
+ /** Triggers an auto-save; the caller (i.e. useFormState) should still dirty & valid check. */
33
+ maybeAutoSave(): void;
34
+ set(value: V, opts?: SetOpts): void;
35
+ /** Reverts back to the original value and resets dirty/touched. */
36
+ reset(): void;
37
+ /** Accepts the current changed value (if any) as the original and resets dirty/touched. */
38
+ save(): void;
39
+ }
40
+ /** Public options for our `set` command. */
41
+ export interface SetOpts {
42
+ /** Whether this `set` should trigger an auto-save; defaults to true. */
43
+ autoSave?: boolean;
44
+ }
45
+ /** Internal `.set` opts for conditionally that form-state internally percolates. */
46
+ export interface InternalSetOpts extends SetOpts {
47
+ /** `resetting` is the code calling `.reset()` to revert to original values. */
48
+ resetting?: boolean;
49
+ /** `refreshing` is when `useFormState` sees a new value. */
50
+ refreshing?: boolean;
51
+ }
52
+ export interface FieldStateInternal<T, V> extends FieldState<T, V> {
53
+ set(value: V, opts?: InternalSetOpts): void;
54
+ _isIdKey: boolean;
55
+ _isDeleteKey: boolean;
56
+ _isReadOnlyKey: boolean;
57
+ _focused: boolean;
58
+ }
59
+ export declare function newValueFieldState<T, K extends keyof T>(parentInstance: T, parentState: () => ObjectState<T>, key: K, rules: Rule<T, T[K] | null | undefined>[], isIdKey: boolean, isDeleteKey: boolean, isReadOnlyKey: boolean, computed: boolean, readOnly: boolean, maybeAutoSave: () => void): FieldState<T, T[K] | null | undefined>;