@homebound/form-state 2.2.6 → 2.2.10
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/formState.js +43 -12
- package/dist/formState.test.js +106 -18
- package/package.json +1 -1
package/dist/formState.js
CHANGED
|
@@ -180,7 +180,7 @@ function newObjectState(config, parentState, instance, key, onBlur) {
|
|
|
180
180
|
throw new Error(`${key || "formState"} is currently readOnly`);
|
|
181
181
|
}
|
|
182
182
|
getFields(this).forEach((field) => {
|
|
183
|
-
if (field.key in value
|
|
183
|
+
if (field.key in value) {
|
|
184
184
|
field.set(value[field.key], opts);
|
|
185
185
|
}
|
|
186
186
|
});
|
|
@@ -314,11 +314,8 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
|
|
|
314
314
|
},
|
|
315
315
|
blur() {
|
|
316
316
|
// Now that the user is done editing the field, we sneak in some trim logic
|
|
317
|
-
if (
|
|
318
|
-
this.
|
|
319
|
-
if (this.value === "") {
|
|
320
|
-
this.set(undefined);
|
|
321
|
-
}
|
|
317
|
+
if (this._focused) {
|
|
318
|
+
this.maybeTrim();
|
|
322
319
|
}
|
|
323
320
|
this._focused = false;
|
|
324
321
|
// touched is readonly, but we're allowed to change it
|
|
@@ -329,8 +326,10 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
|
|
|
329
326
|
if (this.readOnly && !opts.resetting && !opts.refreshing) {
|
|
330
327
|
throw new Error(`${key} is currently readOnly`);
|
|
331
328
|
}
|
|
332
|
-
if (opts.refreshing && this.dirty) {
|
|
333
|
-
// Ignore
|
|
329
|
+
if (opts.refreshing && this.dirty && this.value !== value) {
|
|
330
|
+
// Ignore incoming values if we have changes (this.dirty) unless our latest change (this.value)
|
|
331
|
+
// matches the incoming value (value), b/c if it does we should accept it and reset originalValue
|
|
332
|
+
// so that we're not longer dirty.
|
|
334
333
|
return;
|
|
335
334
|
}
|
|
336
335
|
else if (computed && (opts.resetting || opts.refreshing)) {
|
|
@@ -349,6 +348,11 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
|
|
|
349
348
|
if (opts.refreshing) {
|
|
350
349
|
this.originalValue = newValue;
|
|
351
350
|
}
|
|
351
|
+
// If we're being set programmatically, i.e. we don't currently have focus,
|
|
352
|
+
// call blur to trigger any auto-saves.
|
|
353
|
+
if (!this._focused && !opts.refreshing && !opts.resetting && this.dirty) {
|
|
354
|
+
this.blur();
|
|
355
|
+
}
|
|
352
356
|
},
|
|
353
357
|
reset() {
|
|
354
358
|
if (!computed) {
|
|
@@ -373,6 +377,17 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
|
|
|
373
377
|
_originalValue = v;
|
|
374
378
|
_originalValueTick.value++;
|
|
375
379
|
},
|
|
380
|
+
maybeTrim() {
|
|
381
|
+
if (typeof this.value === "string") {
|
|
382
|
+
let newValue = this.value.trim();
|
|
383
|
+
if (newValue === "") {
|
|
384
|
+
newValue = undefined;
|
|
385
|
+
}
|
|
386
|
+
if (newValue !== this.value) {
|
|
387
|
+
this.set(newValue);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
},
|
|
376
391
|
};
|
|
377
392
|
return field;
|
|
378
393
|
}
|
|
@@ -411,9 +426,18 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
411
426
|
},
|
|
412
427
|
// private
|
|
413
428
|
hasNewItems() {
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
429
|
+
const [current, original] = [this.value || [], originalCopy || []];
|
|
430
|
+
// Instead of relying on just object identities, we look up each child's state
|
|
431
|
+
// in rowMap, because we already dedup/check object identity (i.e. look for id fields)
|
|
432
|
+
// when create object states.
|
|
433
|
+
const a = current.every((e) => {
|
|
434
|
+
const state = rowMap.get(e);
|
|
435
|
+
return original.some((e) => rowMap.get(e) === state);
|
|
436
|
+
});
|
|
437
|
+
const b = original.every((e) => {
|
|
438
|
+
const state = rowMap.get(e);
|
|
439
|
+
return current.some((e) => rowMap.get(e) === state);
|
|
440
|
+
});
|
|
417
441
|
const isSame = a && b;
|
|
418
442
|
return !isSame;
|
|
419
443
|
},
|
|
@@ -496,6 +520,10 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
496
520
|
// Return the already-observable'd value so that our `parent.value[key] = values` doesn't re-proxy things
|
|
497
521
|
return childState.value;
|
|
498
522
|
});
|
|
523
|
+
// Reset originalCopy so that our dirty checks have the right # of rows.
|
|
524
|
+
if (opts.refreshing) {
|
|
525
|
+
originalCopy = [...(parentInstance[key] || [])];
|
|
526
|
+
}
|
|
499
527
|
_tick.value++;
|
|
500
528
|
},
|
|
501
529
|
add(value, spliceIndex) {
|
|
@@ -541,7 +569,10 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
541
569
|
_tick.value++;
|
|
542
570
|
},
|
|
543
571
|
};
|
|
544
|
-
return list
|
|
572
|
+
return mobx_1.makeAutoObservable(list, {
|
|
573
|
+
// See other makeAutoObservable comment
|
|
574
|
+
value: mobx_1.computed({ equals: () => false }),
|
|
575
|
+
});
|
|
545
576
|
}
|
|
546
577
|
function isNotUndefined(value) {
|
|
547
578
|
return value !== undefined;
|
package/dist/formState.test.js
CHANGED
|
@@ -117,6 +117,19 @@ describe("formState", () => {
|
|
|
117
117
|
expect(state.books.rows[2].originalValue === b1).toEqual(true);
|
|
118
118
|
expect(ticks).toEqual(3);
|
|
119
119
|
});
|
|
120
|
+
it("list value can observe changes", () => {
|
|
121
|
+
const b1 = { title: "t1" };
|
|
122
|
+
const a1 = { firstName: "a1", books: [b1] };
|
|
123
|
+
const state = createAuthorInputState(a1);
|
|
124
|
+
let books;
|
|
125
|
+
let ticks = 0;
|
|
126
|
+
mobx_1.autorun(() => {
|
|
127
|
+
books = state.books.value;
|
|
128
|
+
ticks++;
|
|
129
|
+
});
|
|
130
|
+
state.books.add({ title: "t2" }, 0);
|
|
131
|
+
expect(ticks).toEqual(2);
|
|
132
|
+
});
|
|
120
133
|
it("maintains unknown fields", () => {
|
|
121
134
|
// Given the form is not directly editing id fields
|
|
122
135
|
const config = {
|
|
@@ -321,7 +334,7 @@ describe("formState", () => {
|
|
|
321
334
|
a1.books.remove(0);
|
|
322
335
|
expect(a1.dirty).toBeFalsy();
|
|
323
336
|
});
|
|
324
|
-
it("
|
|
337
|
+
it("calls onBlur when adding or removing to a list", () => {
|
|
325
338
|
const onBlur = jest.fn();
|
|
326
339
|
const a1 = createAuthorInputState({ books: [] }, onBlur);
|
|
327
340
|
expect(a1.books.dirty).toBeFalsy();
|
|
@@ -331,6 +344,60 @@ describe("formState", () => {
|
|
|
331
344
|
expect(a1.dirty).toBeFalsy();
|
|
332
345
|
expect(onBlur).toBeCalledTimes(2);
|
|
333
346
|
});
|
|
347
|
+
it("calls onBlur when programmatically setting a value", () => {
|
|
348
|
+
const onBlur = jest.fn();
|
|
349
|
+
// Given an author listening for blur
|
|
350
|
+
const a1 = createAuthorInputState({ books: [{}] }, onBlur);
|
|
351
|
+
// When we programmatically set a field that isn't focused
|
|
352
|
+
a1.firstName.value = "first";
|
|
353
|
+
// Then we call onBlur
|
|
354
|
+
expect(onBlur).toBeCalledTimes(1);
|
|
355
|
+
// And when we set a nested value
|
|
356
|
+
a1.books.rows[0].title.value = "title";
|
|
357
|
+
// Then we called onBlur again
|
|
358
|
+
expect(onBlur).toBeCalledTimes(2);
|
|
359
|
+
});
|
|
360
|
+
it("defers calling onBlur when setting a bound value", () => {
|
|
361
|
+
const onBlur = jest.fn();
|
|
362
|
+
// Given an author listening for blur
|
|
363
|
+
const a1 = createAuthorInputState({ books: [{}] }, onBlur);
|
|
364
|
+
// And the field is focused
|
|
365
|
+
a1.firstName.focus();
|
|
366
|
+
// When we we set the field
|
|
367
|
+
a1.firstName.value = "first";
|
|
368
|
+
// Then we don't call onBlur
|
|
369
|
+
expect(onBlur).toBeCalledTimes(0);
|
|
370
|
+
});
|
|
371
|
+
it("skips onBlur when refreshing", () => {
|
|
372
|
+
const onBlur = jest.fn();
|
|
373
|
+
// Given an author listening for blur
|
|
374
|
+
const a1 = createAuthorInputState({ books: [{}] }, onBlur);
|
|
375
|
+
// When we programmatically set a field that isn't focused
|
|
376
|
+
a1.set({ firstName: "first" }, { refreshing: true });
|
|
377
|
+
// Then we don't call onBlur
|
|
378
|
+
expect(onBlur).toBeCalledTimes(0);
|
|
379
|
+
});
|
|
380
|
+
it("skips onBlur when resetting", () => {
|
|
381
|
+
const onBlur = jest.fn();
|
|
382
|
+
// Given an author listening for blur
|
|
383
|
+
const a1 = createAuthorInputState({ books: [{}] }, onBlur);
|
|
384
|
+
// And we called onBlur once
|
|
385
|
+
a1.set({ firstName: "first" });
|
|
386
|
+
expect(onBlur).toBeCalledTimes(1);
|
|
387
|
+
// When we reset
|
|
388
|
+
a1.reset();
|
|
389
|
+
// We don't call blur again
|
|
390
|
+
expect(onBlur).toBeCalledTimes(1);
|
|
391
|
+
});
|
|
392
|
+
it("skips onBlur when not dirty", () => {
|
|
393
|
+
const onBlur = jest.fn();
|
|
394
|
+
// Given an author listening for blur
|
|
395
|
+
const a1 = createAuthorInputState({ firstName: "first", books: [{}] }, onBlur);
|
|
396
|
+
// When we programmatically set a field to it's existing valued
|
|
397
|
+
a1.firstName.value = "first";
|
|
398
|
+
// Then we don't call onBlur
|
|
399
|
+
expect(onBlur).toBeCalledTimes(0);
|
|
400
|
+
});
|
|
334
401
|
it("knows list of primitives are dirty", () => {
|
|
335
402
|
const a1 = formState_1.createObjectState({ favoriteColors: { type: "value" } }, {});
|
|
336
403
|
expect(a1.favoriteColors.dirty).toBeFalsy();
|
|
@@ -931,7 +998,7 @@ describe("formState", () => {
|
|
|
931
998
|
// And they have wip edits
|
|
932
999
|
formState.firstName.set("ff");
|
|
933
1000
|
// When a mutation result sets both firstName and lastName
|
|
934
|
-
formState.set({ firstName: "f2", lastName: "l2" });
|
|
1001
|
+
formState.set({ firstName: "f2", lastName: "l2" }, { refreshing: true });
|
|
935
1002
|
// Then we don't overwrite the user's WIP work
|
|
936
1003
|
expect(formState.firstName.value).toEqual("ff");
|
|
937
1004
|
expect(formState.lastName.value).toEqual("l2");
|
|
@@ -944,13 +1011,14 @@ describe("formState", () => {
|
|
|
944
1011
|
// Given first name is focused, but has not changed
|
|
945
1012
|
formState.firstName.focus();
|
|
946
1013
|
// When a mutation result sets both firstName and lastName
|
|
947
|
-
formState.set({ firstName: "f2", lastName: "l2" });
|
|
1014
|
+
formState.set({ firstName: "f2", lastName: "l2" }, { refreshing: true });
|
|
948
1015
|
// Then we do update the value
|
|
949
1016
|
expect(formState.firstName.value).toEqual("f2");
|
|
950
1017
|
});
|
|
951
1018
|
it("trims string values on blur", () => {
|
|
952
1019
|
const formState = formState_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
953
1020
|
// Given the user is typing with spaces
|
|
1021
|
+
formState.firstName.focus();
|
|
954
1022
|
formState.firstName.set("f ");
|
|
955
1023
|
// And we initially keep the space
|
|
956
1024
|
expect(formState.firstName.value).toEqual("f ");
|
|
@@ -962,6 +1030,7 @@ describe("formState", () => {
|
|
|
962
1030
|
it("trims empty values to undefined on blur", () => {
|
|
963
1031
|
const formState = formState_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
|
|
964
1032
|
// Given the user is typing with only spaces
|
|
1033
|
+
formState.firstName.focus();
|
|
965
1034
|
formState.firstName.set(" ");
|
|
966
1035
|
// And we initially keep the space
|
|
967
1036
|
expect(formState.firstName.value).toEqual(" ");
|
|
@@ -1197,6 +1266,18 @@ describe("formState", () => {
|
|
|
1197
1266
|
{ id: "b:3", title: "b3" },
|
|
1198
1267
|
],
|
|
1199
1268
|
};
|
|
1269
|
+
// Eventually our local values are saved
|
|
1270
|
+
const data3 = {
|
|
1271
|
+
id: "a:1",
|
|
1272
|
+
firstName: "local",
|
|
1273
|
+
lastName: "l2",
|
|
1274
|
+
address: { id: "address:1", street: "local", city: "c2" },
|
|
1275
|
+
books: [
|
|
1276
|
+
{ id: "b:1", title: "local" },
|
|
1277
|
+
{ id: "b:2", title: "b2" },
|
|
1278
|
+
{ id: "b:3", title: "b3" },
|
|
1279
|
+
],
|
|
1280
|
+
};
|
|
1200
1281
|
// And we start out with data1
|
|
1201
1282
|
const [data, setData] = react_1.useState(data1);
|
|
1202
1283
|
const form = formState_1.useFormState({ config, init: { input: data, map: (d) => d } });
|
|
@@ -1205,9 +1286,6 @@ describe("formState", () => {
|
|
|
1205
1286
|
form.address.street.value = "local";
|
|
1206
1287
|
form.books.rows[0].title.value = "local";
|
|
1207
1288
|
}
|
|
1208
|
-
function refreshData() {
|
|
1209
|
-
setData(data2);
|
|
1210
|
-
}
|
|
1211
1289
|
return (jsx_runtime_1.jsx(mobx_react_1.Observer, { children: () => (jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "firstName" }, { children: form.firstName.value }), void 0),
|
|
1212
1290
|
jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "lastName" }, { children: form.lastName.value }), void 0),
|
|
1213
1291
|
jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "street" }, { children: form.address.street.value }), void 0),
|
|
@@ -1216,7 +1294,8 @@ describe("formState", () => {
|
|
|
1216
1294
|
jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "title2" }, { children: form.books.rows[1].title.value }), void 0),
|
|
1217
1295
|
jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "booksLength" }, { children: form.books.rows.length }), void 0),
|
|
1218
1296
|
jsx_runtime_1.jsx("button", { "data-testid": "makeLocalChanges", onClick: makeLocalChanges }, void 0),
|
|
1219
|
-
jsx_runtime_1.jsx("button", { "data-testid": "refreshData", onClick:
|
|
1297
|
+
jsx_runtime_1.jsx("button", { "data-testid": "refreshData", onClick: () => setData(data2) }, void 0),
|
|
1298
|
+
jsx_runtime_1.jsx("button", { "data-testid": "saveData", onClick: () => setData(data3) }, void 0),
|
|
1220
1299
|
jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "changedValue" }, { children: JSON.stringify(form.changedValue) }), void 0)] }, void 0)) }, void 0));
|
|
1221
1300
|
}
|
|
1222
1301
|
// And we start out with the initial query data
|
|
@@ -1253,6 +1332,12 @@ describe("formState", () => {
|
|
|
1253
1332
|
books: [{ id: "b:1", title: "local" }, { id: "b:2" }, { id: "b:3" }],
|
|
1254
1333
|
firstName: "local",
|
|
1255
1334
|
});
|
|
1335
|
+
// And then when our mutation results come back
|
|
1336
|
+
rtl_utils_1.click(r.saveData);
|
|
1337
|
+
// Then changedValue doesn't show our local changes anymore
|
|
1338
|
+
expect(JSON.parse(r.changedValue().textContent)).toEqual({
|
|
1339
|
+
id: "a:1",
|
|
1340
|
+
});
|
|
1256
1341
|
});
|
|
1257
1342
|
it("useFormState can accept new data while read only", async () => {
|
|
1258
1343
|
// Given a component
|
|
@@ -1332,8 +1417,8 @@ describe("formState", () => {
|
|
|
1332
1417
|
expect(r.firstName().textContent).toEqual("f2");
|
|
1333
1418
|
expect(r.fullName().textContent).toEqual("f2 l2");
|
|
1334
1419
|
});
|
|
1335
|
-
it("can trigger
|
|
1336
|
-
const
|
|
1420
|
+
it("can trigger auto save for fields in list that were initially undefined", async () => {
|
|
1421
|
+
const autoSave = jest.fn();
|
|
1337
1422
|
// Given a component
|
|
1338
1423
|
function TestComponent() {
|
|
1339
1424
|
// When the data is initially undefined
|
|
@@ -1342,27 +1427,30 @@ describe("formState", () => {
|
|
|
1342
1427
|
const form = formState_1.useFormState({
|
|
1343
1428
|
config: authorWithBooksConfig,
|
|
1344
1429
|
init: { input: data, map: (d) => d, ifUndefined: { books: [] } },
|
|
1345
|
-
autoSave
|
|
1430
|
+
autoSave,
|
|
1346
1431
|
});
|
|
1347
1432
|
return (jsx_runtime_1.jsx(mobx_react_1.Observer, { children: () => (jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsx("button", { "data-testid": "refreshData", onClick: () => setData(data2) }, void 0),
|
|
1348
1433
|
jsx_runtime_1.jsx("button", { "data-testid": "add", onClick: () => form.books.add({ title: "New Book" }) }, void 0),
|
|
1349
|
-
jsx_runtime_1.jsx("button", { "data-testid": "
|
|
1350
|
-
jsx_runtime_1.jsx("button", { "data-testid": "
|
|
1434
|
+
jsx_runtime_1.jsx("button", { "data-testid": "blurBookOne", onClick: () => form.books.rows[0].title.blur() }, void 0),
|
|
1435
|
+
jsx_runtime_1.jsx("button", { "data-testid": "blurBookTwo", onClick: () => form.books.rows[1].title.blur() }, void 0)] }, void 0)) }, void 0));
|
|
1351
1436
|
}
|
|
1352
1437
|
// Given a formState with `onBlur` set
|
|
1353
1438
|
const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
|
|
1354
1439
|
// When the data/child is now available
|
|
1355
1440
|
rtl_utils_1.click(r.refreshData);
|
|
1356
1441
|
// And the field is blurred
|
|
1357
|
-
rtl_utils_1.click(r.
|
|
1358
|
-
// Then
|
|
1359
|
-
expect(
|
|
1442
|
+
rtl_utils_1.click(r.blurBookOne);
|
|
1443
|
+
// Then we don't auto-save because nothing has changed
|
|
1444
|
+
expect(autoSave).toBeCalledTimes(0);
|
|
1360
1445
|
// And when adding a new book
|
|
1361
1446
|
rtl_utils_1.click(r.add);
|
|
1447
|
+
// We autoSave the new row right away (because we don't have any validation rules
|
|
1448
|
+
// that say the new row can't be empty)
|
|
1449
|
+
expect(autoSave).toBeCalledTimes(1);
|
|
1362
1450
|
// And the new book is blurred
|
|
1363
|
-
rtl_utils_1.click(r.
|
|
1364
|
-
// Then
|
|
1365
|
-
expect(
|
|
1451
|
+
rtl_utils_1.click(r.blurBookTwo);
|
|
1452
|
+
// Then we auto save again
|
|
1453
|
+
expect(autoSave).toBeCalledTimes(2);
|
|
1366
1454
|
});
|
|
1367
1455
|
});
|
|
1368
1456
|
class ObservableObject {
|