@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 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 && (!field.dirty || !field._focused)) {
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 refreshed values if we're already dirty
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 currentList = this.value;
415
- const a = (currentList || []).every((e) => (originalCopy || []).includes(e));
416
- const b = (originalCopy || []).every((e) => (currentList || []).includes(e));
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) {
@@ -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: 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,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 onBlur for fields in list that were initially undefined", async () => {
1326
- const onBlur = jest.fn();
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: () => onBlur(),
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": "blur", onClick: () => form.books.rows[0].title.blur() }, void 0)] }, void 0)) }, 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));
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.blur);
1346
- // Then expect onBlur to triggered
1347
- expect(onBlur).toBeCalledTimes(1);
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" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/form-state",
3
- "version": "2.2.4",
3
+ "version": "2.2.8",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",