@homebound/form-state 2.2.7 → 2.2.11

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", () => {
@@ -117,6 +113,19 @@ describe("formState", () => {
117
113
  expect(state.books.rows[2].originalValue === b1).toEqual(true);
118
114
  expect(ticks).toEqual(3);
119
115
  });
116
+ it("list value can observe changes", () => {
117
+ const b1 = { title: "t1" };
118
+ const a1 = { firstName: "a1", books: [b1] };
119
+ const state = createAuthorInputState(a1);
120
+ let books;
121
+ let ticks = 0;
122
+ mobx_1.autorun(() => {
123
+ books = state.books.value;
124
+ ticks++;
125
+ });
126
+ state.books.add({ title: "t2" }, 0);
127
+ expect(ticks).toEqual(2);
128
+ });
120
129
  it("maintains unknown fields", () => {
121
130
  // Given the form is not directly editing id fields
122
131
  const config = {
@@ -321,7 +330,7 @@ describe("formState", () => {
321
330
  a1.books.remove(0);
322
331
  expect(a1.dirty).toBeFalsy();
323
332
  });
324
- it("it calls onBlur when adding or removing to a list", () => {
333
+ it("calls onBlur when adding or removing to a list", () => {
325
334
  const onBlur = jest.fn();
326
335
  const a1 = createAuthorInputState({ books: [] }, onBlur);
327
336
  expect(a1.books.dirty).toBeFalsy();
@@ -331,6 +340,60 @@ describe("formState", () => {
331
340
  expect(a1.dirty).toBeFalsy();
332
341
  expect(onBlur).toBeCalledTimes(2);
333
342
  });
343
+ it("calls onBlur when programmatically setting a value", () => {
344
+ const onBlur = jest.fn();
345
+ // Given an author listening for blur
346
+ const a1 = createAuthorInputState({ books: [{}] }, onBlur);
347
+ // When we programmatically set a field that isn't focused
348
+ a1.firstName.value = "first";
349
+ // Then we call onBlur
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
+ });
356
+ it("defers calling onBlur when setting a bound value", () => {
357
+ const onBlur = jest.fn();
358
+ // Given an author listening for blur
359
+ const a1 = createAuthorInputState({ books: [{}] }, onBlur);
360
+ // And the field is focused
361
+ a1.firstName.focus();
362
+ // When we we set the field
363
+ a1.firstName.value = "first";
364
+ // Then we don't call onBlur
365
+ expect(onBlur).toBeCalledTimes(0);
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
+ });
334
397
  it("knows list of primitives are dirty", () => {
335
398
  const a1 = formState_1.createObjectState({ favoriteColors: { type: "value" } }, {});
336
399
  expect(a1.favoriteColors.dirty).toBeFalsy();
@@ -654,74 +717,6 @@ describe("formState", () => {
654
717
  }
655
718
  `);
656
719
  });
657
- it("can pick a value field", () => {
658
- const a = formState_1.pickFields(
659
- //
660
- { firstName: { type: "value" } }, { firstName: "a", b: "ignored" });
661
- expect(a).toMatchInlineSnapshot(`
662
- Object {
663
- "firstName": "a",
664
- }
665
- `);
666
- });
667
- it("can pick an unset object fields", () => {
668
- const a = formState_1.pickFields(authorWithAddressConfig, { firstName: "a", b: "ignored" });
669
- expect(a).toMatchInlineSnapshot(`
670
- Object {
671
- "address": undefined,
672
- "firstName": "a",
673
- "id": undefined,
674
- "lastName": undefined,
675
- }
676
- `);
677
- });
678
- it("can pick a set object field", () => {
679
- const a = formState_1.pickFields(authorWithAddressConfig, { firstName: "a", b: "ignored", address: {} });
680
- expect(a).toMatchInlineSnapshot(`
681
- Object {
682
- "address": Object {
683
- "city": undefined,
684
- "street": undefined,
685
- },
686
- "firstName": "a",
687
- "id": undefined,
688
- "lastName": undefined,
689
- }
690
- `);
691
- });
692
- it("can pick an unset list field", () => {
693
- const a = formState_1.pickFields(authorWithBooksConfig, { firstName: "a", b: "ignored" });
694
- expect(a).toMatchInlineSnapshot(`
695
- Object {
696
- "books": undefined,
697
- "firstName": "a",
698
- "id": undefined,
699
- "lastName": undefined,
700
- }
701
- `);
702
- });
703
- it("can pick a set list field", () => {
704
- const a = formState_1.pickFields(authorWithBooksConfig, { firstName: "a", b: "ignored", books: [{}] });
705
- expect(a).toMatchInlineSnapshot(`
706
- Object {
707
- "books": Array [
708
- Object {
709
- "classification": undefined,
710
- "id": undefined,
711
- "title": undefined,
712
- },
713
- ],
714
- "firstName": "a",
715
- "id": undefined,
716
- "lastName": undefined,
717
- }
718
- `);
719
- });
720
- it("can pick a set observable list field", () => {
721
- const books = mobx_1.observable([]);
722
- const a = formState_1.pickFields(authorWithBooksConfig, { firstName: "a", b: "ignored", books });
723
- expect(mobx_1.isObservable(a.books)).toEqual(true);
724
- });
725
720
  it("can wrap an existing list observable", () => {
726
721
  // Given an array observable that is already created
727
722
  const b1 = { title: "b1" };
@@ -931,7 +926,7 @@ describe("formState", () => {
931
926
  // And they have wip edits
932
927
  formState.firstName.set("ff");
933
928
  // When a mutation result sets both firstName and lastName
934
- formState.set({ firstName: "f2", lastName: "l2" });
929
+ formState.set({ firstName: "f2", lastName: "l2" }, { refreshing: true });
935
930
  // Then we don't overwrite the user's WIP work
936
931
  expect(formState.firstName.value).toEqual("ff");
937
932
  expect(formState.lastName.value).toEqual("l2");
@@ -944,13 +939,14 @@ describe("formState", () => {
944
939
  // Given first name is focused, but has not changed
945
940
  formState.firstName.focus();
946
941
  // When a mutation result sets both firstName and lastName
947
- formState.set({ firstName: "f2", lastName: "l2" });
942
+ formState.set({ firstName: "f2", lastName: "l2" }, { refreshing: true });
948
943
  // Then we do update the value
949
944
  expect(formState.firstName.value).toEqual("f2");
950
945
  });
951
946
  it("trims string values on blur", () => {
952
947
  const formState = formState_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
953
948
  // Given the user is typing with spaces
949
+ formState.firstName.focus();
954
950
  formState.firstName.set("f ");
955
951
  // And we initially keep the space
956
952
  expect(formState.firstName.value).toEqual("f ");
@@ -962,6 +958,7 @@ describe("formState", () => {
962
958
  it("trims empty values to undefined on blur", () => {
963
959
  const formState = formState_1.createObjectState(authorWithBooksConfig, { firstName: "f", lastName: "l" });
964
960
  // Given the user is typing with only spaces
961
+ formState.firstName.focus();
965
962
  formState.firstName.set(" ");
966
963
  // And we initially keep the space
967
964
  expect(formState.firstName.value).toEqual(" ");
@@ -1013,113 +1010,6 @@ describe("formState", () => {
1013
1010
  a.address.set({ street: "123", city: "nyc" });
1014
1011
  expect(a.value).toEqual({ address: { street: "123", city: "nyc" } });
1015
1012
  });
1016
- it("calls init.map if init.input is defined", async () => {
1017
- // Given a component
1018
- function TestComponent() {
1019
- const config = { firstName: { type: "value" } };
1020
- // And we have query data that may or may not be defined
1021
- const data = Math.random() >= 0 ? { firstName: "bob" } : undefined;
1022
- // Then the lambda is passed the "de-undefined" data
1023
- const form = formState_1.useFormState({
1024
- config,
1025
- init: { input: data, map: (d) => ({ firstName: d.firstName }) },
1026
- });
1027
- return jsx_runtime_1.jsx("div", { children: form.firstName.value }, void 0);
1028
- }
1029
- const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
1030
- expect(r.baseElement).toHaveTextContent("bob");
1031
- });
1032
- it("memoizes on init.input being an array", async () => {
1033
- const config = { firstName: { type: "value" } };
1034
- function TestComponent() {
1035
- const [, setTick] = react_1.useState(0);
1036
- const form = formState_1.useFormState({
1037
- config,
1038
- init: { input: ["a", "b"], map: ([a, b]) => ({ firstName: a + b }) },
1039
- });
1040
- const onClick = () => [form.firstName.set("fred"), setTick(1)];
1041
- return (jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsx("button", { "data-testid": "change", onClick: onClick }, void 0),
1042
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "firstName" }, { children: form.firstName.value }), void 0)] }, void 0));
1043
- }
1044
- const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
1045
- expect(r.firstName()).toHaveTextContent("ab");
1046
- rtl_utils_1.click(r.change);
1047
- expect(r.firstName()).toHaveTextContent("fred");
1048
- });
1049
- it("uses default if init.input is undefined", async () => {
1050
- // Given a component
1051
- function TestComponent() {
1052
- const config = { firstName: { type: "value" } };
1053
- // And we have query data that may or may not be defined (but is actually undefined)
1054
- const data = Math.random() >= 0 ? undefined : { firstName: "bob" };
1055
- const form = formState_1.useFormState({
1056
- config,
1057
- init: { input: data, map: (d) => ({ firstName: d.firstName }) },
1058
- });
1059
- return jsx_runtime_1.jsx("div", { children: form.firstName.value }, void 0);
1060
- }
1061
- const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
1062
- // Then we init.map wasn't called, and we used {} instead
1063
- expect(r.baseElement.textContent).toEqual("");
1064
- });
1065
- it("uses custom init.ifUndefined if init.input is undefined", async () => {
1066
- // Given a component
1067
- function TestComponent() {
1068
- const config = {
1069
- id: { type: "value" },
1070
- firstName: { type: "value" },
1071
- };
1072
- // And we have query data that may or may not be defined (but is actually undefined)
1073
- const data = Math.random() >= 0 ? undefined : { firstName: "bob" };
1074
- const form = formState_1.useFormState({
1075
- config,
1076
- // And we pass `ifUndefined`
1077
- init: {
1078
- input: data,
1079
- map: (d) => ({ firstName: d.firstName }),
1080
- ifUndefined: { firstName: "default" },
1081
- },
1082
- });
1083
- return (jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "firstName" }, { children: form.firstName.value }), void 0),
1084
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "changedValue" }, { children: JSON.stringify(form.changedValue) }), void 0)] }, void 0));
1085
- }
1086
- const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
1087
- // Then we use the ifUndefined value
1088
- expect(r.firstName.textContent).toEqual("default");
1089
- expect(r.changedValue.textContent).toEqual(JSON.stringify({ firstName: "default" }));
1090
- });
1091
- it("uses init if set as a value", async () => {
1092
- const config = { firstName: { type: "value" } };
1093
- function TestComponent() {
1094
- const [, setTick] = react_1.useState(0);
1095
- const form = formState_1.useFormState({
1096
- config,
1097
- // That's using a raw init value
1098
- init: { firstName: "bob" },
1099
- });
1100
- return (jsx_runtime_1.jsxs("div", { children: [jsx_runtime_1.jsx("button", { "data-testid": "change", onClick: () => {
1101
- // When that value changes
1102
- form.firstName.set("fred");
1103
- // And also we re-render the component
1104
- setTick(1);
1105
- } }, void 0),
1106
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "firstName" }, { children: form.firstName.value }), void 0)] }, void 0));
1107
- }
1108
- const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
1109
- expect(r.firstName()).toHaveTextContent("bob");
1110
- rtl_utils_1.click(r.change);
1111
- // Then the change didn't get dropped due to init being unstable
1112
- expect(r.firstName()).toHaveTextContent("fred");
1113
- });
1114
- it("doesn't required an init value", async () => {
1115
- function TestComponent() {
1116
- const config = { firstName: { type: "value" } };
1117
- const form = formState_1.useFormState({ config });
1118
- return jsx_runtime_1.jsx("div", { children: form.firstName.value }, void 0);
1119
- }
1120
- const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
1121
- expect(r.baseElement.textContent).toEqual("");
1122
- });
1123
1013
  it("provides isNewEntity", () => {
1124
1014
  // Given an author without an id
1125
1015
  const formState = formState_1.createObjectState(authorWithAddressConfig, {
@@ -1171,218 +1061,6 @@ describe("formState", () => {
1171
1061
  expect(formState.books.rows[1].isNewEntity).toBeTruthy();
1172
1062
  expect(formState.books.isNewEntity).toBeFalsy();
1173
1063
  });
1174
- it("keeps local changed values when a query refreshes", async () => {
1175
- // Given a component
1176
- function TestComponent() {
1177
- const config = authorWithAddressAndBooksConfig;
1178
- // And we have two sets of data
1179
- const data1 = {
1180
- id: "a:1",
1181
- firstName: "f1",
1182
- lastName: "l1",
1183
- address: { id: "address:1", street: "s1", city: "c1" },
1184
- books: [
1185
- { id: "b:1", title: "a1" },
1186
- { id: "b:2", title: "b1" },
1187
- ],
1188
- };
1189
- const data2 = {
1190
- id: "a:1",
1191
- firstName: "f2",
1192
- lastName: "l2",
1193
- address: { id: "address:1", street: "s2", city: "c2" },
1194
- books: [
1195
- { id: "b:1", title: "a2" },
1196
- { id: "b:2", title: "b2" },
1197
- { id: "b:3", title: "b3" },
1198
- ],
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
- };
1212
- // And we start out with data1
1213
- const [data, setData] = react_1.useState(data1);
1214
- const form = formState_1.useFormState({ config, init: { input: data, map: (d) => d } });
1215
- function makeLocalChanges() {
1216
- form.firstName.value = "local";
1217
- form.address.street.value = "local";
1218
- form.books.rows[0].title.value = "local";
1219
- }
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),
1221
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "lastName" }, { children: form.lastName.value }), void 0),
1222
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "street" }, { children: form.address.street.value }), void 0),
1223
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "city" }, { children: form.address.city.value }), void 0),
1224
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "title1" }, { children: form.books.rows[0].title.value }), void 0),
1225
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "title2" }, { children: form.books.rows[1].title.value }), void 0),
1226
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "booksLength" }, { children: form.books.rows.length }), void 0),
1227
- jsx_runtime_1.jsx("button", { "data-testid": "makeLocalChanges", onClick: makeLocalChanges }, 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),
1230
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "changedValue" }, { children: JSON.stringify(form.changedValue) }), void 0)] }, void 0)) }, void 0));
1231
- }
1232
- // And we start out with the initial query data
1233
- const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
1234
- expect(r.firstName().textContent).toEqual("f1");
1235
- expect(r.street().textContent).toEqual("s1");
1236
- expect(r.title1().textContent).toEqual("a1");
1237
- // When we make some local changes
1238
- rtl_utils_1.click(r.makeLocalChanges);
1239
- // Then we see them
1240
- expect(r.firstName().textContent).toEqual("local");
1241
- expect(r.street().textContent).toEqual("local");
1242
- expect(r.title1().textContent).toEqual("local");
1243
- expect(JSON.parse(r.changedValue().textContent)).toEqual({
1244
- id: "a:1",
1245
- address: { id: "address:1", street: "local" },
1246
- books: [{ id: "b:1", title: "local" }, { id: "b:2" }],
1247
- firstName: "local",
1248
- });
1249
- // And when the new query is ran i.e. due to a cache refresh
1250
- rtl_utils_1.click(r.refreshData);
1251
- // Then we kept our local changes
1252
- expect(r.firstName().textContent).toEqual("local");
1253
- expect(r.street().textContent).toEqual("local");
1254
- expect(r.title1().textContent).toEqual("local");
1255
- // But we also see the new data for fields we have not changed
1256
- expect(r.lastName().textContent).toEqual("l2");
1257
- expect(r.city().textContent).toEqual("c2");
1258
- expect(r.title2().textContent).toEqual("b2");
1259
- expect(r.booksLength().textContent).toEqual("3");
1260
- expect(JSON.parse(r.changedValue().textContent)).toEqual({
1261
- id: "a:1",
1262
- address: { id: "address:1", street: "local" },
1263
- books: [{ id: "b:1", title: "local" }, { id: "b:2" }, { id: "b:3" }],
1264
- firstName: "local",
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
- });
1272
- });
1273
- it("useFormState can accept new data while read only", async () => {
1274
- // Given a component
1275
- function TestComponent() {
1276
- const config = authorWithAddressAndBooksConfig;
1277
- // And we have two sets of data
1278
- const data1 = {
1279
- firstName: "f1",
1280
- address: { street: "s1" },
1281
- books: [{ title: "a1" }],
1282
- };
1283
- const data2 = {
1284
- firstName: "f2",
1285
- address: { street: "s2" },
1286
- books: [{ title: "a2" }],
1287
- };
1288
- // And we start out with data1
1289
- const [data, setData] = react_1.useState(data1);
1290
- const form = formState_1.useFormState({
1291
- config,
1292
- init: { input: data, map: (d) => d },
1293
- // And the form is read only
1294
- readOnly: true,
1295
- });
1296
- 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),
1297
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "street" }, { children: form.address.street.value }), void 0),
1298
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "title1" }, { children: form.books.rows[0].title.value }), void 0),
1299
- jsx_runtime_1.jsx("button", { "data-testid": "refreshData", onClick: () => setData(data2) }, void 0)] }, void 0)) }, void 0));
1300
- }
1301
- // And we start out with the initial query data
1302
- const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
1303
- expect(r.firstName().textContent).toEqual("f1");
1304
- expect(r.street().textContent).toEqual("s1");
1305
- expect(r.title1().textContent).toEqual("a1");
1306
- // When the new query is ran i.e. due to a cache refresh
1307
- rtl_utils_1.click(r.refreshData);
1308
- // Then we see the latest data
1309
- expect(r.firstName().textContent).toEqual("f2");
1310
- expect(r.street().textContent).toEqual("s2");
1311
- expect(r.title1().textContent).toEqual("a2");
1312
- });
1313
- it("useFormState can accept new data with computed fields", async () => {
1314
- // Given a component
1315
- function TestComponent() {
1316
- // And it's using a class/mobx proxy as the basis for the data
1317
- class AuthorRow {
1318
- constructor(firstName, lastName) {
1319
- this.firstName = firstName;
1320
- this.lastName = lastName;
1321
- }
1322
- get fullName() {
1323
- return this.firstName + " " + this.lastName;
1324
- }
1325
- }
1326
- // And we have two sets of data
1327
- const data1 = { firstName: "f1", lastName: "l1" };
1328
- const data2 = { firstName: "f2", lastName: "l2" };
1329
- // And we start out with data1
1330
- const [data, setData] = react_1.useState(data1);
1331
- const author = react_1.useMemo(() => new AuthorRow(data.firstName, data.lastName), [data]);
1332
- const config = react_1.useMemo(() => ({
1333
- firstName: { type: "value" },
1334
- lastName: { type: "value" },
1335
- fullName: { type: "value", computed: true },
1336
- }), []);
1337
- const form = formState_1.useFormState({ config, init: { input: author, map: (a) => a } });
1338
- 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),
1339
- jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "fullName" }, { children: form.fullName.value }), void 0),
1340
- jsx_runtime_1.jsx("button", { "data-testid": "refreshData", onClick: () => setData(data2) }, void 0)] }, void 0)) }, void 0));
1341
- }
1342
- // And we start out with the initial query data
1343
- const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
1344
- expect(r.firstName().textContent).toEqual("f1");
1345
- expect(r.fullName().textContent).toEqual("f1 l1");
1346
- // When the new query is ran i.e. due to a cache refresh
1347
- rtl_utils_1.click(r.refreshData);
1348
- expect(r.firstName().textContent).toEqual("f2");
1349
- expect(r.fullName().textContent).toEqual("f2 l2");
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
- });
1386
1064
  });
1387
1065
  class ObservableObject {
1388
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
+ };