@homebound/form-state 2.33.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.
- package/dist/index.d.ts +437 -9
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1109 -14
- package/dist/index.js.map +1 -0
- package/package.json +32 -35
- package/dist/FormStateApp.d.ts +0 -5
- package/dist/FormStateApp.js +0 -54
- package/dist/FormStateApp.test.d.ts +0 -1
- package/dist/FormStateApp.test.js +0 -26
- package/dist/config.d.ts +0 -78
- package/dist/config.js +0 -2
- package/dist/configBuilders.d.ts +0 -67
- package/dist/configBuilders.js +0 -115
- package/dist/configBuilders.test.d.ts +0 -1
- package/dist/configBuilders.test.js +0 -65
- package/dist/fields/deepEquals.d.ts +0 -7
- package/dist/fields/deepEquals.js +0 -53
- package/dist/fields/fragmentField.d.ts +0 -4
- package/dist/fields/fragmentField.js +0 -58
- package/dist/fields/listField.d.ts +0 -11
- package/dist/fields/listField.js +0 -355
- package/dist/fields/objectField.d.ts +0 -63
- package/dist/fields/objectField.js +0 -277
- package/dist/fields/valueField.d.ts +0 -81
- package/dist/fields/valueField.js +0 -244
- package/dist/formState.stories.d.ts +0 -4
- package/dist/formState.stories.js +0 -12
- package/dist/formState.test.d.ts +0 -11
- package/dist/formState.test.js +0 -2109
- package/dist/formStateDomain.d.ts +0 -47
- package/dist/formStateDomain.js +0 -28
- package/dist/mobx-behavior.test.d.ts +0 -1
- package/dist/mobx-behavior.test.js +0 -51
- package/dist/proxies.d.ts +0 -5
- package/dist/proxies.js +0 -20
- package/dist/rules.d.ts +0 -10
- package/dist/rules.js +0 -11
- package/dist/setupTests.d.ts +0 -1
- package/dist/setupTests.js +0 -15
- package/dist/stackoverlfow.d.ts +0 -17
- package/dist/stackoverlfow.js +0 -17
- package/dist/temporal.d.ts +0 -6
- package/dist/temporal.js +0 -34
- package/dist/temporal.test.d.ts +0 -1
- package/dist/temporal.test.js +0 -97
- package/dist/useFormState.d.ts +0 -82
- package/dist/useFormState.js +0 -158
- package/dist/useFormState.test.d.ts +0 -1
- package/dist/useFormState.test.js +0 -787
- package/dist/useFormStates.d.ts +0 -68
- package/dist/useFormStates.js +0 -101
- package/dist/useFormStates.test.d.ts +0 -1
- package/dist/useFormStates.test.js +0 -223
- package/dist/utils.d.ts +0 -52
- package/dist/utils.js +0 -198
- package/dist/utils.test.d.ts +0 -1
- 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,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
|
-
}
|
package/dist/formState.test.d.ts
DELETED
|
@@ -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
|
-
}
|