@homebound/form-state 2.2.2 → 2.2.6

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
@@ -42,8 +42,9 @@ function useFormState(opts) {
42
42
  // For this useMemo, we (almost) never re-run so that we can have a stable `form` identity across query refreshes.
43
43
  // eslint-disable-next-line react-hooks/exhaustive-deps
44
44
  [config, ...(isWrappingMobxProxy ? dep : [])]);
45
- // For this useMemo, we re-run any time input changes (could be a useEffect, but running immediately seems better)
46
- react_1.useMemo(() => {
45
+ // We use useEffect so that any mutations to the proxies, which will call `setState`s on any observers to
46
+ // queue their components' render), don't happen during our render, per https://fb.me/setstate-in-render.
47
+ react_1.useEffect(() => {
47
48
  // Ignore the 1st run b/c our 1st useMemo already initialized `form` with the current `init` value.
48
49
  // Also for mobx proxies, we recreate a new form-state every time init changes, so that our
49
50
  // fields fundamentally pointing to the right proxy. So just defer to the ^ useMemo.
@@ -489,7 +490,7 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
489
490
  }
490
491
  }
491
492
  // If we didn't have an existing child, just make a new object state
492
- childState = createObjectState(config, value);
493
+ childState = createObjectState(config, value, { onBlur });
493
494
  rowMap.set(value, childState);
494
495
  }
495
496
  // Return the already-observable'd value so that our `parent.value[key] = values` doesn't re-proxy things
@@ -499,11 +500,12 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
499
500
  },
500
501
  add(value, spliceIndex) {
501
502
  // This is called by the user, so value should be a non-proxy value we should keep
502
- const childState = createObjectState(config, value);
503
+ const childState = createObjectState(config, value, { onBlur });
503
504
  rowMap.set(value, childState);
504
505
  this.ensureSet();
505
506
  this.value.splice(typeof spliceIndex === "number" ? spliceIndex : this.value.length, 0, childState.value);
506
507
  _tick.value++;
508
+ onBlur();
507
509
  },
508
510
  remove(indexOrValue) {
509
511
  this.ensureSet();
@@ -517,6 +519,7 @@ function newListFieldState(parentInstance, parentState, key, rules, listConfig,
517
519
  }
518
520
  }
519
521
  _tick.value++;
522
+ onBlur();
520
523
  },
521
524
  reset() {
522
525
  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();
@@ -1322,6 +1332,38 @@ describe("formState", () => {
1322
1332
  expect(r.firstName().textContent).toEqual("f2");
1323
1333
  expect(r.fullName().textContent).toEqual("f2 l2");
1324
1334
  });
1335
+ it("can trigger onBlur for fields in list that were initially undefined", async () => {
1336
+ const onBlur = jest.fn();
1337
+ // Given a component
1338
+ function TestComponent() {
1339
+ // When the data is initially undefined
1340
+ const [data, setData] = react_1.useState();
1341
+ const data2 = { books: [{ title: "Title 1" }] };
1342
+ const form = formState_1.useFormState({
1343
+ config: authorWithBooksConfig,
1344
+ init: { input: data, map: (d) => d, ifUndefined: { books: [] } },
1345
+ autoSave: () => onBlur(),
1346
+ });
1347
+ 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
+ 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": "blur", onClick: () => form.books.rows[0].title.blur() }, void 0),
1350
+ jsx_runtime_1.jsx("button", { "data-testid": "blurNew", onClick: () => form.books.rows[1].title.blur() }, void 0)] }, void 0)) }, void 0));
1351
+ }
1352
+ // Given a formState with `onBlur` set
1353
+ const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
1354
+ // When the data/child is now available
1355
+ rtl_utils_1.click(r.refreshData);
1356
+ // And the field is blurred
1357
+ rtl_utils_1.click(r.blur);
1358
+ // Then expect onBlur to triggered
1359
+ expect(onBlur).toBeCalledTimes(1);
1360
+ // And when adding a new book
1361
+ rtl_utils_1.click(r.add);
1362
+ // And the new book is blurred
1363
+ rtl_utils_1.click(r.blurNew);
1364
+ // Then expect onBlur to be triggered again
1365
+ expect(onBlur).toBeCalledTimes(3);
1366
+ });
1325
1367
  });
1326
1368
  class ObservableObject {
1327
1369
  constructor() {
@@ -1354,8 +1396,8 @@ const authorWithBooksConfig = {
1354
1396
  },
1355
1397
  },
1356
1398
  };
1357
- function createAuthorInputState(input) {
1358
- return formState_1.createObjectState(authorWithBooksConfig, input);
1399
+ function createAuthorInputState(input, onBlur) {
1400
+ return formState_1.createObjectState(authorWithBooksConfig, input, { onBlur });
1359
1401
  }
1360
1402
  const authorWithAddressConfig = {
1361
1403
  id: { type: "value" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/form-state",
3
- "version": "2.2.2",
3
+ "version": "2.2.6",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",