@homebound/form-state 2.2.9 → 2.2.13

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.
@@ -1,10 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const jsx_runtime_1 = require("react/jsx-runtime");
4
- const rtl_utils_1 = require("@homebound/rtl-utils");
5
3
  const mobx_1 = require("mobx");
6
- const mobx_react_1 = require("mobx-react");
7
- const react_1 = require("react");
8
4
  const formStateDomain_1 = require("./formStateDomain");
9
5
  const formState_1 = require("./formState");
10
6
  describe("formState", () => {
@@ -352,6 +348,10 @@ describe("formState", () => {
352
348
  a1.firstName.value = "first";
353
349
  // Then we call onBlur
354
350
  expect(onBlur).toBeCalledTimes(1);
351
+ // And when we set a nested value
352
+ a1.books.rows[0].title.value = "title";
353
+ // Then we called onBlur again
354
+ expect(onBlur).toBeCalledTimes(2);
355
355
  });
356
356
  it("defers calling onBlur when setting a bound value", () => {
357
357
  const onBlur = jest.fn();
@@ -364,6 +364,36 @@ describe("formState", () => {
364
364
  // Then we don't call onBlur
365
365
  expect(onBlur).toBeCalledTimes(0);
366
366
  });
367
+ it("skips onBlur when refreshing", () => {
368
+ const onBlur = jest.fn();
369
+ // Given an author listening for blur
370
+ const a1 = createAuthorInputState({ books: [{}] }, onBlur);
371
+ // When we programmatically set a field that isn't focused
372
+ a1.set({ firstName: "first" }, { refreshing: true });
373
+ // Then we don't call onBlur
374
+ expect(onBlur).toBeCalledTimes(0);
375
+ });
376
+ it("skips onBlur when resetting", () => {
377
+ const onBlur = jest.fn();
378
+ // Given an author listening for blur
379
+ const a1 = createAuthorInputState({ books: [{}] }, onBlur);
380
+ // And we called onBlur once
381
+ a1.set({ firstName: "first" });
382
+ expect(onBlur).toBeCalledTimes(1);
383
+ // When we reset
384
+ a1.reset();
385
+ // We don't call blur again
386
+ expect(onBlur).toBeCalledTimes(1);
387
+ });
388
+ it("skips onBlur when not dirty", () => {
389
+ const onBlur = jest.fn();
390
+ // Given an author listening for blur
391
+ const a1 = createAuthorInputState({ firstName: "first", books: [{}] }, onBlur);
392
+ // When we programmatically set a field to it's existing valued
393
+ a1.firstName.value = "first";
394
+ // Then we don't call onBlur
395
+ expect(onBlur).toBeCalledTimes(0);
396
+ });
367
397
  it("knows list of primitives are dirty", () => {
368
398
  const a1 = formState_1.createObjectState({ favoriteColors: { type: "value" } }, {});
369
399
  expect(a1.favoriteColors.dirty).toBeFalsy();
@@ -687,74 +717,6 @@ describe("formState", () => {
687
717
  }
688
718
  `);
689
719
  });
690
- it("can pick a value field", () => {
691
- const a = formState_1.pickFields(
692
- //
693
- { firstName: { type: "value" } }, { firstName: "a", b: "ignored" });
694
- expect(a).toMatchInlineSnapshot(`
695
- Object {
696
- "firstName": "a",
697
- }
698
- `);
699
- });
700
- it("can pick an unset object fields", () => {
701
- const a = formState_1.pickFields(authorWithAddressConfig, { firstName: "a", b: "ignored" });
702
- expect(a).toMatchInlineSnapshot(`
703
- Object {
704
- "address": undefined,
705
- "firstName": "a",
706
- "id": undefined,
707
- "lastName": undefined,
708
- }
709
- `);
710
- });
711
- it("can pick a set object field", () => {
712
- const a = formState_1.pickFields(authorWithAddressConfig, { firstName: "a", b: "ignored", address: {} });
713
- expect(a).toMatchInlineSnapshot(`
714
- Object {
715
- "address": Object {
716
- "city": undefined,
717
- "street": undefined,
718
- },
719
- "firstName": "a",
720
- "id": undefined,
721
- "lastName": undefined,
722
- }
723
- `);
724
- });
725
- it("can pick an unset list field", () => {
726
- const a = formState_1.pickFields(authorWithBooksConfig, { firstName: "a", b: "ignored" });
727
- expect(a).toMatchInlineSnapshot(`
728
- Object {
729
- "books": undefined,
730
- "firstName": "a",
731
- "id": undefined,
732
- "lastName": undefined,
733
- }
734
- `);
735
- });
736
- it("can pick a set list field", () => {
737
- const a = formState_1.pickFields(authorWithBooksConfig, { firstName: "a", b: "ignored", books: [{}] });
738
- expect(a).toMatchInlineSnapshot(`
739
- Object {
740
- "books": Array [
741
- Object {
742
- "classification": undefined,
743
- "id": undefined,
744
- "title": undefined,
745
- },
746
- ],
747
- "firstName": "a",
748
- "id": undefined,
749
- "lastName": undefined,
750
- }
751
- `);
752
- });
753
- it("can pick a set observable list field", () => {
754
- const books = mobx_1.observable([]);
755
- const a = formState_1.pickFields(authorWithBooksConfig, { firstName: "a", b: "ignored", books });
756
- expect(mobx_1.isObservable(a.books)).toEqual(true);
757
- });
758
720
  it("can wrap an existing list observable", () => {
759
721
  // Given an array observable that is already created
760
722
  const b1 = { title: "b1" };
@@ -1048,113 +1010,6 @@ describe("formState", () => {
1048
1010
  a.address.set({ street: "123", city: "nyc" });
1049
1011
  expect(a.value).toEqual({ address: { street: "123", city: "nyc" } });
1050
1012
  });
1051
- it("calls init.map if init.input is defined", async () => {
1052
- // Given a component
1053
- function TestComponent() {
1054
- const config = { firstName: { type: "value" } };
1055
- // And we have query data that may or may not be defined
1056
- const data = Math.random() >= 0 ? { firstName: "bob" } : undefined;
1057
- // Then the lambda is passed the "de-undefined" data
1058
- const form = formState_1.useFormState({
1059
- config,
1060
- init: { input: data, map: (d) => ({ firstName: d.firstName }) },
1061
- });
1062
- return jsx_runtime_1.jsx("div", { children: form.firstName.value }, void 0);
1063
- }
1064
- const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
1065
- expect(r.baseElement).toHaveTextContent("bob");
1066
- });
1067
- it("memoizes on init.input being an array", async () => {
1068
- const config = { firstName: { type: "value" } };
1069
- function TestComponent() {
1070
- const [, setTick] = react_1.useState(0);
1071
- const form = formState_1.useFormState({
1072
- config,
1073
- init: { input: ["a", "b"], map: ([a, b]) => ({ firstName: a + b }) },
1074
- });
1075
- const onClick = () => [form.firstName.set("fred"), setTick(1)];
1076
- return (jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsx("button", { "data-testid": "change", onClick: onClick }, void 0),
1077
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "firstName" }, { children: form.firstName.value }), void 0)] }, void 0));
1078
- }
1079
- const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
1080
- expect(r.firstName()).toHaveTextContent("ab");
1081
- rtl_utils_1.click(r.change);
1082
- expect(r.firstName()).toHaveTextContent("fred");
1083
- });
1084
- it("uses default if init.input is undefined", async () => {
1085
- // Given a component
1086
- function TestComponent() {
1087
- const config = { firstName: { type: "value" } };
1088
- // And we have query data that may or may not be defined (but is actually undefined)
1089
- const data = Math.random() >= 0 ? undefined : { firstName: "bob" };
1090
- const form = formState_1.useFormState({
1091
- config,
1092
- init: { input: data, map: (d) => ({ firstName: d.firstName }) },
1093
- });
1094
- return jsx_runtime_1.jsx("div", { children: form.firstName.value }, void 0);
1095
- }
1096
- const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
1097
- // Then we init.map wasn't called, and we used {} instead
1098
- expect(r.baseElement.textContent).toEqual("");
1099
- });
1100
- it("uses custom init.ifUndefined if init.input is undefined", async () => {
1101
- // Given a component
1102
- function TestComponent() {
1103
- const config = {
1104
- id: { type: "value" },
1105
- firstName: { type: "value" },
1106
- };
1107
- // And we have query data that may or may not be defined (but is actually undefined)
1108
- const data = Math.random() >= 0 ? undefined : { firstName: "bob" };
1109
- const form = formState_1.useFormState({
1110
- config,
1111
- // And we pass `ifUndefined`
1112
- init: {
1113
- input: data,
1114
- map: (d) => ({ firstName: d.firstName }),
1115
- ifUndefined: { firstName: "default" },
1116
- },
1117
- });
1118
- return (jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "firstName" }, { children: form.firstName.value }), void 0),
1119
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "changedValue" }, { children: JSON.stringify(form.changedValue) }), void 0)] }, void 0));
1120
- }
1121
- const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
1122
- // Then we use the ifUndefined value
1123
- expect(r.firstName.textContent).toEqual("default");
1124
- expect(r.changedValue.textContent).toEqual(JSON.stringify({ firstName: "default" }));
1125
- });
1126
- it("uses init if set as a value", async () => {
1127
- const config = { firstName: { type: "value" } };
1128
- function TestComponent() {
1129
- const [, setTick] = react_1.useState(0);
1130
- const form = formState_1.useFormState({
1131
- config,
1132
- // That's using a raw init value
1133
- init: { firstName: "bob" },
1134
- });
1135
- return (jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsx("button", { "data-testid": "change", onClick: () => {
1136
- // When that value changes
1137
- form.firstName.set("fred");
1138
- // And also we re-render the component
1139
- setTick(1);
1140
- } }, void 0),
1141
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "firstName" }, { children: form.firstName.value }), void 0)] }, void 0));
1142
- }
1143
- const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
1144
- expect(r.firstName()).toHaveTextContent("bob");
1145
- rtl_utils_1.click(r.change);
1146
- // Then the change didn't get dropped due to init being unstable
1147
- expect(r.firstName()).toHaveTextContent("fred");
1148
- });
1149
- it("doesn't required an init value", async () => {
1150
- function TestComponent() {
1151
- const config = { firstName: { type: "value" } };
1152
- const form = formState_1.useFormState({ config });
1153
- return jsx_runtime_1.jsx("div", { children: form.firstName.value }, void 0);
1154
- }
1155
- const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
1156
- expect(r.baseElement.textContent).toEqual("");
1157
- });
1158
1013
  it("provides isNewEntity", () => {
1159
1014
  // Given an author without an id
1160
1015
  const formState = formState_1.createObjectState(authorWithAddressConfig, {
@@ -1206,218 +1061,6 @@ describe("formState", () => {
1206
1061
  expect(formState.books.rows[1].isNewEntity).toBeTruthy();
1207
1062
  expect(formState.books.isNewEntity).toBeFalsy();
1208
1063
  });
1209
- it("keeps local changed values when a query refreshes", async () => {
1210
- // Given a component
1211
- function TestComponent() {
1212
- const config = authorWithAddressAndBooksConfig;
1213
- // And we have two sets of data
1214
- const data1 = {
1215
- id: "a:1",
1216
- firstName: "f1",
1217
- lastName: "l1",
1218
- address: { id: "address:1", street: "s1", city: "c1" },
1219
- books: [
1220
- { id: "b:1", title: "a1" },
1221
- { id: "b:2", title: "b1" },
1222
- ],
1223
- };
1224
- const data2 = {
1225
- id: "a:1",
1226
- firstName: "f2",
1227
- lastName: "l2",
1228
- address: { id: "address:1", street: "s2", city: "c2" },
1229
- books: [
1230
- { id: "b:1", title: "a2" },
1231
- { id: "b:2", title: "b2" },
1232
- { id: "b:3", title: "b3" },
1233
- ],
1234
- };
1235
- // Eventually our local values are saved
1236
- const data3 = {
1237
- id: "a:1",
1238
- firstName: "local",
1239
- lastName: "l2",
1240
- address: { id: "address:1", street: "local", city: "c2" },
1241
- books: [
1242
- { id: "b:1", title: "local" },
1243
- { id: "b:2", title: "b2" },
1244
- { id: "b:3", title: "b3" },
1245
- ],
1246
- };
1247
- // And we start out with data1
1248
- const [data, setData] = react_1.useState(data1);
1249
- const form = formState_1.useFormState({ config, init: { input: data, map: (d) => d } });
1250
- function makeLocalChanges() {
1251
- form.firstName.value = "local";
1252
- form.address.street.value = "local";
1253
- form.books.rows[0].title.value = "local";
1254
- }
1255
- 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),
1256
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "lastName" }, { children: form.lastName.value }), void 0),
1257
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "street" }, { children: form.address.street.value }), void 0),
1258
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "city" }, { children: form.address.city.value }), void 0),
1259
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "title1" }, { children: form.books.rows[0].title.value }), void 0),
1260
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "title2" }, { children: form.books.rows[1].title.value }), void 0),
1261
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "booksLength" }, { children: form.books.rows.length }), void 0),
1262
- jsx_runtime_1.jsx("button", { "data-testid": "makeLocalChanges", onClick: makeLocalChanges }, void 0),
1263
- jsx_runtime_1.jsx("button", { "data-testid": "refreshData", onClick: () => setData(data2) }, void 0),
1264
- jsx_runtime_1.jsx("button", { "data-testid": "saveData", onClick: () => setData(data3) }, void 0),
1265
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "changedValue" }, { children: JSON.stringify(form.changedValue) }), void 0)] }, void 0)) }, void 0));
1266
- }
1267
- // And we start out with the initial query data
1268
- const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
1269
- expect(r.firstName().textContent).toEqual("f1");
1270
- expect(r.street().textContent).toEqual("s1");
1271
- expect(r.title1().textContent).toEqual("a1");
1272
- // When we make some local changes
1273
- rtl_utils_1.click(r.makeLocalChanges);
1274
- // Then we see them
1275
- expect(r.firstName().textContent).toEqual("local");
1276
- expect(r.street().textContent).toEqual("local");
1277
- expect(r.title1().textContent).toEqual("local");
1278
- expect(JSON.parse(r.changedValue().textContent)).toEqual({
1279
- id: "a:1",
1280
- address: { id: "address:1", street: "local" },
1281
- books: [{ id: "b:1", title: "local" }, { id: "b:2" }],
1282
- firstName: "local",
1283
- });
1284
- // And when the new query is ran i.e. due to a cache refresh
1285
- rtl_utils_1.click(r.refreshData);
1286
- // Then we kept our local changes
1287
- expect(r.firstName().textContent).toEqual("local");
1288
- expect(r.street().textContent).toEqual("local");
1289
- expect(r.title1().textContent).toEqual("local");
1290
- // But we also see the new data for fields we have not changed
1291
- expect(r.lastName().textContent).toEqual("l2");
1292
- expect(r.city().textContent).toEqual("c2");
1293
- expect(r.title2().textContent).toEqual("b2");
1294
- expect(r.booksLength().textContent).toEqual("3");
1295
- expect(JSON.parse(r.changedValue().textContent)).toEqual({
1296
- id: "a:1",
1297
- address: { id: "address:1", street: "local" },
1298
- books: [{ id: "b:1", title: "local" }, { id: "b:2" }, { id: "b:3" }],
1299
- firstName: "local",
1300
- });
1301
- // And then when our mutation results come back
1302
- rtl_utils_1.click(r.saveData);
1303
- // Then changedValue doesn't show our local changes anymore
1304
- expect(JSON.parse(r.changedValue().textContent)).toEqual({
1305
- id: "a:1",
1306
- });
1307
- });
1308
- it("useFormState can accept new data while read only", async () => {
1309
- // Given a component
1310
- function TestComponent() {
1311
- const config = authorWithAddressAndBooksConfig;
1312
- // And we have two sets of data
1313
- const data1 = {
1314
- firstName: "f1",
1315
- address: { street: "s1" },
1316
- books: [{ title: "a1" }],
1317
- };
1318
- const data2 = {
1319
- firstName: "f2",
1320
- address: { street: "s2" },
1321
- books: [{ title: "a2" }],
1322
- };
1323
- // And we start out with data1
1324
- const [data, setData] = react_1.useState(data1);
1325
- const form = formState_1.useFormState({
1326
- config,
1327
- init: { input: data, map: (d) => d },
1328
- // And the form is read only
1329
- readOnly: true,
1330
- });
1331
- 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),
1332
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "street" }, { children: form.address.street.value }), void 0),
1333
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "title1" }, { children: form.books.rows[0].title.value }), void 0),
1334
- jsx_runtime_1.jsx("button", { "data-testid": "refreshData", onClick: () => setData(data2) }, void 0)] }, void 0)) }, void 0));
1335
- }
1336
- // And we start out with the initial query data
1337
- const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
1338
- expect(r.firstName().textContent).toEqual("f1");
1339
- expect(r.street().textContent).toEqual("s1");
1340
- expect(r.title1().textContent).toEqual("a1");
1341
- // When the new query is ran i.e. due to a cache refresh
1342
- rtl_utils_1.click(r.refreshData);
1343
- // Then we see the latest data
1344
- expect(r.firstName().textContent).toEqual("f2");
1345
- expect(r.street().textContent).toEqual("s2");
1346
- expect(r.title1().textContent).toEqual("a2");
1347
- });
1348
- it("useFormState can accept new data with computed fields", async () => {
1349
- // Given a component
1350
- function TestComponent() {
1351
- // And it's using a class/mobx proxy as the basis for the data
1352
- class AuthorRow {
1353
- constructor(firstName, lastName) {
1354
- this.firstName = firstName;
1355
- this.lastName = lastName;
1356
- }
1357
- get fullName() {
1358
- return this.firstName + " " + this.lastName;
1359
- }
1360
- }
1361
- // And we have two sets of data
1362
- const data1 = { firstName: "f1", lastName: "l1" };
1363
- const data2 = { firstName: "f2", lastName: "l2" };
1364
- // And we start out with data1
1365
- const [data, setData] = react_1.useState(data1);
1366
- const author = react_1.useMemo(() => new AuthorRow(data.firstName, data.lastName), [data]);
1367
- const config = react_1.useMemo(() => ({
1368
- firstName: { type: "value" },
1369
- lastName: { type: "value" },
1370
- fullName: { type: "value", computed: true },
1371
- }), []);
1372
- const form = formState_1.useFormState({ config, init: { input: author, map: (a) => a } });
1373
- 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),
1374
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "fullName" }, { children: form.fullName.value }), void 0),
1375
- jsx_runtime_1.jsx("button", { "data-testid": "refreshData", onClick: () => setData(data2) }, void 0)] }, void 0)) }, void 0));
1376
- }
1377
- // And we start out with the initial query data
1378
- const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
1379
- expect(r.firstName().textContent).toEqual("f1");
1380
- expect(r.fullName().textContent).toEqual("f1 l1");
1381
- // When the new query is ran i.e. due to a cache refresh
1382
- rtl_utils_1.click(r.refreshData);
1383
- expect(r.firstName().textContent).toEqual("f2");
1384
- expect(r.fullName().textContent).toEqual("f2 l2");
1385
- });
1386
- it("can trigger auto save for fields in list that were initially undefined", async () => {
1387
- const autoSave = jest.fn();
1388
- // Given a component
1389
- function TestComponent() {
1390
- // When the data is initially undefined
1391
- const [data, setData] = react_1.useState();
1392
- const data2 = { books: [{ title: "Title 1" }] };
1393
- const form = formState_1.useFormState({
1394
- config: authorWithBooksConfig,
1395
- init: { input: data, map: (d) => d, ifUndefined: { books: [] } },
1396
- autoSave,
1397
- });
1398
- 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),
1399
- jsx_runtime_1.jsx("button", { "data-testid": "add", onClick: () => form.books.add({ title: "New Book" }) }, void 0),
1400
- jsx_runtime_1.jsx("button", { "data-testid": "blurBookOne", onClick: () => form.books.rows[0].title.blur() }, void 0),
1401
- jsx_runtime_1.jsx("button", { "data-testid": "blurBookTwo", onClick: () => form.books.rows[1].title.blur() }, void 0)] }, void 0)) }, void 0));
1402
- }
1403
- // Given a formState with `onBlur` set
1404
- const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
1405
- // When the data/child is now available
1406
- rtl_utils_1.click(r.refreshData);
1407
- // And the field is blurred
1408
- rtl_utils_1.click(r.blurBookOne);
1409
- // Then we don't auto-save because nothing has changed
1410
- expect(autoSave).toBeCalledTimes(0);
1411
- // And when adding a new book
1412
- rtl_utils_1.click(r.add);
1413
- // We autoSave the new row right away (because we don't have any validation rules
1414
- // that say the new row can't be empty)
1415
- expect(autoSave).toBeCalledTimes(1);
1416
- // And the new book is blurred
1417
- rtl_utils_1.click(r.blurBookTwo);
1418
- // Then we auto save again
1419
- expect(autoSave).toBeCalledTimes(2);
1420
- });
1421
1064
  });
1422
1065
  class ObservableObject {
1423
1066
  constructor() {
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
- export { createObjectState, FieldState, ListFieldState, ObjectConfig, ObjectState, required, Rule, useFormState, } from "./formState";
1
+ export { createObjectState, FieldState, ListFieldState, ObjectConfig, ObjectState, required, Rule, } from "./formState";
2
+ export { useFormState } from "./useFormState";
package/dist/index.js CHANGED
@@ -4,4 +4,5 @@ exports.useFormState = exports.required = exports.createObjectState = void 0;
4
4
  var formState_1 = require("./formState");
5
5
  Object.defineProperty(exports, "createObjectState", { enumerable: true, get: function () { return formState_1.createObjectState; } });
6
6
  Object.defineProperty(exports, "required", { enumerable: true, get: function () { return formState_1.required; } });
7
- Object.defineProperty(exports, "useFormState", { enumerable: true, get: function () { return formState_1.useFormState; } });
7
+ var useFormState_1 = require("./useFormState");
8
+ Object.defineProperty(exports, "useFormState", { enumerable: true, get: function () { return useFormState_1.useFormState; } });
@@ -0,0 +1,57 @@
1
+ import { ObjectConfig, ObjectState } from "./formState";
2
+ export declare type UseFormStateOpts<T, I> = {
3
+ /** The form configuration, should be a module-level const or useMemo'd. */
4
+ config: ObjectConfig<T>;
5
+ /**
6
+ * Provides the form's initial value.
7
+ *
8
+ * User's can either:
9
+ *
10
+ * - Provide no initial value (don't set `init` at all)
11
+ * - Provide an initial value that already matches the form type `T` (i.e. set `init: data`)
12
+ * - Provide an initial value from an object that _almost_ matches the form type `T`,
13
+ * but needs to be mapped from it's input type `I` to the form type
14
+ * (i.e. set `init: { input: data, map: (data) => ...}`).
15
+ *
16
+ * The value of using the 3rd option is that: a) we internally `useMemo` on the identity of the
17
+ * `init.input` (i.e. a response from an Apollo hook) and don't require the `map` function
18
+ * to have a stable identity, and also b) we will null-check/undefined-check `init.input` and
19
+ * only call `init.map` if it's set, otherwise we'll use `init.ifDefined` or `{}`, saving you
20
+ * from having to null check within your `init.map` function.
21
+ */
22
+ init?: T | {
23
+ input: I;
24
+ map: (input: Exclude<I, null | undefined>) => T;
25
+ ifUndefined?: T;
26
+ };
27
+ /**
28
+ * A hook to add custom, cross-field validation rules that can be difficult to setup directly in the config DSL.
29
+ *
30
+ * Does not need to be stable/useMemo'd.
31
+ */
32
+ addRules?: (state: ObjectState<T>) => void;
33
+ /** Whether the form should be read only, when changed it won't re-create the whole form. */
34
+ readOnly?: boolean;
35
+ /**
36
+ * Fired when the form should auto-save, i.e. after a) blur and b) all fields are valid.
37
+ *
38
+ * Does not need to be stable/useMemo'd.
39
+ */
40
+ autoSave?: (state: ObjectState<T>) => void;
41
+ };
42
+ /**
43
+ * Creates a formState instance for editing in a form.
44
+ */
45
+ export declare function useFormState<T, I>(opts: UseFormStateOpts<T, I>): ObjectState<T>;
46
+ /**
47
+ * Picks ony fields out of `instance` that are in the `formConfig`.
48
+ *
49
+ * This is useful for creating a form input from a GraphQL query result,
50
+ * but ignoring extra data in the query that we don't want/need in the form.
51
+ *
52
+ * (Especially because the form.value will become our mutation's input type
53
+ * that goes on the wire, we don't want superfluous data in it.)
54
+ */
55
+ export declare function pickFields<T, I>(formConfig: ObjectConfig<T>, instance: I): {
56
+ [K in keyof T]: K extends keyof I ? I[K] : never;
57
+ };
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pickFields = exports.useFormState = void 0;
4
+ const is_plain_object_1 = require("is-plain-object");
5
+ const mobx_1 = require("mobx");
6
+ const react_1 = require("react");
7
+ const formState_1 = require("./formState");
8
+ const utils_1 = require("./utils");
9
+ let isAutoSaving = false;
10
+ /**
11
+ * Creates a formState instance for editing in a form.
12
+ */
13
+ function useFormState(opts) {
14
+ const { config, init, addRules, readOnly = false, autoSave } = opts;
15
+ // Use a ref so our memo'ized `onBlur` always see the latest value
16
+ const autoSaveRef = react_1.useRef(autoSave);
17
+ autoSaveRef.current = autoSave;
18
+ const firstRunRef = react_1.useRef(true);
19
+ // This is a little weird, but we need to know ahead of time, before the form useMemo, if we're working with classes/mobx proxies
20
+ const [firstInitValue] = react_1.useState(() => initValue(config, init));
21
+ const isWrappingMobxProxy = !is_plain_object_1.isPlainObject(firstInitValue);
22
+ // If they're using init.input, useMemo on it (and it might be an array), otherwise allow the identity of init be unstable
23
+ const dep = init && "input" in init && "map" in init ? (Array.isArray(init.input) ? init.input : [init.input]) : [];
24
+ const form = react_1.useMemo(() => {
25
+ function onBlur() {
26
+ // Don't use canSave() because we don't want to set touched for all of the field
27
+ if (autoSaveRef.current && form.dirty && form.valid && !isAutoSaving) {
28
+ try {
29
+ isAutoSaving = true;
30
+ autoSaveRef.current(form);
31
+ }
32
+ finally {
33
+ isAutoSaving = false;
34
+ }
35
+ }
36
+ }
37
+ const value = firstRunRef.current ? firstInitValue : initValue(config, init);
38
+ const form = formState_1.createObjectState(config, value, { onBlur });
39
+ form.readOnly = readOnly;
40
+ // The identity of `addRules` is not stable, but assume that it is for better UX.
41
+ // eslint-disable-next-line react-hooks/exhaustive-deps
42
+ (addRules || (() => { }))(form);
43
+ firstRunRef.current = true;
44
+ return form;
45
+ },
46
+ // For this useMemo, we (almost) never re-run so that we can have a stable `form` identity across query refreshes.
47
+ // eslint-disable-next-line react-hooks/exhaustive-deps
48
+ [config, ...(isWrappingMobxProxy ? dep : [])]);
49
+ // We use useEffect so that any mutations to the proxies, which will call `setState`s on any observers to
50
+ // queue their components' render), don't happen during our render, per https://fb.me/setstate-in-render.
51
+ react_1.useEffect(() => {
52
+ // Ignore the 1st run b/c our 1st useMemo already initialized `form` with the current `init` value.
53
+ // Also for mobx proxies, we recreate a new form-state every time init changes, so that our
54
+ // fields fundamentally pointing to the right proxy. So just defer to the ^ useMemo.
55
+ if (firstRunRef.current || isWrappingMobxProxy) {
56
+ firstRunRef.current = false;
57
+ return;
58
+ }
59
+ form.set(initValue(config, init), { refreshing: true });
60
+ }, [form, ...dep]);
61
+ // Use useEffect so that we don't touch the form.init proxy during a render
62
+ react_1.useEffect(() => {
63
+ form.readOnly = readOnly;
64
+ }, [form, readOnly]);
65
+ return form;
66
+ }
67
+ exports.useFormState = useFormState;
68
+ /** Introspects the `init` prop to see if has a `map` function/etc. and returns the form value. */
69
+ function initValue(config, init) {
70
+ const initValue = init && "input" in init && "map" in init
71
+ ? init.input
72
+ ? init.map(init.input)
73
+ : init.ifUndefined || {}
74
+ : init || {};
75
+ return pickFields(config, initValue);
76
+ }
77
+ /**
78
+ * Picks ony fields out of `instance` that are in the `formConfig`.
79
+ *
80
+ * This is useful for creating a form input from a GraphQL query result,
81
+ * but ignoring extra data in the query that we don't want/need in the form.
82
+ *
83
+ * (Especially because the form.value will become our mutation's input type
84
+ * that goes on the wire, we don't want superfluous data in it.)
85
+ */
86
+ function pickFields(formConfig, instance) {
87
+ // If the caller is using classes, i.e. with their own custom observable behavior, then just use as-is
88
+ if (!is_plain_object_1.isPlainObject(instance)) {
89
+ return instance;
90
+ }
91
+ return Object.fromEntries(Object.entries(formConfig).map(([key, _keyConfig]) => {
92
+ const keyConfig = _keyConfig;
93
+ const value = instance[key];
94
+ if (keyConfig.type === "object") {
95
+ if (value) {
96
+ return [key, pickFields(keyConfig.config, value)];
97
+ }
98
+ else {
99
+ return [key, value];
100
+ }
101
+ }
102
+ else if (keyConfig.type === "list") {
103
+ if (mobx_1.isObservable(value)) {
104
+ // If we hit an observable array, leave it as the existing proxy so the our
105
+ // ListFieldState will react to changes in the original array.
106
+ return [key, value];
107
+ }
108
+ else if (value) {
109
+ return [key, value.map((u) => pickFields(keyConfig.config, u))];
110
+ }
111
+ else {
112
+ return [key, value];
113
+ }
114
+ }
115
+ else if (keyConfig.type === "value") {
116
+ return [key, value];
117
+ }
118
+ else {
119
+ return utils_1.assertNever(keyConfig);
120
+ }
121
+ }));
122
+ }
123
+ exports.pickFields = pickFields;
@@ -0,0 +1 @@
1
+ export {};