@homebound/form-state 2.21.0 → 2.22.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/fields/listField.d.ts +2 -2
- package/dist/fields/listField.js +49 -24
- package/dist/fields/objectField.d.ts +1 -0
- package/dist/fields/objectField.js +16 -6
- package/dist/fields/valueField.d.ts +1 -1
- package/dist/fields/valueField.js +7 -8
- package/dist/formState.test.js +81 -14
- package/dist/useFormStates.js +3 -1
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +16 -1
- package/package.json +1 -1
|
@@ -3,9 +3,9 @@ import { ObjectState } from "./objectField";
|
|
|
3
3
|
import { FieldState } from "./valueField";
|
|
4
4
|
import { Rule } from "../rules";
|
|
5
5
|
/** Form state for list of children, i.e. `U` is a `Book` in a form with a `books: Book[]`. */
|
|
6
|
-
export interface ListFieldState<U> extends
|
|
6
|
+
export interface ListFieldState<U> extends FieldState<U[]> {
|
|
7
7
|
readonly rows: ReadonlyArray<ObjectState<U>>;
|
|
8
8
|
add(value: U, index?: number): void;
|
|
9
9
|
remove(indexOrValue: number | U): void;
|
|
10
10
|
}
|
|
11
|
-
export declare function newListFieldState<T, K extends keyof T, U>(parentInstance: T, parentState: () => ObjectState<T>, key: K, rules: Rule<readonly ObjectState<U>[]>[], listConfig: ListFieldConfig<U>, config: ObjectConfig<U>, strictOrder: boolean, maybeAutoSave: () => void): ListFieldState<U>;
|
|
11
|
+
export declare function newListFieldState<T, K extends keyof T, U>(parentCopy: T, parentInstance: T, parentState: () => ObjectState<T>, key: K, rules: Rule<readonly ObjectState<U>[]>[], listConfig: ListFieldConfig<U>, config: ObjectConfig<U>, strictOrder: boolean, maybeAutoSave: () => void): ListFieldState<U>;
|
package/dist/fields/listField.js
CHANGED
|
@@ -5,13 +5,20 @@ const mobx_1 = require("mobx");
|
|
|
5
5
|
const objectField_1 = require("./objectField");
|
|
6
6
|
const rules_1 = require("../rules");
|
|
7
7
|
const utils_1 = require("../utils");
|
|
8
|
-
function newListFieldState(parentInstance, parentState, key, rules, listConfig, config, strictOrder, maybeAutoSave) {
|
|
8
|
+
function newListFieldState(parentCopy, parentInstance, parentState, key, rules, listConfig, config, strictOrder, maybeAutoSave) {
|
|
9
|
+
var _a;
|
|
9
10
|
// Keep a map of "item in the parent list" -> "that item's ObjectState"
|
|
10
11
|
const rowMap = new Map();
|
|
11
12
|
const _tick = (0, mobx_1.observable)({ value: 1 });
|
|
13
|
+
const _originalValueTick = (0, mobx_1.observable)({ value: 1 });
|
|
12
14
|
const _childTick = (0, mobx_1.observable)({ value: 1 });
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
+
// When child rows don't have ids (i.e. for new rows that aren't saved yet), we need an id-less
|
|
16
|
+
// "clone <-> current" map to tell if we're dirty or not, i.e. whether `parentInstance[key]` has
|
|
17
|
+
// drifted from `parentCopy[key]`
|
|
18
|
+
const copyMap = new Map();
|
|
19
|
+
((_a = parentCopy[key]) !== null && _a !== void 0 ? _a : []).forEach((copy, i) => {
|
|
20
|
+
copyMap.set(copy, parentInstance[key][i]);
|
|
21
|
+
});
|
|
15
22
|
const list = {
|
|
16
23
|
key: key,
|
|
17
24
|
// Our fundamental state of wrapped Us
|
|
@@ -46,22 +53,19 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
46
53
|
return parentState().isNewEntity;
|
|
47
54
|
},
|
|
48
55
|
// private
|
|
56
|
+
/** Returns whether a row has been added/removed. */
|
|
49
57
|
hasChanged() {
|
|
50
|
-
const [current, original] = [this.value || [],
|
|
51
|
-
if (current.length !== original.length)
|
|
58
|
+
const [current, original] = [this.value || [], this.originalValue];
|
|
59
|
+
if (current.length !== original.length)
|
|
52
60
|
return true;
|
|
53
|
-
}
|
|
54
61
|
if (strictOrder) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
// With strict order, every copy[i] === original[i] must be true
|
|
63
|
+
return original.some((e, idx) => current[idx] !== copyMap.get(e));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// With loose order, we just want every copy to still have its original somewhere
|
|
67
|
+
return !original.every((e) => current.includes(copyMap.get(e)));
|
|
60
68
|
}
|
|
61
|
-
return !original.every((e) => {
|
|
62
|
-
const state = rowMap.get(e);
|
|
63
|
-
return current.some((e) => rowMap.get(e) === state);
|
|
64
|
-
});
|
|
65
69
|
},
|
|
66
70
|
// And we can derive each value's ObjectState wrapper as needed from the rowMap cache
|
|
67
71
|
get rows() {
|
|
@@ -92,7 +96,7 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
92
96
|
get valid() {
|
|
93
97
|
const value = this.rows;
|
|
94
98
|
// TODO Passing `originalCopy || []` is probably not 100% right
|
|
95
|
-
const opts = { value, key: key, originalValue:
|
|
99
|
+
const opts = { value, key: key, originalValue: this.originalValue, object: parentState() };
|
|
96
100
|
const collectionValid = this.rules.every((r) => r(opts) === undefined);
|
|
97
101
|
const entriesValid = this.rows.filter((r) => !r._considerDeleted()).every((r) => r.valid);
|
|
98
102
|
return collectionValid && entriesValid;
|
|
@@ -100,7 +104,7 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
100
104
|
get errors() {
|
|
101
105
|
if (_tick.value < 0)
|
|
102
106
|
(0, utils_1.fail)();
|
|
103
|
-
const opts = { value: this.rows, key: key, originalValue:
|
|
107
|
+
const opts = { value: this.rows, key: key, originalValue: this.originalValue, object: parentState() };
|
|
104
108
|
return this.rules.map((r) => r(opts)).filter(utils_1.isNotUndefined);
|
|
105
109
|
},
|
|
106
110
|
get changedValue() {
|
|
@@ -143,6 +147,11 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
143
147
|
// We should be passed values that are non-proxies.
|
|
144
148
|
parentInstance[key] = values.map((value) => {
|
|
145
149
|
let childState = rowMap.get(value);
|
|
150
|
+
// If we're being reverted to our originalValue, i.e. values is actually
|
|
151
|
+
// a list of copies, use the copyMap to recover our child states
|
|
152
|
+
if (!childState && copyMap.has(value)) {
|
|
153
|
+
childState = rowMap.get(copyMap.get(value));
|
|
154
|
+
}
|
|
146
155
|
if (!childState) {
|
|
147
156
|
// Look for an existing child (requires having an id key configured)
|
|
148
157
|
for (const [, otherState] of rowMap.entries()) {
|
|
@@ -159,11 +168,12 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
159
168
|
// Return the already-observable'd value so that our `parent.value[key] = values` doesn't re-proxy things
|
|
160
169
|
return childState.value;
|
|
161
170
|
});
|
|
171
|
+
// Make sure to tick first so that `setOriginalValue` sees the latest `rows`
|
|
172
|
+
_tick.value++;
|
|
162
173
|
// Reset originalCopy so that our dirty checks have the right # of rows.
|
|
163
174
|
if (opts.refreshing) {
|
|
164
|
-
|
|
175
|
+
this.setOriginalValue();
|
|
165
176
|
}
|
|
166
|
-
_tick.value++;
|
|
167
177
|
},
|
|
168
178
|
add(value, spliceIndex) {
|
|
169
179
|
// This is called by the user, so value should be a non-proxy value we should keep
|
|
@@ -189,16 +199,31 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
189
199
|
maybeAutoSave();
|
|
190
200
|
},
|
|
191
201
|
revertChanges() {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
202
|
+
this.set(this.originalValue, { resetting: true });
|
|
203
|
+
this.rows.forEach((r) => r.revertChanges());
|
|
204
|
+
this.touched = false;
|
|
196
205
|
},
|
|
197
206
|
commitChanges() {
|
|
207
|
+
// Tell each child ObjectState to have its fields commit into its parentCopy
|
|
198
208
|
this.rows.forEach((r) => r.commitChanges());
|
|
199
|
-
|
|
209
|
+
this.setOriginalValue();
|
|
210
|
+
this.touched = false;
|
|
200
211
|
_tick.value++;
|
|
201
212
|
},
|
|
213
|
+
get originalValue() {
|
|
214
|
+
// A dummy check to for reactivity around our non-proxy value
|
|
215
|
+
const value = _originalValueTick.value > -1 ? parentCopy[key] : parentCopy[key];
|
|
216
|
+
return value !== null && value !== void 0 ? value : [];
|
|
217
|
+
},
|
|
218
|
+
// This should only be called when value === originalValue
|
|
219
|
+
setOriginalValue() {
|
|
220
|
+
// Use the rows' originalValues to update the parentCopy
|
|
221
|
+
parentCopy[key] = this.rows.map((r) => r.originalValue);
|
|
222
|
+
parentCopy[key].forEach((copy, i) => {
|
|
223
|
+
copyMap.set(copy, parentInstance[key][i]);
|
|
224
|
+
});
|
|
225
|
+
_originalValueTick.value++;
|
|
226
|
+
},
|
|
202
227
|
ensureSet() {
|
|
203
228
|
if (!parentInstance[key]) {
|
|
204
229
|
parentInstance[key] = [];
|
|
@@ -36,6 +36,7 @@ export type ObjectState<T> = FieldStates<T> & FieldState<T> & {
|
|
|
36
36
|
};
|
|
37
37
|
export type ObjectStateInternal<T> = ObjectState<T> & {
|
|
38
38
|
set(value: T, opts?: InternalSetOpts): void;
|
|
39
|
+
isSameEntity(other: T): boolean;
|
|
39
40
|
};
|
|
40
41
|
declare const fragmentSym: unique symbol;
|
|
41
42
|
export type Fragment<V> = V & {
|
|
@@ -34,20 +34,26 @@ function newObjectState(config, parentState, parentListState, instance, key, may
|
|
|
34
34
|
}
|
|
35
35
|
return proxy;
|
|
36
36
|
}
|
|
37
|
+
// We directly mutate `instance` as the user edits the form, so keep a deep copy of the POJO.
|
|
38
|
+
// If the user passed us `instance` that was a mobx store/class, this originalValue won't really
|
|
39
|
+
// be a true match (i.e. pass instanceof), but it should be good enough
|
|
40
|
+
const originalCopy = (0, utils_1.deepClone)(instance);
|
|
37
41
|
const objectConfig = config;
|
|
38
42
|
const fieldStates = Object.entries(config).map(([_key, _config]) => {
|
|
39
|
-
var _a, _b;
|
|
43
|
+
var _a, _b, _c, _d;
|
|
40
44
|
const key = _key;
|
|
41
45
|
const config = _config;
|
|
42
46
|
let field;
|
|
43
47
|
if (config.type === "value") {
|
|
44
|
-
field = (0, valueField_1.newValueFieldState)(instance, getObjectState, key, config.rules || [], config.isIdKey ||
|
|
48
|
+
field = (0, valueField_1.newValueFieldState)(originalCopy, instance, getObjectState, key, config.rules || [], config.isIdKey ||
|
|
45
49
|
// Default the id key to "id" unless some other field has isIdKey set
|
|
46
50
|
(key === "id" &&
|
|
47
|
-
!Object.entries(objectConfig).some(([other, c]) => other !== key && c.isIdKey)), config.isDeleteKey || false, config.isReadOnlyKey || false,
|
|
51
|
+
!Object.entries(objectConfig).some(([other, c]) => other !== key && c.isIdKey)), config.isDeleteKey || false, config.isReadOnlyKey || false, (_a = config.computed) !== null && _a !== void 0 ? _a :
|
|
52
|
+
// If instance is a mobx class, we can detect computeds as they won't be enumerable
|
|
53
|
+
((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);
|
|
48
54
|
}
|
|
49
55
|
else if (config.type === "list") {
|
|
50
|
-
field = (0, listField_1.newListFieldState)(instance, getObjectState, key, config.rules || [], config, config.config, (
|
|
56
|
+
field = (0, listField_1.newListFieldState)(originalCopy, instance, getObjectState, key, config.rules || [], config, config.config, (_d = config.strictOrder) !== null && _d !== void 0 ? _d : true, maybeAutoSave);
|
|
51
57
|
}
|
|
52
58
|
else if (config.type === "object") {
|
|
53
59
|
if (!instance[key]) {
|
|
@@ -176,7 +182,8 @@ function newObjectState(config, parentState, parentListState, instance, key, may
|
|
|
176
182
|
return result;
|
|
177
183
|
},
|
|
178
184
|
get originalValue() {
|
|
179
|
-
|
|
185
|
+
getFields(proxy).map((f) => f.originalValue);
|
|
186
|
+
return originalCopy;
|
|
180
187
|
},
|
|
181
188
|
// An internal helper method to see if `other` is for "the same entity" as our current row
|
|
182
189
|
isSameEntity(other) {
|
|
@@ -184,7 +191,9 @@ function newObjectState(config, parentState, parentListState, instance, key, may
|
|
|
184
191
|
if (!idField) {
|
|
185
192
|
return false;
|
|
186
193
|
}
|
|
187
|
-
|
|
194
|
+
// If the otherIdValue is undefined, it's a new entity so can't be the same as us
|
|
195
|
+
const otherIdValue = other[idField.key];
|
|
196
|
+
return otherIdValue !== undefined && otherIdValue === this[idField.key].value;
|
|
188
197
|
},
|
|
189
198
|
};
|
|
190
199
|
proxy = (0, mobx_1.makeAutoObservable)(obj, {
|
|
@@ -193,6 +202,7 @@ function newObjectState(config, parentState, parentListState, instance, key, may
|
|
|
193
202
|
// to mobx this looks like the value never changes, and it will never invoke observers
|
|
194
203
|
// even with our tick-based hacks.
|
|
195
204
|
value: (0, mobx_1.computed)({ equals: () => false }),
|
|
205
|
+
originalValue: (0, mobx_1.computed)({ equals: () => false }),
|
|
196
206
|
});
|
|
197
207
|
// Any time a field changes, percolate that change up to us
|
|
198
208
|
(0, mobx_1.reaction)(() => getFields(proxy).map((f) => f.value), () => _tick.value++);
|
|
@@ -71,4 +71,4 @@ export interface FieldStateInternal<T, V> extends FieldState<V> {
|
|
|
71
71
|
_isReadOnlyKey: boolean;
|
|
72
72
|
_focused: boolean;
|
|
73
73
|
}
|
|
74
|
-
export declare function newValueFieldState<T, K extends keyof T>(parentInstance: T, parentState: () => ObjectState<T>, key: K, rules: Rule<T[K] | null | undefined>[], isIdKey: boolean, isDeleteKey: boolean, isReadOnlyKey: boolean, computed: boolean, readOnly: boolean, strictOrder: boolean, maybeAutoSave: () => void): FieldState<T[K] | null | undefined>;
|
|
74
|
+
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, computed: boolean, readOnly: boolean, strictOrder: boolean, maybeAutoSave: () => void): FieldState<T[K] | null | undefined>;
|
|
@@ -6,10 +6,7 @@ const mobx_1 = require("mobx");
|
|
|
6
6
|
const proxies_1 = require("../proxies");
|
|
7
7
|
const rules_1 = require("../rules");
|
|
8
8
|
const utils_1 = require("../utils");
|
|
9
|
-
function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, isDeleteKey, isReadOnlyKey, computed, readOnly, strictOrder, maybeAutoSave) {
|
|
10
|
-
// keep a copy here for reference equality
|
|
11
|
-
const value = parentInstance[key];
|
|
12
|
-
let _originalValue = value === null ? undefined : (0, is_plain_object_1.isPlainObject)(value) ? (0, mobx_1.toJS)(value) : value;
|
|
9
|
+
function newValueFieldState(parentCopy, parentInstance, parentState, key, rules, isIdKey, isDeleteKey, isReadOnlyKey, computed, readOnly, strictOrder, maybeAutoSave) {
|
|
13
10
|
// Because we read/write the value directly back into parentInstance[key],
|
|
14
11
|
// which itself is not a proxy, we use this as our "value changed" trigger.
|
|
15
12
|
const _tick = (0, mobx_1.observable)({ value: 1 });
|
|
@@ -31,7 +28,7 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
|
|
|
31
28
|
// Re-create the `keepNull` logic on sets but for our initial read where our
|
|
32
29
|
// originalValue is null (empty) but we want to expose it as undefined for
|
|
33
30
|
// consistency of "empty-ness" to our UI components.
|
|
34
|
-
return value === null && (0, utils_1.isEmpty)(
|
|
31
|
+
return value === null && (0, utils_1.isEmpty)(parentCopy[key]) ? undefined : value;
|
|
35
32
|
},
|
|
36
33
|
set value(v) {
|
|
37
34
|
this.set(v);
|
|
@@ -94,7 +91,7 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
|
|
|
94
91
|
if (opts.refreshing && this.dirty && this.value !== value) {
|
|
95
92
|
// Ignore incoming values if we have changes (this.dirty) unless our latest change (this.value)
|
|
96
93
|
// matches the incoming value (value), b/c if it does we should accept it and reset originalValue
|
|
97
|
-
// so that we're
|
|
94
|
+
// so that we're no longer dirty.
|
|
98
95
|
return;
|
|
99
96
|
}
|
|
100
97
|
else if (computed && (opts.resetting || opts.refreshing)) {
|
|
@@ -140,12 +137,14 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
|
|
|
140
137
|
},
|
|
141
138
|
get originalValue() {
|
|
142
139
|
// A dummy check to for reactivity around our non-proxy value
|
|
143
|
-
const value = _originalValueTick.value > -1 ?
|
|
140
|
+
const value = _originalValueTick.value > -1 ? parentCopy[key] : parentCopy[key];
|
|
144
141
|
// Re-create the `keepNull` logic so that `.value` === `.originalValue`
|
|
145
142
|
return value === null ? undefined : value;
|
|
146
143
|
},
|
|
147
144
|
set originalValue(v) {
|
|
148
|
-
|
|
145
|
+
const canSkip = v === undefined && !(key in parentCopy);
|
|
146
|
+
if (!canSkip)
|
|
147
|
+
parentCopy[key] = v;
|
|
149
148
|
_originalValueTick.value++;
|
|
150
149
|
},
|
|
151
150
|
maybeTrim() {
|
package/dist/formState.test.js
CHANGED
|
@@ -112,7 +112,8 @@ describe("formState", () => {
|
|
|
112
112
|
const a1 = { firstName: "a1" };
|
|
113
113
|
const state = createAuthorInputState(a1);
|
|
114
114
|
state.firstName.set("a2");
|
|
115
|
-
expect(state.
|
|
115
|
+
expect(state.value === a1).toEqual(true);
|
|
116
|
+
expect(state.originalValue !== a1).toEqual(true);
|
|
116
117
|
expect(a1.firstName).toEqual("a2");
|
|
117
118
|
});
|
|
118
119
|
it("maintains object identity of lists", () => {
|
|
@@ -122,11 +123,11 @@ describe("formState", () => {
|
|
|
122
123
|
const state = createAuthorInputState(a1);
|
|
123
124
|
const b2 = { title: "t2" };
|
|
124
125
|
state.books.add(b2);
|
|
125
|
-
expect(state.
|
|
126
|
+
expect(state.value.books === a1.books).toEqual(true);
|
|
126
127
|
expect(state.books.value.length).toEqual(2);
|
|
127
128
|
expect((_a = a1.books) === null || _a === void 0 ? void 0 : _a.length).toEqual(2);
|
|
128
|
-
expect(state.books.rows[0].
|
|
129
|
-
expect(state.books.rows[1].
|
|
129
|
+
expect(state.books.rows[0].value === b1).toEqual(true);
|
|
130
|
+
expect(state.books.rows[1].value === b2).toEqual(true);
|
|
130
131
|
expect(a1.books[1] === b2).toEqual(true);
|
|
131
132
|
});
|
|
132
133
|
it("can add items anywhere within a list", () => {
|
|
@@ -141,14 +142,14 @@ describe("formState", () => {
|
|
|
141
142
|
});
|
|
142
143
|
const b2 = { title: "t2" };
|
|
143
144
|
state.books.add(b2, 0);
|
|
144
|
-
expect(state.books.rows[0].
|
|
145
|
-
expect(state.books.rows[1].
|
|
145
|
+
expect(state.books.rows[0].value === b2).toEqual(true);
|
|
146
|
+
expect(state.books.rows[1].value === b1).toEqual(true);
|
|
146
147
|
expect(ticks).toEqual(2);
|
|
147
148
|
const b3 = { title: "t3" };
|
|
148
149
|
state.books.add(b3, 1);
|
|
149
|
-
expect(state.books.rows[0].
|
|
150
|
-
expect(state.books.rows[1].
|
|
151
|
-
expect(state.books.rows[2].
|
|
150
|
+
expect(state.books.rows[0].value === b2).toEqual(true);
|
|
151
|
+
expect(state.books.rows[1].value === b3).toEqual(true);
|
|
152
|
+
expect(state.books.rows[2].value === b1).toEqual(true);
|
|
152
153
|
expect(ticks).toEqual(3);
|
|
153
154
|
});
|
|
154
155
|
it("list value can observe changes", () => {
|
|
@@ -164,6 +165,34 @@ describe("formState", () => {
|
|
|
164
165
|
state.books.add({ title: "t2" }, 0);
|
|
165
166
|
expect(ticks).toEqual(2);
|
|
166
167
|
});
|
|
168
|
+
it("list originalValue can observe changes", () => {
|
|
169
|
+
const b1 = { title: "t1" };
|
|
170
|
+
const a1 = { firstName: "a1", books: [b1] };
|
|
171
|
+
const state = createAuthorInputState(a1);
|
|
172
|
+
let books;
|
|
173
|
+
let ticks = 0;
|
|
174
|
+
(0, mobx_1.autorun)(() => {
|
|
175
|
+
books = state.books.originalValue;
|
|
176
|
+
ticks++;
|
|
177
|
+
});
|
|
178
|
+
state.books.add({ title: "t2" }, 0);
|
|
179
|
+
expect(ticks).toEqual(1);
|
|
180
|
+
state.commitChanges();
|
|
181
|
+
expect(ticks).toEqual(2);
|
|
182
|
+
});
|
|
183
|
+
it("state originalValue can observe changes", () => {
|
|
184
|
+
const a1 = { firstName: "a1", books: [] };
|
|
185
|
+
const state = createAuthorInputState(a1);
|
|
186
|
+
let ticks = 0;
|
|
187
|
+
(0, mobx_1.autorun)(() => {
|
|
188
|
+
noop(state.originalValue);
|
|
189
|
+
ticks++;
|
|
190
|
+
});
|
|
191
|
+
state.books.add({ title: "t2" }, 0);
|
|
192
|
+
expect(ticks).toEqual(1);
|
|
193
|
+
state.commitChanges();
|
|
194
|
+
expect(ticks).toEqual(2);
|
|
195
|
+
});
|
|
167
196
|
it("maintains unknown fields", () => {
|
|
168
197
|
// Given the form is not directly editing id fields
|
|
169
198
|
const config = {
|
|
@@ -177,9 +206,8 @@ describe("formState", () => {
|
|
|
177
206
|
state.firstName.set("a2");
|
|
178
207
|
state.books.add({ title: "t2" });
|
|
179
208
|
// When we get back the originalValue
|
|
180
|
-
const a2 = state.originalValue;
|
|
181
209
|
// Then it has the ids and the new values
|
|
182
|
-
expect(
|
|
210
|
+
expect(state.value).toMatchObject({
|
|
183
211
|
id: "1",
|
|
184
212
|
books: [{ id: "2", title: "t1" }, { title: "t2" }],
|
|
185
213
|
});
|
|
@@ -600,8 +628,8 @@ describe("formState", () => {
|
|
|
600
628
|
firstName: "a1",
|
|
601
629
|
lastName: "aL1",
|
|
602
630
|
books: [
|
|
603
|
-
{ title: "b1", classification: formStateDomain_1.dd100 },
|
|
604
|
-
{ title: "b2", classification: formStateDomain_1.dd100 },
|
|
631
|
+
{ id: "b:1", title: "b1", classification: formStateDomain_1.dd100 },
|
|
632
|
+
{ id: "b:2", title: "b2", classification: formStateDomain_1.dd100 },
|
|
605
633
|
],
|
|
606
634
|
});
|
|
607
635
|
// And some values have been changed
|
|
@@ -678,7 +706,7 @@ describe("formState", () => {
|
|
|
678
706
|
a1.firstName.value = "";
|
|
679
707
|
// Then we keep it as null
|
|
680
708
|
expect(a1.firstName.value).toBeNull();
|
|
681
|
-
expect(a1.originalValue.firstName).
|
|
709
|
+
expect(a1.originalValue.firstName).toBe("asdf");
|
|
682
710
|
expect(a1.firstName.dirty).toBeTruthy();
|
|
683
711
|
});
|
|
684
712
|
it("initializes null values to be undefined", () => {
|
|
@@ -1076,6 +1104,45 @@ describe("formState", () => {
|
|
|
1076
1104
|
id: "a:1",
|
|
1077
1105
|
books: [{ id: "b:1", title: "t1b" }, { id: "b:2" }],
|
|
1078
1106
|
});
|
|
1107
|
+
// And add a 3rd new book
|
|
1108
|
+
formState.books.add({ title: "t3" });
|
|
1109
|
+
// Then we get the author id (for updates) and all 3 books
|
|
1110
|
+
expect(formState.changedValue).toEqual({
|
|
1111
|
+
id: "a:1",
|
|
1112
|
+
books: [{ id: "b:1", title: "t1b" }, { id: "b:2" }, { title: "t3" }],
|
|
1113
|
+
});
|
|
1114
|
+
// And we can still get the original value
|
|
1115
|
+
expect(formState.originalValue).toEqual({
|
|
1116
|
+
id: "a:1",
|
|
1117
|
+
firstName: "f",
|
|
1118
|
+
books: [
|
|
1119
|
+
{ id: "b:1", title: "t1" },
|
|
1120
|
+
{ id: "b:2", title: "t2" },
|
|
1121
|
+
],
|
|
1122
|
+
});
|
|
1123
|
+
// And the books.originalValue as well
|
|
1124
|
+
expect(formState.books.originalValue).toEqual([
|
|
1125
|
+
{ id: "b:1", title: "t1" },
|
|
1126
|
+
{ id: "b:2", title: "t2" },
|
|
1127
|
+
]);
|
|
1128
|
+
// And when we commit changes
|
|
1129
|
+
formState.commitChanges();
|
|
1130
|
+
// Then our originalValue reflects the commit
|
|
1131
|
+
expect(formState.originalValue).toEqual({
|
|
1132
|
+
id: "a:1",
|
|
1133
|
+
firstName: "f",
|
|
1134
|
+
books: [
|
|
1135
|
+
{ id: "b:1", title: "t1b" },
|
|
1136
|
+
{ id: "b:2", title: "t2" },
|
|
1137
|
+
{ id: undefined, title: "t3" },
|
|
1138
|
+
],
|
|
1139
|
+
});
|
|
1140
|
+
// And the books.originalValue as well
|
|
1141
|
+
expect(formState.books.originalValue).toEqual([
|
|
1142
|
+
{ id: "b:1", title: "t1b" },
|
|
1143
|
+
{ id: "b:2", title: "t2" },
|
|
1144
|
+
{ title: "t3" },
|
|
1145
|
+
]);
|
|
1079
1146
|
});
|
|
1080
1147
|
it("can return only changed but incremental list fields", () => {
|
|
1081
1148
|
// Given an author
|
package/dist/useFormStates.js
CHANGED
|
@@ -64,7 +64,9 @@ function useFormStates(opts) {
|
|
|
64
64
|
}
|
|
65
65
|
// If the source of truth changed, then update the existing state and return it.
|
|
66
66
|
if (existing && existing[1] !== input) {
|
|
67
|
-
form.set((0, utils_1.initValue)(config, map ? { map, input } : input), {
|
|
67
|
+
form.set((0, utils_1.initValue)(config, map ? { map, input } : input), {
|
|
68
|
+
refreshing: true,
|
|
69
|
+
});
|
|
68
70
|
existing[1] = input;
|
|
69
71
|
}
|
|
70
72
|
form.readOnly = readOnlyRef.current || !!opts.readOnly;
|
package/dist/utils.d.ts
CHANGED
|
@@ -44,3 +44,5 @@ export declare function areEqual<T>(a?: T, b?: T, strictOrder?: boolean): boolea
|
|
|
44
44
|
export declare function hasToJSON(o?: unknown): o is {
|
|
45
45
|
toJSON(): void;
|
|
46
46
|
};
|
|
47
|
+
/** Make a clone of `obj`, but only recurse into POJOs and Arrays...and stores. */
|
|
48
|
+
export declare function deepClone<T>(obj: T, map?: WeakMap<WeakKey, any>): T;
|
package/dist/utils.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.hasToJSON = exports.areEqual = exports.isEmpty = exports.isNotUndefined = exports.pickFields = exports.isQuery = exports.isInput = exports.initValue = exports.assertNever = exports.fail = void 0;
|
|
6
|
+
exports.deepClone = exports.hasToJSON = exports.areEqual = exports.isEmpty = exports.isNotUndefined = exports.pickFields = exports.isQuery = exports.isInput = exports.initValue = exports.assertNever = exports.fail = void 0;
|
|
7
7
|
const fast_deep_equal_1 = __importDefault(require("fast-deep-equal"));
|
|
8
8
|
const is_plain_object_1 = require("is-plain-object");
|
|
9
9
|
const mobx_1 = require("mobx");
|
|
@@ -126,3 +126,18 @@ function hasToJSON(o) {
|
|
|
126
126
|
return !!(o && typeof o === "object" && "toJSON" in o);
|
|
127
127
|
}
|
|
128
128
|
exports.hasToJSON = hasToJSON;
|
|
129
|
+
/** Make a clone of `obj`, but only recurse into POJOs and Arrays...and stores. */
|
|
130
|
+
function deepClone(obj, map = new WeakMap()) {
|
|
131
|
+
if (obj && typeof obj === "object" && ((0, is_plain_object_1.isPlainObject)(obj) || Array.isArray(obj) || (0, mobx_1.isObservable)(obj))) {
|
|
132
|
+
if (map.has(obj))
|
|
133
|
+
return map.get(obj);
|
|
134
|
+
const result = Array.isArray(obj) ? [] : {};
|
|
135
|
+
map.set(obj, result);
|
|
136
|
+
Object.assign(result, ...Object.keys(obj).map((key) => ({ [key]: deepClone(obj[key], map) })));
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
return obj;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
exports.deepClone = deepClone;
|