@homebound/form-state 2.23.2 → 2.24.0
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/FormStateApp.js +7 -5
- package/dist/useFormState.d.ts +8 -2
- package/dist/useFormState.test.js +8 -30
- package/dist/useFormStates.d.ts +19 -0
- package/dist/useFormStates.js +15 -2
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +8 -4
- package/package.json +1 -1
package/dist/FormStateApp.js
CHANGED
|
@@ -9,11 +9,13 @@ function FormStateApp() {
|
|
|
9
9
|
config: formConfig,
|
|
10
10
|
// Simulate getting the initial form state back from a server call
|
|
11
11
|
init: {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
input: {
|
|
13
|
+
firstName: "a1",
|
|
14
|
+
books: [...Array(2)].map((_, i) => ({
|
|
15
|
+
title: `b${i}`,
|
|
16
|
+
classification: { number: `10${i + 1}`, category: `Test Category ${i}` },
|
|
17
|
+
})),
|
|
18
|
+
},
|
|
17
19
|
},
|
|
18
20
|
addRules(state) {
|
|
19
21
|
state.lastName.rules.push(() => {
|
package/dist/useFormState.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type Query<I> = {
|
|
|
7
7
|
};
|
|
8
8
|
export type InputAndMap<T, I> = {
|
|
9
9
|
input: I;
|
|
10
|
-
map
|
|
10
|
+
map?: (input: Exclude<I, null | undefined>) => T;
|
|
11
11
|
ifUndefined?: T;
|
|
12
12
|
};
|
|
13
13
|
export type QueryAndMap<T, I> = {
|
|
@@ -15,6 +15,12 @@ export type QueryAndMap<T, I> = {
|
|
|
15
15
|
map: (input: Exclude<I, null | undefined>) => T;
|
|
16
16
|
ifUndefined?: T;
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* The opts has for `useFormState`.
|
|
20
|
+
*
|
|
21
|
+
* @typeparam T the form type, which is usually as close as possible to your *GraphQL input*
|
|
22
|
+
* @typeparam I the *form input* type, which is usually the *GraphQL output* type, i.e. the type of the response from your GraphQL query
|
|
23
|
+
*/
|
|
18
24
|
export type UseFormStateOpts<T, I> = {
|
|
19
25
|
/** The form configuration, should be a module-level const or useMemo'd. */
|
|
20
26
|
config: ObjectConfig<T>;
|
|
@@ -35,7 +41,7 @@ export type UseFormStateOpts<T, I> = {
|
|
|
35
41
|
* only call `init.map` if it's set, otherwise we'll use `init.ifDefined` or `{}`, saving you
|
|
36
42
|
* from having to null check within your `init.map` function.
|
|
37
43
|
*/
|
|
38
|
-
init?:
|
|
44
|
+
init?: InputAndMap<T, I> | QueryAndMap<T, I>;
|
|
39
45
|
/**
|
|
40
46
|
* A hook to add custom, cross-field validation rules that can be difficult to setup directly in the config DSL.
|
|
41
47
|
*
|
|
@@ -83,28 +83,6 @@ describe("useFormState", () => {
|
|
|
83
83
|
expect(r.firstName.textContent).toEqual("default");
|
|
84
84
|
expect(r.changedValue.textContent).toEqual(JSON.stringify({ firstName: "default" }));
|
|
85
85
|
});
|
|
86
|
-
it("uses init if set as a value", async () => {
|
|
87
|
-
const config = { firstName: { type: "value" } };
|
|
88
|
-
function TestComponent() {
|
|
89
|
-
const [, setTick] = (0, react_2.useState)(0);
|
|
90
|
-
const form = (0, useFormState_1.useFormState)({
|
|
91
|
-
config,
|
|
92
|
-
// That's using a raw init value
|
|
93
|
-
init: { firstName: "bob" },
|
|
94
|
-
});
|
|
95
|
-
return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("button", { "data-testid": "change", onClick: () => {
|
|
96
|
-
// When that value changes
|
|
97
|
-
form.firstName.set("fred");
|
|
98
|
-
// And also we re-render the component
|
|
99
|
-
setTick(1);
|
|
100
|
-
} }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: form.firstName.value })] }));
|
|
101
|
-
}
|
|
102
|
-
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
103
|
-
expect(r.firstName).toHaveTextContent("bob");
|
|
104
|
-
(0, rtl_utils_1.click)(r.change);
|
|
105
|
-
// Then the change didn't get dropped due to init being unstable
|
|
106
|
-
expect(r.firstName).toHaveTextContent("fred");
|
|
107
|
-
});
|
|
108
86
|
it("doesn't required an init value", async () => {
|
|
109
87
|
function TestComponent() {
|
|
110
88
|
const config = { firstName: { type: "value" } };
|
|
@@ -154,7 +132,7 @@ describe("useFormState", () => {
|
|
|
154
132
|
};
|
|
155
133
|
// And we start out with data1
|
|
156
134
|
const [data, setData] = (0, react_2.useState)(data1);
|
|
157
|
-
const form = (0, useFormState_1.useFormState)({ config, init: { input: data
|
|
135
|
+
const form = (0, useFormState_1.useFormState)({ config, init: { input: data } });
|
|
158
136
|
function makeLocalChanges() {
|
|
159
137
|
form.firstName.value = "local";
|
|
160
138
|
form.address.street.value = "local";
|
|
@@ -222,7 +200,7 @@ describe("useFormState", () => {
|
|
|
222
200
|
const [data, setData] = (0, react_2.useState)(data1);
|
|
223
201
|
const form = (0, useFormState_1.useFormState)({
|
|
224
202
|
config,
|
|
225
|
-
init: { input: data
|
|
203
|
+
init: { input: data },
|
|
226
204
|
// And the form is read only
|
|
227
205
|
readOnly: true,
|
|
228
206
|
});
|
|
@@ -318,7 +296,7 @@ describe("useFormState", () => {
|
|
|
318
296
|
const data2 = { books: [{ title: "Title 1" }] };
|
|
319
297
|
const form = (0, useFormState_1.useFormState)({
|
|
320
298
|
config: authorWithBooksConfig,
|
|
321
|
-
init: { input: data,
|
|
299
|
+
init: { input: data, ifUndefined: { books: [] } },
|
|
322
300
|
autoSave,
|
|
323
301
|
});
|
|
324
302
|
return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("button", { "data-testid": "refreshData", onClick: () => setData(data2) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "add", onClick: () => form.books.add({ title: "New Book" }) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "blurBookOne", onClick: () => focusAndBlur(form.books.rows[0].title) }), (0, jsx_runtime_1.jsx)("button", { "data-testid": "blurBookTwo", onClick: () => focusAndBlur(form.books.rows[1].title) })] })) }));
|
|
@@ -349,7 +327,7 @@ describe("useFormState", () => {
|
|
|
349
327
|
const data = { firstName: "f1", lastName: "f1" };
|
|
350
328
|
const form = (0, useFormState_1.useFormState)({
|
|
351
329
|
config,
|
|
352
|
-
init: data,
|
|
330
|
+
init: { input: data },
|
|
353
331
|
// And there is reactive business logic in the `autoSave` method
|
|
354
332
|
async autoSave(state) {
|
|
355
333
|
state.lastName.set("l2");
|
|
@@ -372,7 +350,7 @@ describe("useFormState", () => {
|
|
|
372
350
|
const data = { firstName: "f1", lastName: "f1" };
|
|
373
351
|
const form = (0, useFormState_1.useFormState)({
|
|
374
352
|
config,
|
|
375
|
-
init: data,
|
|
353
|
+
init: { input: data },
|
|
376
354
|
autoSave: (form) => autoSave(form.changedValue),
|
|
377
355
|
});
|
|
378
356
|
return ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("button", { "data-testid": "set", onClick: () => {
|
|
@@ -453,7 +431,7 @@ describe("useFormState", () => {
|
|
|
453
431
|
(0, mobx_1.reaction)(() => fs.firstName.value, (curr) => (fs.lastName.value = curr));
|
|
454
432
|
},
|
|
455
433
|
autoSave: (fs) => autoSaveStub(fs.changedValue),
|
|
456
|
-
init: { id: "a:1" },
|
|
434
|
+
init: { input: { id: "a:1" } },
|
|
457
435
|
});
|
|
458
436
|
return (0, jsx_runtime_1.jsx)(FormStateApp_1.TextField, { field: fs.firstName });
|
|
459
437
|
}
|
|
@@ -482,7 +460,7 @@ describe("useFormState", () => {
|
|
|
482
460
|
it("sets loading if input.data is undefined", async () => {
|
|
483
461
|
const config = { firstName: { type: "value" } };
|
|
484
462
|
function TestComponent({ data }) {
|
|
485
|
-
const form = (0, useFormState_1.useFormState)({ config, init: { input: data
|
|
463
|
+
const form = (0, useFormState_1.useFormState)({ config, init: { input: data } });
|
|
486
464
|
return (0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => (0, jsx_runtime_1.jsx)("div", { "data-testid": "loading", children: String(form.loading) }) });
|
|
487
465
|
}
|
|
488
466
|
// And we initially pass in `init.input: undefined`
|
|
@@ -546,7 +524,7 @@ describe("useFormState", () => {
|
|
|
546
524
|
const config = authorConfig;
|
|
547
525
|
const form = (0, useFormState_1.useFormState)({
|
|
548
526
|
config,
|
|
549
|
-
init: { firstName: "f1", lastName: "f1" },
|
|
527
|
+
init: { input: { firstName: "f1", lastName: "f1" } },
|
|
550
528
|
autoSave: async (form) => {
|
|
551
529
|
autoSave(form.changedValue);
|
|
552
530
|
// And the autoSave functions erroneously calls commitChanges
|
package/dist/useFormStates.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { ObjectConfig } from "./config";
|
|
2
2
|
import { ObjectState } from "./fields/objectField";
|
|
3
3
|
export type ObjectStateCache<T, I> = Record<string, [ObjectState<T>, I]>;
|
|
4
|
+
/**
|
|
5
|
+
* The opts has for `useFormStates`.
|
|
6
|
+
*
|
|
7
|
+
* @typeparam T the form type, which is usually as close as possible to your *GraphQL input*
|
|
8
|
+
* @typeparam I the *form input* type, which is usually the *GraphQL output* type, i.e. the type of the response from your GraphQL query
|
|
9
|
+
*/
|
|
4
10
|
type UseFormStatesOpts<T, I> = {
|
|
5
11
|
/**
|
|
6
12
|
* The config to use for each form state.
|
|
@@ -45,5 +51,18 @@ type UseFormStatesHook<T, I> = {
|
|
|
45
51
|
readOnly?: boolean;
|
|
46
52
|
}) => ObjectState<T>;
|
|
47
53
|
};
|
|
54
|
+
/**
|
|
55
|
+
* A hook to manage many "mini-forms" on a single page, typically one form per row
|
|
56
|
+
* in a table.
|
|
57
|
+
*
|
|
58
|
+
* This hook basically provides the page/table with a cache, so each table row naively ask "what's
|
|
59
|
+
* the form state for this given row's data?" and get back a new-or-existing `ObjectState` instance
|
|
60
|
+
* that, if already existing, still has any of the user's WIP changes.
|
|
61
|
+
*
|
|
62
|
+
* Each mini-form/row can have its own autoSave calls, independent of the other rows.
|
|
63
|
+
*
|
|
64
|
+
* @typeparam T the form type, which is usually as close as possible to your *GraphQL input*
|
|
65
|
+
* @typeparam I the *form input* type, which is usually the *GraphQL output* type, i.e. the type of the response from your GraphQL query
|
|
66
|
+
*/
|
|
48
67
|
export declare function useFormStates<T, I = T>(opts: UseFormStatesOpts<T, I>): UseFormStatesHook<T, I>;
|
|
49
68
|
export {};
|
package/dist/useFormStates.js
CHANGED
|
@@ -4,6 +4,19 @@ exports.useFormStates = void 0;
|
|
|
4
4
|
const react_1 = require("react");
|
|
5
5
|
const objectField_1 = require("./fields/objectField");
|
|
6
6
|
const utils_1 = require("./utils");
|
|
7
|
+
/**
|
|
8
|
+
* A hook to manage many "mini-forms" on a single page, typically one form per row
|
|
9
|
+
* in a table.
|
|
10
|
+
*
|
|
11
|
+
* This hook basically provides the page/table with a cache, so each table row naively ask "what's
|
|
12
|
+
* the form state for this given row's data?" and get back a new-or-existing `ObjectState` instance
|
|
13
|
+
* that, if already existing, still has any of the user's WIP changes.
|
|
14
|
+
*
|
|
15
|
+
* Each mini-form/row can have its own autoSave calls, independent of the other rows.
|
|
16
|
+
*
|
|
17
|
+
* @typeparam T the form type, which is usually as close as possible to your *GraphQL input*
|
|
18
|
+
* @typeparam I the *form input* type, which is usually the *GraphQL output* type, i.e. the type of the response from your GraphQL query
|
|
19
|
+
*/
|
|
7
20
|
function useFormStates(opts) {
|
|
8
21
|
const { config, autoSave, getId, map, addRules, readOnly = false } = opts;
|
|
9
22
|
const objectStateCache = (0, react_1.useMemo)(() => ({}),
|
|
@@ -54,7 +67,7 @@ function useFormStates(opts) {
|
|
|
54
67
|
}
|
|
55
68
|
// If it didn't exist, then add to the cache.
|
|
56
69
|
if (!form) {
|
|
57
|
-
form = (0, objectField_1.createObjectState)(config, (0, utils_1.initValue)(config,
|
|
70
|
+
form = (0, objectField_1.createObjectState)(config, (0, utils_1.initValue)(config, { map, input }), {
|
|
58
71
|
maybeAutoSave: () => maybeAutoSave(form),
|
|
59
72
|
});
|
|
60
73
|
if (addRules) {
|
|
@@ -64,7 +77,7 @@ function useFormStates(opts) {
|
|
|
64
77
|
}
|
|
65
78
|
// If the source of truth changed, then update the existing state and return it.
|
|
66
79
|
if (existing && existing[1] !== input) {
|
|
67
|
-
form.set((0, utils_1.initValue)(config,
|
|
80
|
+
form.set((0, utils_1.initValue)(config, { map, input }), {
|
|
68
81
|
refreshing: true,
|
|
69
82
|
});
|
|
70
83
|
existing[1] = input;
|
package/dist/utils.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type OmitIf<Base, Condition> = Pick<Base, {
|
|
|
13
13
|
}[keyof Base]>;
|
|
14
14
|
export declare function fail(message?: string): never;
|
|
15
15
|
export declare function assertNever(x: never): never;
|
|
16
|
-
/** Introspects the `init` prop to see if has a `map` function/etc. and returns the form value. */
|
|
16
|
+
/** Introspects the `init` prop to see if it has a `map` function/etc. and returns the form value. */
|
|
17
17
|
export declare function initValue<T>(config: ObjectConfig<T>, init: any): T;
|
|
18
18
|
export declare function isInput<T, I>(init: UseFormStateOpts<T, I>["init"]): init is InputAndMap<T, I>;
|
|
19
19
|
export declare function isQuery<T, I>(init: UseFormStateOpts<T, I>["init"]): init is QueryAndMap<T, I>;
|
package/dist/utils.js
CHANGED
|
@@ -15,23 +15,27 @@ function assertNever(x) {
|
|
|
15
15
|
throw new Error("Unexpected object: " + x);
|
|
16
16
|
}
|
|
17
17
|
exports.assertNever = assertNever;
|
|
18
|
-
/** Introspects the `init` prop to see if has a `map` function/etc. and returns the form value. */
|
|
18
|
+
/** Introspects the `init` prop to see if it has a `map` function/etc. and returns the form value. */
|
|
19
19
|
function initValue(config, init) {
|
|
20
20
|
let value;
|
|
21
21
|
if (isInput(init)) {
|
|
22
|
-
value = init.input ? init.map(init.input) : init.ifUndefined;
|
|
22
|
+
value = init.input ? (init.map ? init.map(init.input) : init.input) : init.ifUndefined;
|
|
23
23
|
}
|
|
24
24
|
else if (isQuery(init)) {
|
|
25
25
|
value = init.query.data ? init.map(init.query.data) : init.ifUndefined;
|
|
26
26
|
}
|
|
27
|
+
else if (init === undefined) {
|
|
28
|
+
// allow completely undefined init
|
|
29
|
+
}
|
|
27
30
|
else {
|
|
28
|
-
|
|
31
|
+
throw new Error("init must have an input or query key");
|
|
29
32
|
}
|
|
33
|
+
// Given our form config, pick out only the subset of fields out of `value` (unless it's a mobx class)
|
|
30
34
|
return pickFields(config, value !== null && value !== void 0 ? value : {});
|
|
31
35
|
}
|
|
32
36
|
exports.initValue = initValue;
|
|
33
37
|
function isInput(init) {
|
|
34
|
-
return !!init && typeof init === "object" && "input" in init
|
|
38
|
+
return !!init && typeof init === "object" && "input" in init;
|
|
35
39
|
}
|
|
36
40
|
exports.isInput = isInput;
|
|
37
41
|
function isQuery(init) {
|