@homebound/form-state 2.2.3 → 2.2.7
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/README.md +2 -2
- package/dist/formState.js +21 -6
- package/dist/formState.test.js +67 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,6 @@ Push release.
|
|
|
4
4
|
|
|
5
5
|
# Todo
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- Add conditional readonly logic, like `{ type: "field", readOnlyIf: i => i.isInternal.value }`
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- Add `omitFromValue` so we can have two fields, `book.author.id` / `book.author.name`, where `book.author.name` is used for showing the author name, but `book.author.id` is the only field that is submitted to the server on mutation (maybe pair this with `Ref` based mutations)
|
package/dist/formState.js
CHANGED
|
@@ -329,7 +329,7 @@ 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) {
|
|
332
|
+
if (opts.refreshing && this.dirty && value !== this.value) {
|
|
333
333
|
// Ignore refreshed values if we're already dirty
|
|
334
334
|
return;
|
|
335
335
|
}
|
|
@@ -411,9 +411,18 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
411
411
|
},
|
|
412
412
|
// private
|
|
413
413
|
hasNewItems() {
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
414
|
+
const [current, original] = [this.value || [], originalCopy || []];
|
|
415
|
+
// Instead of relying on just object identities, we look up each child's state
|
|
416
|
+
// in rowMap, because we already dedup/check object identity (i.e. look for id fields)
|
|
417
|
+
// when create object states.
|
|
418
|
+
const a = current.every((e) => {
|
|
419
|
+
const state = rowMap.get(e);
|
|
420
|
+
return original.some((e) => rowMap.get(e) === state);
|
|
421
|
+
});
|
|
422
|
+
const b = original.every((e) => {
|
|
423
|
+
const state = rowMap.get(e);
|
|
424
|
+
return current.some((e) => rowMap.get(e) === state);
|
|
425
|
+
});
|
|
417
426
|
const isSame = a && b;
|
|
418
427
|
return !isSame;
|
|
419
428
|
},
|
|
@@ -490,21 +499,26 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
490
499
|
}
|
|
491
500
|
}
|
|
492
501
|
// If we didn't have an existing child, just make a new object state
|
|
493
|
-
childState = createObjectState(config, value);
|
|
502
|
+
childState = createObjectState(config, value, { onBlur });
|
|
494
503
|
rowMap.set(value, childState);
|
|
495
504
|
}
|
|
496
505
|
// Return the already-observable'd value so that our `parent.value[key] = values` doesn't re-proxy things
|
|
497
506
|
return childState.value;
|
|
498
507
|
});
|
|
508
|
+
// Reset originalCopy so that our dirty checks have the right # of rows.
|
|
509
|
+
if (opts.refreshing) {
|
|
510
|
+
originalCopy = [...(parentInstance[key] || [])];
|
|
511
|
+
}
|
|
499
512
|
_tick.value++;
|
|
500
513
|
},
|
|
501
514
|
add(value, spliceIndex) {
|
|
502
515
|
// This is called by the user, so value should be a non-proxy value we should keep
|
|
503
|
-
const childState = createObjectState(config, value);
|
|
516
|
+
const childState = createObjectState(config, value, { onBlur });
|
|
504
517
|
rowMap.set(value, childState);
|
|
505
518
|
this.ensureSet();
|
|
506
519
|
this.value.splice(typeof spliceIndex === "number" ? spliceIndex : this.value.length, 0, childState.value);
|
|
507
520
|
_tick.value++;
|
|
521
|
+
onBlur();
|
|
508
522
|
},
|
|
509
523
|
remove(indexOrValue) {
|
|
510
524
|
this.ensureSet();
|
|
@@ -518,6 +532,7 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
|
|
|
518
532
|
}
|
|
519
533
|
}
|
|
520
534
|
_tick.value++;
|
|
535
|
+
onBlur();
|
|
521
536
|
},
|
|
522
537
|
reset() {
|
|
523
538
|
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();
|
|
@@ -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,6 +1348,41 @@ describe("formState", () => {
|
|
|
1322
1348
|
expect(r.firstName().textContent).toEqual("f2");
|
|
1323
1349
|
expect(r.fullName().textContent).toEqual("f2 l2");
|
|
1324
1350
|
});
|
|
1351
|
+
it("can trigger auto save for fields in list that were initially undefined", async () => {
|
|
1352
|
+
const autoSave = jest.fn();
|
|
1353
|
+
// Given a component
|
|
1354
|
+
function TestComponent() {
|
|
1355
|
+
// When the data is initially undefined
|
|
1356
|
+
const [data, setData] = react_1.useState();
|
|
1357
|
+
const data2 = { books: [{ title: "Title 1" }] };
|
|
1358
|
+
const form = formState_1.useFormState({
|
|
1359
|
+
config: authorWithBooksConfig,
|
|
1360
|
+
init: { input: data, map: (d) => d, ifUndefined: { books: [] } },
|
|
1361
|
+
autoSave,
|
|
1362
|
+
});
|
|
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),
|
|
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));
|
|
1367
|
+
}
|
|
1368
|
+
// Given a formState with `onBlur` set
|
|
1369
|
+
const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
|
|
1370
|
+
// When the data/child is now available
|
|
1371
|
+
rtl_utils_1.click(r.refreshData);
|
|
1372
|
+
// And the field is blurred
|
|
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);
|
|
1385
|
+
});
|
|
1325
1386
|
});
|
|
1326
1387
|
class ObservableObject {
|
|
1327
1388
|
constructor() {
|
|
@@ -1354,8 +1415,8 @@ const authorWithBooksConfig = {
|
|
|
1354
1415
|
},
|
|
1355
1416
|
},
|
|
1356
1417
|
};
|
|
1357
|
-
function createAuthorInputState(input) {
|
|
1358
|
-
return formState_1.createObjectState(authorWithBooksConfig, input);
|
|
1418
|
+
function createAuthorInputState(input, onBlur) {
|
|
1419
|
+
return formState_1.createObjectState(authorWithBooksConfig, input, { onBlur });
|
|
1359
1420
|
}
|
|
1360
1421
|
const authorWithAddressConfig = {
|
|
1361
1422
|
id: { type: "value" },
|