@homebound/form-state 2.32.0 → 3.0.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.
Files changed (57) hide show
  1. package/dist/index.d.ts +437 -9
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +1109 -14
  4. package/dist/index.js.map +1 -0
  5. package/package.json +40 -43
  6. package/dist/FormStateApp.d.ts +0 -5
  7. package/dist/FormStateApp.js +0 -54
  8. package/dist/FormStateApp.test.d.ts +0 -1
  9. package/dist/FormStateApp.test.js +0 -26
  10. package/dist/config.d.ts +0 -78
  11. package/dist/config.js +0 -2
  12. package/dist/configBuilders.d.ts +0 -67
  13. package/dist/configBuilders.js +0 -115
  14. package/dist/configBuilders.test.d.ts +0 -1
  15. package/dist/configBuilders.test.js +0 -65
  16. package/dist/fields/deepEquals.d.ts +0 -7
  17. package/dist/fields/deepEquals.js +0 -53
  18. package/dist/fields/fragmentField.d.ts +0 -4
  19. package/dist/fields/fragmentField.js +0 -58
  20. package/dist/fields/listField.d.ts +0 -11
  21. package/dist/fields/listField.js +0 -355
  22. package/dist/fields/objectField.d.ts +0 -63
  23. package/dist/fields/objectField.js +0 -277
  24. package/dist/fields/valueField.d.ts +0 -81
  25. package/dist/fields/valueField.js +0 -244
  26. package/dist/formState.stories.d.ts +0 -4
  27. package/dist/formState.stories.js +0 -12
  28. package/dist/formState.test.d.ts +0 -11
  29. package/dist/formState.test.js +0 -2109
  30. package/dist/formStateDomain.d.ts +0 -47
  31. package/dist/formStateDomain.js +0 -28
  32. package/dist/mobx-behavior.test.d.ts +0 -1
  33. package/dist/mobx-behavior.test.js +0 -51
  34. package/dist/proxies.d.ts +0 -5
  35. package/dist/proxies.js +0 -20
  36. package/dist/rules.d.ts +0 -10
  37. package/dist/rules.js +0 -11
  38. package/dist/setupTests.d.ts +0 -1
  39. package/dist/setupTests.js +0 -13
  40. package/dist/stackoverlfow.d.ts +0 -17
  41. package/dist/stackoverlfow.js +0 -17
  42. package/dist/temporal.d.ts +0 -6
  43. package/dist/temporal.js +0 -34
  44. package/dist/temporal.test.d.ts +0 -1
  45. package/dist/temporal.test.js +0 -97
  46. package/dist/useFormState.d.ts +0 -82
  47. package/dist/useFormState.js +0 -158
  48. package/dist/useFormState.test.d.ts +0 -1
  49. package/dist/useFormState.test.js +0 -787
  50. package/dist/useFormStates.d.ts +0 -68
  51. package/dist/useFormStates.js +0 -101
  52. package/dist/useFormStates.test.d.ts +0 -1
  53. package/dist/useFormStates.test.js +0 -223
  54. package/dist/utils.d.ts +0 -52
  55. package/dist/utils.js +0 -198
  56. package/dist/utils.test.d.ts +0 -1
  57. package/dist/utils.test.js +0 -102
