@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 CHANGED
@@ -4,6 +4,6 @@ Push release.
4
4
 
5
5
  # Todo
6
6
 
7
- * Add conditional readonly logic, like `{ type: "field", readOnlyIf: i => i.isInternal.value }`
7
+ - Add conditional readonly logic, like `{ type: "field", readOnlyIf: i => i.isInternal.value }`
8
8
 
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)
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 currentList = this.value;
415
- const a = (currentList || []).every((e) => (originalCopy || []).includes(e));
416
- const b = (originalCopy || []).every((e) => (currentList || []).includes(e));
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) {
@@ -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: refreshData }, void 0),
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" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/form-state",
3
- "version": "2.2.3",
3
+ "version": "2.2.7",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",