@homebound/form-state 2.2.4 → 2.2.8
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 +24 -7
- package/dist/formState.test.js +52 -15
- 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
|
});
|
|
@@ -329,8 +329,10 @@ function newValueFieldState(parentInstance, parentState, key, rules, isIdKey, is
|
|
|
329
329
|
if (this.readOnly && !opts.resetting && !opts.refreshing) {
|
|
330
330
|
throw new Error(`${key} is currently readOnly`);
|
|
331
331
|
}
|
|
332
|
-
if (opts.refreshing && this.dirty) {
|
|
333
|
-
// Ignore
|
|
332
|
+
if (opts.refreshing && this.dirty && this.value !== value) {
|
|
333
|
+
// Ignore incoming values if we have changes (this.dirty) unless our latest change (this.value)
|
|
334
|
+
// matches the incoming value (value), b/c if it does we should accept it and reset originalValue
|
|
335
|
+
// so that we're not longer dirty.
|
|
334
336
|
return;
|
|
335
337
|
}
|
|
336
338
|
else if (computed && (opts.resetting || opts.refreshing)) {
|
|
@@ -411,9 +413,18 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
411
413
|
},
|
|
412
414
|
// private
|
|
413
415
|
hasNewItems() {
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
416
|
+
const [current, original] = [this.value || [], originalCopy || []];
|
|
417
|
+
// Instead of relying on just object identities, we look up each child's state
|
|
418
|
+
// in rowMap, because we already dedup/check object identity (i.e. look for id fields)
|
|
419
|
+
// when create object states.
|
|
420
|
+
const a = current.every((e) => {
|
|
421
|
+
const state = rowMap.get(e);
|
|
422
|
+
return original.some((e) => rowMap.get(e) === state);
|
|
423
|
+
});
|
|
424
|
+
const b = original.every((e) => {
|
|
425
|
+
const state = rowMap.get(e);
|
|
426
|
+
return current.some((e) => rowMap.get(e) === state);
|
|
427
|
+
});
|
|
417
428
|
const isSame = a && b;
|
|
418
429
|
return !isSame;
|
|
419
430
|
},
|
|
@@ -496,15 +507,20 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
496
507
|
// Return the already-observable'd value so that our `parent.value[key] = values` doesn't re-proxy things
|
|
497
508
|
return childState.value;
|
|
498
509
|
});
|
|
510
|
+
// Reset originalCopy so that our dirty checks have the right # of rows.
|
|
511
|
+
if (opts.refreshing) {
|
|
512
|
+
originalCopy = [...(parentInstance[key] || [])];
|
|
513
|
+
}
|
|
499
514
|
_tick.value++;
|
|
500
515
|
},
|
|
501
516
|
add(value, spliceIndex) {
|
|
502
517
|
// This is called by the user, so value should be a non-proxy value we should keep
|
|
503
|
-
const childState = createObjectState(config, value);
|
|
518
|
+
const childState = createObjectState(config, value, { onBlur });
|
|
504
519
|
rowMap.set(value, childState);
|
|
505
520
|
this.ensureSet();
|
|
506
521
|
this.value.splice(typeof spliceIndex === "number" ? spliceIndex : this.value.length, 0, childState.value);
|
|
507
522
|
_tick.value++;
|
|
523
|
+
onBlur();
|
|
508
524
|
},
|
|
509
525
|
remove(indexOrValue) {
|
|
510
526
|
this.ensureSet();
|
|
@@ -518,6 +534,7 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
518
534
|
}
|
|
519
535
|
}
|
|
520
536
|
_tick.value++;
|
|
537
|
+
onBlur();
|
|
521
538
|
},
|
|
522
539
|
reset() {
|
|
523
540
|
if (originalCopy) {
|
package/dist/formState.test.js
CHANGED
|
@@ -321,6 +321,16 @@ describe("formState", () => {
|
|
|
321
321
|
a1.books.remove(0);
|
|
322
322
|
expect(a1.dirty).toBeFalsy();
|
|
323
323
|
});
|
|
324
|
+
it("it calls onBlur when adding or removing to a list", () => {
|
|
325
|
+
const onBlur = jest.fn();
|
|
326
|
+
const a1 = createAuthorInputState({ books: [] }, onBlur);
|
|
327
|
+
expect(a1.books.dirty).toBeFalsy();
|
|
328
|
+
a1.books.add({ title: "t2" });
|
|
329
|
+
expect(a1.books.dirty).toBeTruthy();
|
|
330
|
+
a1.books.remove(0);
|
|
331
|
+
expect(a1.dirty).toBeFalsy();
|
|
332
|
+
expect(onBlur).toBeCalledTimes(2);
|
|
333
|
+
});
|
|
324
334
|
it("knows list of primitives are dirty", () => {
|
|
325
335
|
const a1 = formState_1.createObjectState({ favoriteColors: { type: "value" } }, {});
|
|
326
336
|
expect(a1.favoriteColors.dirty).toBeFalsy();
|
|
@@ -921,7 +931,7 @@ describe("formState", () => {
|
|
|
921
931
|
// And they have wip edits
|
|
922
932
|
formState.firstName.set("ff");
|
|
923
933
|
// When a mutation result sets both firstName and lastName
|
|
924
|
-
formState.set({ firstName: "f2", lastName: "l2" });
|
|
934
|
+
formState.set({ firstName: "f2", lastName: "l2" }, { refreshing: true });
|
|
925
935
|
// Then we don't overwrite the user's WIP work
|
|
926
936
|
expect(formState.firstName.value).toEqual("ff");
|
|
927
937
|
expect(formState.lastName.value).toEqual("l2");
|
|
@@ -934,7 +944,7 @@ describe("formState", () => {
|
|
|
934
944
|
// Given first name is focused, but has not changed
|
|
935
945
|
formState.firstName.focus();
|
|
936
946
|
// When a mutation result sets both firstName and lastName
|
|
937
|
-
formState.set({ firstName: "f2", lastName: "l2" });
|
|
947
|
+
formState.set({ firstName: "f2", lastName: "l2" }, { refreshing: true });
|
|
938
948
|
// Then we do update the value
|
|
939
949
|
expect(formState.firstName.value).toEqual("f2");
|
|
940
950
|
});
|
|
@@ -1187,6 +1197,18 @@ describe("formState", () => {
|
|
|
1187
1197
|
{ id: "b:3", title: "b3" },
|
|
1188
1198
|
],
|
|
1189
1199
|
};
|
|
1200
|
+
// Eventually our local values are saved
|
|
1201
|
+
const data3 = {
|
|
1202
|
+
id: "a:1",
|
|
1203
|
+
firstName: "local",
|
|
1204
|
+
lastName: "l2",
|
|
1205
|
+
address: { id: "address:1", street: "local", city: "c2" },
|
|
1206
|
+
books: [
|
|
1207
|
+
{ id: "b:1", title: "local" },
|
|
1208
|
+
{ id: "b:2", title: "b2" },
|
|
1209
|
+
{ id: "b:3", title: "b3" },
|
|
1210
|
+
],
|
|
1211
|
+
};
|
|
1190
1212
|
// And we start out with data1
|
|
1191
1213
|
const [data, setData] = react_1.useState(data1);
|
|
1192
1214
|
const form = formState_1.useFormState({ config, init: { input: data, map: (d) => d } });
|
|
@@ -1195,9 +1217,6 @@ describe("formState", () => {
|
|
|
1195
1217
|
form.address.street.value = "local";
|
|
1196
1218
|
form.books.rows[0].title.value = "local";
|
|
1197
1219
|
}
|
|
1198
|
-
function refreshData() {
|
|
1199
|
-
setData(data2);
|
|
1200
|
-
}
|
|
1201
1220
|
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),
|
|
1202
1221
|
jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "lastName" }, { children: form.lastName.value }), void 0),
|
|
1203
1222
|
jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "street" }, { children: form.address.street.value }), void 0),
|
|
@@ -1206,7 +1225,8 @@ describe("formState", () => {
|
|
|
1206
1225
|
jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "title2" }, { children: form.books.rows[1].title.value }), void 0),
|
|
1207
1226
|
jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "booksLength" }, { children: form.books.rows.length }), void 0),
|
|
1208
1227
|
jsx_runtime_1.jsx("button", { "data-testid": "makeLocalChanges", onClick: makeLocalChanges }, void 0),
|
|
1209
|
-
jsx_runtime_1.jsx("button", { "data-testid": "refreshData", onClick:
|
|
1228
|
+
jsx_runtime_1.jsx("button", { "data-testid": "refreshData", onClick: () => setData(data2) }, void 0),
|
|
1229
|
+
jsx_runtime_1.jsx("button", { "data-testid": "saveData", onClick: () => setData(data3) }, void 0),
|
|
1210
1230
|
jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "changedValue" }, { children: JSON.stringify(form.changedValue) }), void 0)] }, void 0)) }, void 0));
|
|
1211
1231
|
}
|
|
1212
1232
|
// And we start out with the initial query data
|
|
@@ -1243,6 +1263,12 @@ describe("formState", () => {
|
|
|
1243
1263
|
books: [{ id: "b:1", title: "local" }, { id: "b:2" }, { id: "b:3" }],
|
|
1244
1264
|
firstName: "local",
|
|
1245
1265
|
});
|
|
1266
|
+
// And then when our mutation results come back
|
|
1267
|
+
rtl_utils_1.click(r.saveData);
|
|
1268
|
+
// Then changedValue doesn't show our local changes anymore
|
|
1269
|
+
expect(JSON.parse(r.changedValue().textContent)).toEqual({
|
|
1270
|
+
id: "a:1",
|
|
1271
|
+
});
|
|
1246
1272
|
});
|
|
1247
1273
|
it("useFormState can accept new data while read only", async () => {
|
|
1248
1274
|
// Given a component
|
|
@@ -1322,8 +1348,8 @@ describe("formState", () => {
|
|
|
1322
1348
|
expect(r.firstName().textContent).toEqual("f2");
|
|
1323
1349
|
expect(r.fullName().textContent).toEqual("f2 l2");
|
|
1324
1350
|
});
|
|
1325
|
-
it("can trigger
|
|
1326
|
-
const
|
|
1351
|
+
it("can trigger auto save for fields in list that were initially undefined", async () => {
|
|
1352
|
+
const autoSave = jest.fn();
|
|
1327
1353
|
// Given a component
|
|
1328
1354
|
function TestComponent() {
|
|
1329
1355
|
// When the data is initially undefined
|
|
@@ -1332,19 +1358,30 @@ describe("formState", () => {
|
|
|
1332
1358
|
const form = formState_1.useFormState({
|
|
1333
1359
|
config: authorWithBooksConfig,
|
|
1334
1360
|
init: { input: data, map: (d) => d, ifUndefined: { books: [] } },
|
|
1335
|
-
autoSave
|
|
1361
|
+
autoSave,
|
|
1336
1362
|
});
|
|
1337
1363
|
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),
|
|
1338
|
-
jsx_runtime_1.jsx("button", { "data-testid": "
|
|
1364
|
+
jsx_runtime_1.jsx("button", { "data-testid": "add", onClick: () => form.books.add({ title: "New Book" }) }, void 0),
|
|
1365
|
+
jsx_runtime_1.jsx("button", { "data-testid": "blurBookOne", onClick: () => form.books.rows[0].title.blur() }, void 0),
|
|
1366
|
+
jsx_runtime_1.jsx("button", { "data-testid": "blurBookTwo", onClick: () => form.books.rows[1].title.blur() }, void 0)] }, void 0)) }, void 0));
|
|
1339
1367
|
}
|
|
1340
1368
|
// Given a formState with `onBlur` set
|
|
1341
1369
|
const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
|
|
1342
1370
|
// When the data/child is now available
|
|
1343
1371
|
rtl_utils_1.click(r.refreshData);
|
|
1344
1372
|
// And the field is blurred
|
|
1345
|
-
rtl_utils_1.click(r.
|
|
1346
|
-
// Then
|
|
1347
|
-
expect(
|
|
1373
|
+
rtl_utils_1.click(r.blurBookOne);
|
|
1374
|
+
// Then we don't auto-save because nothing has changed
|
|
1375
|
+
expect(autoSave).toBeCalledTimes(0);
|
|
1376
|
+
// And when adding a new book
|
|
1377
|
+
rtl_utils_1.click(r.add);
|
|
1378
|
+
// We autoSave the new row right away (because we don't have any validation rules
|
|
1379
|
+
// that say the new row can't be empty)
|
|
1380
|
+
expect(autoSave).toBeCalledTimes(1);
|
|
1381
|
+
// And the new book is blurred
|
|
1382
|
+
rtl_utils_1.click(r.blurBookTwo);
|
|
1383
|
+
// Then we auto save again
|
|
1384
|
+
expect(autoSave).toBeCalledTimes(2);
|
|
1348
1385
|
});
|
|
1349
1386
|
});
|
|
1350
1387
|
class ObservableObject {
|
|
@@ -1378,8 +1415,8 @@ const authorWithBooksConfig = {
|
|
|
1378
1415
|
},
|
|
1379
1416
|
},
|
|
1380
1417
|
};
|
|
1381
|
-
function createAuthorInputState(input) {
|
|
1382
|
-
return formState_1.createObjectState(authorWithBooksConfig, input);
|
|
1418
|
+
function createAuthorInputState(input, onBlur) {
|
|
1419
|
+
return formState_1.createObjectState(authorWithBooksConfig, input, { onBlur });
|
|
1383
1420
|
}
|
|
1384
1421
|
const authorWithAddressConfig = {
|
|
1385
1422
|
id: { type: "value" },
|