@@ -1,277 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.fragment = fragment;
4
- exports.createObjectState = createObjectState;
5
- exports.newObjectState = newObjectState;
6
- const mobx_1 = require("mobx");
7
- const fragmentField_1 = require("./fragmentField");
8
- const listField_1 = require("./listField");
9
- const valueField_1 = require("./valueField");
10
- const utils_1 = require("../utils");
11
- const fragmentSym = Symbol("fragment");
12
- function fragment(value) {
13
- return value;
14
- }
15
- /**
16
- * Creates a new `ObjectState` for a given form object `T` given config rules in `config`.
17
- *
18
- * The returned `ObjectState` can be used in a mobx `useLocalObservable` to drive an
19
- * interactive form that tracks the current valid/touched/etc. state of both each
20
- * individual fields as well as the top-level form/object itself.
21
- */
22
- function createObjectState(config, instance, opts = {}) {
23
- const noop = () => { };
24
- return newObjectState({ type: "object", config }, undefined, undefined, undefined, instance, undefined, opts.maybeAutoSave || noop, false);
25
- }
26
- /** A more internal version of `createObjectState`. */
27
- function newObjectState(config, parentState, parentInstance, parentListState, instance, key, maybeAutoSave, deepExhaustive) {
28
- // This is what we return, but we only know it's value until we call `observable`, so
29
- // we create a mutable variable to capture it so that we can create fields/call their
30
- // constructors and give them a way to access it later.
31
- let proxy = undefined;
32
- function getObjectState() {
33
- if (!proxy) {
34
- throw new Error("Race condition");
35
- }
36
- return proxy;
37
- }
38
- // We directly mutate `instance` as the user edits the form, so keep a deep copy of the POJO.
39
- // If the user passed us `instance` that was a mobx store/class, this originalValue won't really
40
- // be a true match (i.e. pass instanceof), but it should be good enough
41
- const originalCopy = (0, utils_1.deepClone)(instance);
42
- const objectConfig = config.config;
43
- const fieldStates = Object.entries(objectConfig).map(([_key, _config]) => {
44
- var _a, _b, _c, _d;
45
- const key = _key;
46
- const config = _config;
47
- let field;
48
- if (config.type === "value") {
49
- field = (0, valueField_1.newValueFieldState)(originalCopy, instance, getObjectState, key, config.rules || [], config.isIdKey ||
50
- // Default the id key to "id" unless some other field has isIdKey set
51
- (key === "id" &&
52
- !("isIdKey" in config) && // Allow setting `isIdKey=false`
53
- !Object.entries(objectConfig).some(([other, c]) => other !== key && c.isIdKey)), config.isDeleteKey || false, config.isReadOnlyKey || false, config.isLocalOnly || false, (_a = config.computed) !== null && _a !== void 0 ? _a :
54
- // If instance is a mobx class, we can detect computeds as they won't be enumerable
55
- ((0, mobx_1.isObservable)(instance) ? !(key in originalCopy) : false), (_b = config.readOnly) !== null && _b !== void 0 ? _b : false, (_c = config.strictOrder) !== null && _c !== void 0 ? _c : true, maybeAutoSave);
56
- }
57
- else if (config.type === "list") {
58
- field = (0, listField_1.newListFieldState)(originalCopy, instance, getObjectState, key, config.rules || [], config, { type: "object", config: config.config }, (_d = config.strictOrder) !== null && _d !== void 0 ? _d : true, maybeAutoSave, deepExhaustive);
59
- }
60
- else if (config.type === "object") {
61
- // Because our objectField will fundamentally want to do `child.firstName.set(...)` or
62
- // even `child.firstName !== undefined`, etc., we "simplify" things by always setting
63
- // an empty object, for our child valueFields/listFields to use/read from, although
64
- // we then have to "oh right ignore {}" in places like `dirty`.
65
- if (!instance[key]) {
66
- instance[key] = {};
67
- }
68
- field = newObjectState(config, getObjectState, instance, undefined, instance[key], key, maybeAutoSave, deepExhaustive);
69
- }
70
- else if (config.type === "fragment") {
71
- field = (0, fragmentField_1.newFragmentField)(instance, key);
72
- }
73
- else {
74
- throw new Error(`Invalid type value ${config.type}`);
75
- }
76
- return [key, field];
77
- });
78
- // We always return the same `instance` field from our `value` method, but
79
- // we want to pretend that it's observable, so use a tick to force it.
80
- const _tick = (0, mobx_1.observable)({ value: 1 });
81
- const fieldNames = Object.keys(objectConfig);
82
- function getFields(proxyThis) {
83
- return fieldNames.map((name) => proxyThis[name]);
84
- }
85
- const obj = {
86
- ...Object.fromEntries(fieldStates),
87
- key,
88
- get value() {
89
- _tick.value > 0 || (0, utils_1.fail)();
90
- return instance;
91
- },
92
- set value(value) {
93
- this.set(value);
94
- },
95
- // private
96
- _kind: "object",
97
- _readOnly: false,
98
- _loading: false,
99
- _isAutoSaving: false,
100
- _considerDeleted() {
101
- const deleteField = getFields(this).find((f) => f._isDeleteKey);
102
- return !!(deleteField === null || deleteField === void 0 ? void 0 : deleteField.value);
103
- },
104
- _considerReadOnly() {
105
- const readOnlyField = getFields(this).find((f) => f._isReadOnlyKey);
106
- return !!(readOnlyField === null || readOnlyField === void 0 ? void 0 : readOnlyField.value);
107
- },
108
- get focused() {
109
- return getFields(this).some((f) => f.focused);
110
- },
111
- get touched() {
112
- return getFields(this).some((f) => f.touched);
113
- },
114
- set touched(touched) {
115
- getFields(this).forEach((f) => (f.touched = touched));
116
- },
117
- get readOnly() {
118
- return (this._readOnly ||
119
- this._considerReadOnly() ||
120
- (parentState && parentState().readOnly) ||
121
- !!(parentListState === null || parentListState === void 0 ? void 0 : parentListState.readOnly));
122
- },
123
- set readOnly(readOnly) {
124
- this._readOnly = readOnly;
125
- },
126
- get loading() {
127
- return this._loading || (parentState && parentState().loading) || !!(parentListState === null || parentListState === void 0 ? void 0 : parentListState.loading);
128
- },
129
- set loading(loading) {
130
- this._loading = loading;
131
- },
132
- get valid() {
133
- return getFields(this).every((f) => {
134
- return f.valid;
135
- });
136
- },
137
- get errors() {
138
- return getFields(this).flatMap((f) => f.errors.map((e) => `${f.key}: ${e}`));
139
- },
140
- get dirty() {
141
- return (getFields(this).some((f) => f.dirty && !f._isLocalOnly) ||
142
- // `isUnset` checks if our `parent[key] === undefined`, which can mean "surely we're dirty",
143
- // but as long as we've got some keys actually set
144
- (this.isUnset() && !(0, utils_1.areEqual)(this.value, {})));
145
- },
146
- get isNewEntity() {
147
- const idField = getFields(this).find((f) => f._isIdKey);
148
- // If we're a line item w/o an immediate id field, look in our parent
149
- if (!idField && parentState) {
150
- return parentState().isNewEntity;
151
- }
152
- // If there is no id field, assume we're not new
153
- return idField !== undefined && (idField.value === null || idField.value === undefined);
154
- },
155
- canSave() {
156
- this.touched = true;
157
- return this.valid;
158
- },
159
- // Accepts new values in bulk, i.e. when setting the form initial state from the backend.
160
- set(value, opts = {}) {
161
- if (this.readOnly && !opts.resetting && !opts.refreshing) {
162
- throw new Error(`${String(key) || "formState"} is currently readOnly`);
163
- }
164
- // Restore our instance if we're being reset
165
- if (value && this.isUnset())
166
- parentInstance[key] = instance;
167
- // Delete our instance if we're being unset
168
- if (!value && parentInstance)
169
- parentInstance[key] = undefined;
170
- // Otherwise just copy over the fields
171
- getFields(this).forEach((field) => {
172
- if (value && typeof value === "object" && field.key in value) {
173
- field.set(value[field.key], opts);
174
- }
175
- });
176
- },
177
- // Resets all fields back to their original values
178
- revertChanges() {
179
- getFields(this).forEach((f) => f.revertChanges());
180
- },
181
- // Saves all current values into _originalValue.
182
- // Deprecated: use formState.update(ackedServerValue) so saved changes are audited field-by-field.
183
- commitChanges() {
184
- if (this._isAutoSaving) {
185
- // `commitChanges` will mark all WIP changes as committed, which might drop changes if there
186
- // is an auto-save in-flight, and the user made more changes, that are waiting for their turn
187
- // on the next auto-save request.
188
- //
189
- // Instead of doing a form-wide "all changes are committed", autosave forms should use
190
- // formState.update(ackedServerValue), or init.map/input, to apply the server's latest results
191
- // within the form. That selectively "un-dirty"/commits the just-saved data, but keeps the
192
- // "still-dirty" WIP data alone.
193
- throw new Error("When using autoSave, you should not manually call commitChanges, instead have init.map/input update the form state");
194
- }
195
- // asdf
196
- getFields(this).forEach((f) => f.commitChanges());
197
- },
198
- // Create a result that is only populated with changed keys
199
- get changedValue() {
200
- const result = {};
201
- // If we're a child, like `author.address` that was unset, return null
202
- if (this.isUnset()) {
203
- // If we're a reference, always return `{ id: ... }` for easy binding
204
- const idField = getFields(this).find((f) => f._isIdKey);
205
- if (idField && config.reference)
206
- return { id: null };
207
- // Otherwise mark the whole child as gone
208
- return null;
209
- }
210
- getFields(this).forEach((f) => {
211
- var _a;
212
- if (f._computed)
213
- return;
214
- // References only include the id key below
215
- if (config.reference)
216
- return;
217
- // Ignore local-only fields
218
- if (f._isLocalOnly)
219
- return;
220
- if (f.dirty ||
221
- // If the caller used useFormState.ifUndefined to provide some default values, then those keys may not
222
- // look dirty, but if we're new we should include them anyway.
223
- (this.isNewEntity && f._kind === "value" && f.value !== undefined) ||
224
- // ...or they're non-empty sub-objects
225
- (this.isNewEntity &&
226
- f._kind === "object" &&
227
- // Child objects that are unset will have `changedValue = null`
228
- f.changedValue &&
229
- Object.entries(f.changedValue).length > 0) ||
230
- // ...or they're non-empty sub-lists
231
- (this.isNewEntity && f._kind === "list" && ((_a = f.value) === null || _a === void 0 ? void 0 : _a.length) > 0) ||
232
- // ...or the parent list is deep-exhaustive
233
- deepExhaustive) {
234
- result[f.key] = f.changedValue;
235
- }
236
- });
237
- // Ensure we always have the id for updates to work
238
- const idField = getFields(this).find((f) => f._isIdKey);
239
- if (idField && idField.value !== undefined) {
240
- result[idField.key] = idField.value;
241
- }
242
- return result;
243
- },
244
- get originalValue() {
245
- getFields(proxy).map((f) => f.originalValue);
246
- return originalCopy;
247
- },
248
- // An internal helper method to see if `other` is for "the same entity" as our current row
249
- isSameEntity(other) {
250
- const idField = getFields(this).find((f) => f._isIdKey);
251
- if (!idField)
252
- return false;
253
- const ourIdValue = idField.value;
254
- const otherIdValue = other[idField.key];
255
- // If the otherIdValue is undefined, it's a new entity so can't be the same as us
256
- return otherIdValue !== undefined && (0, utils_1.areEqual)(otherIdValue, ourIdValue);
257
- },
258
- isUnset() {
259
- return !!parentInstance && parentInstance[key] === undefined;
260
- },
261
- get idKey() {
262
- var _a;
263
- return (_a = getFields(this).find((f) => f._isIdKey)) === null || _a === void 0 ? void 0 : _a.key;
264
- },
265
- };
266
- proxy = (0, mobx_1.makeAutoObservable)(obj, {
267
- // Use custom equality on `value` that is _never_ equals. This sounds weird, but
268
- // because our `value` is always the same `instance` that was passed to `newObjectState`,
269
- // to mobx this looks like the value never changes, and it will never invoke observers
270
- // even with our tick-based hacks.
271
- value: (0, mobx_1.computed)({ equals: () => false }),
272
- originalValue: (0, mobx_1.computed)({ equals: () => false }),
273
- });
274
- // Any time a field changes, percolate that change up to us
275
- (0, mobx_1.reaction)(() => getFields(proxy).map((f) => f.value), () => _tick.value++);
276
- return proxy;
277
- }
@@ -1,81 +0,0 @@
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<V> {
14
- /** The key in the parent object, i.e. `firstName` in `author: { firstName: string }`. */
15
- readonly key: string;
16
- /** The current value of the field. */
17
- value: V;
18
- readonly originalValue: V;
19
- touched: boolean;
20
- readOnly: boolean;
21
- loading: boolean;
22
- readonly required: boolean;
23
- readonly dirty: boolean;
24
- readonly valid: boolean;
25
- readonly focused: boolean;
26
- readonly isNewEntity: boolean;
27
- rules: Rule<V>[];
28
- readonly errors: string[];
29
- /** Returns a subset of V with only the changed values. Currently not observable. */
30
- readonly changedValue: V;
31
- /** Focuses the field. Disables changes from `ObjectState.set` calls. */
32
- focus(): void;
33
- /** Blur marks the field as touched and triggers an auto-save. */
34
- blur(): void;
35
- /** Triggers an auto-save; the caller (i.e. useFormState) should still dirty & valid check. */
36
- maybeAutoSave(): void;
37
- set(value: V, opts?: SetOpts): void;
38
- /** Reverts back to the original value and resets dirty/touched. */
39
- revertChanges(): void;
40
- /**
41
- * Accepts the current changed value (if any) as the original and resets dirty/touched.
42
- *
43
- * @deprecated Use `formState.update(ackedServerValue)` instead. `commitChanges` marks all current
44
- * changes as saved, which risks discarding a user's in-flight edits. Updating from the server ack gives
45
- * form-state a field-by-field audit of which changes were successfully saved.
46
- */
47
- commitChanges(): void;
48
- /** Creates a new FieldState with a transformation of the value, i.e. string to int, or feet to inches. */
49
- adapt<V2>(adapter: ValueAdapter<V, V2>): FieldState<V2>;
50
- }
51
- /**
52
- * Allows changing a type in the formState (like a string) to a different type in the UI (like a number).
53
- *
54
- * Or doing unit of measure conversions within the same type, like from meters to feet.
55
- */
56
- export interface ValueAdapter<V, V2 = V> {
57
- /** Converts the original FieldState's value `V` into new `V2` type. */
58
- toValue(value: V): V2;
59
- /** Converts the adapted FieldState's value `V2` back into the original `V` type. */
60
- fromValue(value: V2): V;
61
- }
62
- /** Public options for our `set` command. */
63
- export interface SetOpts {
64
- /** Whether this `set` should trigger an auto-save; defaults to true. */
65
- autoSave?: boolean;
66
- }
67
- /** Internal `.set` opts for conditionally that form-state internally percolates. */
68
- export interface InternalSetOpts extends SetOpts {
69
- /** `resetting` is the code calling `.reset()` to revert to original values. */
70
- resetting?: boolean;
71
- /** `refreshing` is when `useFormState` sees a new value. */
72
- refreshing?: boolean;
73
- }
74
- export interface FieldStateInternal<T, V> extends FieldState<V> {
75
- set(value: V, opts?: InternalSetOpts): void;
76
- _isIdKey: boolean;
77
- _isDeleteKey: boolean;
78
- _isReadOnlyKey: boolean;
79
- _isLocalOnly: boolean;
80
- }
81
- export declare function newValueFieldState<T, K extends keyof T>(parentCopy: T, parentInstance: T, parentState: () => ObjectState<T>, key: K, rules: Rule<T[K] | null | undefined>[], isIdKey: boolean, isDeleteKey: boolean, isReadOnlyKey: boolean, isLocalOnly: boolean, computed: boolean, readOnly: boolean, strictOrder: boolean, maybeAutoSave: () => void): FieldState<T[K] | null | undefined>;
@@ -1,244 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.newValueFieldState = newValueFieldState;
4
- const is_plain_object_1 = require("is-plain-object");
5
- const mobx_1 = require("mobx");
6
- const proxies_1 = require("../proxies");
7
- const rules_1 = require("../rules");
8
- const utils_1 = require("../utils");
9
- function newValueFieldState(parentCopy, parentInstance, parentState, key, rules, isIdKey, isDeleteKey, isReadOnlyKey, isLocalOnly, computed, readOnly, strictOrder, maybeAutoSave) {
10
- // Because we read/write the value directly back into parentInstance[key],
11
- // which itself is not a proxy, we use this as our "value changed" trigger.
12
- const _tick = (0, mobx_1.observable)({ value: 1 });
13
- const _originalValueTick = (0, mobx_1.observable)({ value: 1 });
14
- // Track "this is probably what we put into a mutation to the server" to allow
15
- // use to better accept server acks/responses that change our initial submission.
16
- //
17
- // I.e. we submit `firstName: bob` and the server acks `firstName: Bob`, we should
18
- // accept it if we can tell we haven't changed `bob` since our initial submission.
19
- let hasInflightChangedValue = false;
20
- let lastChangedValue = undefined;
21
- const field = {
22
- key: key,
23
- touched: false,
24
- /** Current readOnly value. */
25
- _readOnly: readOnly || false,
26
- _loading: false,
27
- _kind: "value",
28
- // Expose so computed can be skipped in changedValue
29
- _computed: computed,
30
- _focused: false,
31
- _isIdKey: isIdKey,
32
- _isDeleteKey: isDeleteKey,
33
- _isReadOnlyKey: isReadOnlyKey,
34
- _isLocalOnly: isLocalOnly,
35
- rules,
36
- get value() {
37
- // If we're wrapping a mobx store, then we'll get reactivity from parentInstance[key]
38
- const value = _tick.value > 0 ? parentInstance[key] : (0, utils_1.fail)();
39
- // Re-create the `keepNull` logic on sets but for our initial read where our
40
- // originalValue is null (empty) but we want to expose it as undefined for
41
- // consistency of "empty-ness" to our UI components.
42
- return value === null && (0, utils_1.isEmpty)(parentCopy[key]) ? undefined : value;
43
- },
44
- set value(v) {
45
- this.set(v);
46
- },
47
- get focused() {
48
- return this._focused;
49
- },
50
- get dirty() {
51
- return !(0, utils_1.areEqual)(this.originalValue, this.value, strictOrder);
52
- },
53
- /** Returns whether this field is readOnly, although if our parent is readOnly then it trumps. */
54
- get readOnly() {
55
- return parentState().readOnly || this._readOnly;
56
- },
57
- /** Sets the field readOnly, but `loading` will still be or-d with the parent's readOnly state. */
58
- set readOnly(readOnly) {
59
- this._readOnly = readOnly;
60
- },
61
- /** Returns whether this field is loading, or if our parent is loading. */
62
- get loading() {
63
- return parentState().loading || this._loading;
64
- },
65
- /** Sets the field loading, but `loading` will still be or-d with the parent's loading state. */
66
- set loading(loading) {
67
- this._loading = loading;
68
- },
69
- // For primitive fields, the changed value is just the value.
70
- get changedValue() {
71
- // Add some hints to let `set(..., { refreshing: true })` accept server-driven changes to our value,
72
- // if the user hasn't changed it yet-again while the changedValue was in in-flight.
73
- hasInflightChangedValue = true;
74
- lastChangedValue = this.value;
75
- // Usually if we see a field being unset, we set `parent[key] = null`, but if we're wrapping
76
- // a mobx observable object, it might have changed to `undefined` without us being able to
77
- // tell it to be `null` (and potentially break the type contract of `string | undefined`).
78
- // So detect un-set-ness here and return `null`.
79
- if (this.value === undefined && this.originalValue !== undefined)
80
- return null;
81
- return this.value;
82
- },
83
- get valid() {
84
- const opts = { value: this.value, key: key, originalValue: this.originalValue, object: parentState() };
85
- return this.rules.every((r) => r(opts) === undefined);
86
- },
87
- get errors() {
88
- const opts = { value: this.value, key: key, originalValue: this.originalValue, object: parentState() };
89
- return this.rules.map((r) => r(opts)).filter(utils_1.isNotUndefined);
90
- },
91
- get required() {
92
- return this.rules.some((rule) => rule === rules_1.required);
93
- },
94
- get isNewEntity() {
95
- return parentState().isNewEntity;
96
- },
97
- focus() {
98
- this._focused = true;
99
- },
100
- blur() {
101
- this.maybeAutoSave();
102
- this._focused = false;
103
- },
104
- maybeAutoSave() {
105
- // Now that the user is done editing the field (note maybe w/o blurring, i.e. if they hit enter), we sneak in some trim logic
106
- this.maybeTrim();
107
- // touched is readonly, but we're allowed to change it
108
- this.touched = true;
109
- maybeAutoSave();
110
- },
111
- set(value, opts = {}) {
112
- if (this.readOnly && !opts.resetting && !opts.refreshing) {
113
- throw new Error(`${String(key)} is currently readOnly`);
114
- }
115
- if (opts.refreshing && this.dirty) {
116
- // See if we should ignore the incoming server-side value, to avoid dropping local WIP changes
117
- const isAckingUnset = this.value === null && (value === null || value === undefined);
118
- const acceptServerAck = hasInflightChangedValue &&
119
- this.value === lastChangedValue &&
120
- // If value === originalValue, this is likely a cache refresh firing in-between "we put the new value
121
- // on the wire" and "the server acked our change". Granted, this heuristic means that if the server
122
- // really does reject/rollback our change, we'll ignore it, but atm we don't have a way of differentiating
123
- // "this refresh is from a pre-response cache refresh" vs. "post-response cache refresh".
124
- value !== this.originalValue;
125
- // Ignore incoming values if we have changes (this.dirty) unless:
126
- // - our latest change (this.value) matches the incoming value (value), i.e. the server
127
- // is exactly acking our change, or
128
- // - the user hasn't made any changes since `.changedValue` was put on the wire, i.e. the
129
- // server received our value, but wanted to change it.
130
- // Compare by content (not `!==`) so array/list-of-primitive values are treated as acked when
131
- // the server returns the same contents in a new array instance (otherwise the field would
132
- // stay dirty forever, since reference equality never holds for a fresh array).
133
- const keepLocalWipChange = !(0, utils_1.areEqual)(this.value, value, strictOrder) && !isAckingUnset && !acceptServerAck;
134
- if (keepLocalWipChange)
135
- return;
136
- }
137
- else if (computed && (opts.resetting || opts.refreshing)) {
138
- // Computeds can't be either reset or refreshed
139
- return;
140
- }
141
- // If the user has deleted/emptied a value that was originally set, keep it as `null`
142
- // so that our partial update to the backend correctly unsets it.
143
- const keepNull = !(0, utils_1.isEmpty)(this.originalValue) && (0, utils_1.isEmpty)(value) && !opts.refreshing;
144
- // If a list of primitives was originally undefined, coerce `[]` to `undefined`
145
- const coerceEmptyList = value && value instanceof Array && value.length === 0 && (0, utils_1.isEmpty)(this.originalValue);
146
- const newValue = keepNull ? null : (0, utils_1.isEmpty)(value) || coerceEmptyList ? undefined : value;
147
- hasInflightChangedValue = false;
148
- // Set the value on our parent object
149
- const changed = !(0, utils_1.areEqual)(newValue, this.value, strictOrder);
150
- parentInstance[key] = newValue;
151
- _tick.value++;
152
- if (opts.refreshing) {
153
- this.originalValue = newValue;
154
- }
155
- // If we're being set programmatically, i.e. we don't currently have focus,
156
- // call blur to trigger any auto-saves.
157
- if (!this._focused && !opts.refreshing && !opts.resetting && this.dirty && changed && opts.autoSave !== false) {
158
- this.maybeAutoSave();
159
- }
160
- },
161
- adapt(adapter) {
162
- return adapt(this, adapter);
163
- },
164
- revertChanges() {
165
- if (!computed) {
166
- this.set(this.originalValue, { resetting: true });
167
- }
168
- this.touched = false;
169
- },
170
- commitChanges() {
171
- if ((0, is_plain_object_1.isPlainObject)(this.originalValue)) {
172
- this.originalValue = (0, mobx_1.toJS)(this.value);
173
- }
174
- else {
175
- this.originalValue = this.value;
176
- }
177
- this.touched = false;
178
- },
179
- get originalValue() {
180
- // A dummy check to for reactivity around our non-proxy value
181
- const value = _originalValueTick.value > -1 ? parentCopy[key] : parentCopy[key];
182
- // Re-create the `keepNull` logic so that `.value` === `.originalValue`
183
- return value === null ? undefined : value;
184
- },
185
- set originalValue(v) {
186
- const canSkip = v === undefined && !(key in parentCopy);
187
- if (!canSkip)
188
- parentCopy[key] = v;
189
- _originalValueTick.value++;
190
- },
191
- maybeTrim() {
192
- if (typeof this.value === "string") {
193
- let newValue = this.value.trim();
194
- if (newValue === "") {
195
- newValue = undefined;
196
- }
197
- if (newValue !== this.value) {
198
- this.set(newValue);
199
- }
200
- }
201
- },
202
- };
203
- // If we're wrapping a mobx observer, watching for external mutations, i.e. from callers not
204
- // going through our FieldState.set/FieldState.value setters.
205
- if ((0, mobx_1.isObservable)(parentInstance)) {
206
- (0, mobx_1.reaction)(() => parentInstance[key], () => {
207
- // Don't auto-save on maybe-external mutation if our field, or another other field (potentially a computed),
208
- // is currently focused, because then it's probably just us doing the writes through `Bound...Field` components.
209
- if (!parentState().focused)
210
- maybeAutoSave();
211
- });
212
- }
213
- return field;
214
- }
215
- /**
216
- * Returns a proxy that looks exactly like the original `field`, in terms of valid/touched/errors/etc., but
217
- * has any methods that use `V` overridden to use be `V2`.
218
- *
219
- * Note that `V2` can be a new type, like string -> number, or just a transformation on the same
220
- * type, i.e. feet -> inches where both are `number`s.
221
- */
222
- function adapt(field, adapter) {
223
- return (0, proxies_1.newDelegateProxy)(field, {
224
- rules: [],
225
- get value() {
226
- return adapter.toValue(field.value);
227
- },
228
- set value(v) {
229
- field.value = adapter.fromValue(v);
230
- },
231
- set: (v) => {
232
- field.value = adapter.fromValue(v);
233
- },
234
- get changedValue() {
235
- return this.value;
236
- },
237
- adapt(adapter) {
238
- return adapt(this, adapter);
239
- },
240
- get originalValue() {
241
- return adapter.toValue(field.originalValue);
242
- },
243
- });
244
- }
@@ -1,4 +0,0 @@
1
- import { Meta } from "@storybook/react";
2
- declare const _default: Meta;
3
- export default _default;
4
- export declare function AppExample(): import("react/jsx-runtime").JSX.Element;
@@ -1,12 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AppExample = AppExample;
4
- const jsx_runtime_1 = require("react/jsx-runtime");
5
- const FormStateApp_1 = require("./FormStateApp");
6
- exports.default = {
7
- component: FormStateApp_1.FormStateApp,
8
- title: "Components/Form State",
9
- };
10
- function AppExample() {
11
- return (0, jsx_runtime_1.jsx)(FormStateApp_1.FormStateApp, {});
12
- }
@@ -1,11 +0,0 @@
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
- }