@homebound/form-state
Advanced tools
Comparing version 2.23.2 to 2.24.0
@@ -12,7 +12,9 @@ "use strict"; | ||
init: { | ||
firstName: "a1", | ||
books: [...Array(2)].map((_, i) => ({ | ||
title: `b${i}`, | ||
classification: { number: `10${i + 1}`, category: `Test Category ${i}` }, | ||
})), | ||
input: { | ||
firstName: "a1", | ||
books: [...Array(2)].map((_, i) => ({ | ||
title: `b${i}`, | ||
classification: { number: `10${i + 1}`, category: `Test Category ${i}` }, | ||
})), | ||
}, | ||
}, | ||
@@ -19,0 +21,0 @@ addRules(state) { |
@@ -10,3 +10,3 @@ import { ObjectConfig } from "./config"; | ||
input: I; | ||
map: (input: Exclude<I, null | undefined>) => T; | ||
map?: (input: Exclude<I, null | undefined>) => T; | ||
ifUndefined?: T; | ||
@@ -19,2 +19,8 @@ }; | ||
}; | ||
/** | ||
* The opts has for `useFormState`. | ||
* | ||
* @typeparam T the form type, which is usually as close as possible to your *GraphQL input* | ||
* @typeparam I the *form input* type, which is usually the *GraphQL output* type, i.e. the type of the response from your GraphQL query | ||
*/ | ||
export type UseFormStateOpts<T, I> = { | ||
@@ -40,3 +46,3 @@ /** The form configuration, should be a module-level const or useMemo'd. */ | ||
*/ | ||
init?: T | InputAndMap<T, I> | QueryAndMap<T, I>; | ||
init?: InputAndMap<T, I> | QueryAndMap<T, I>; | ||
/** | ||
@@ -43,0 +49,0 @@ * A hook to add custom, cross-field validation rules that can be difficult to setup directly in the config DSL. |
@@ -86,24 +86,2 @@ "use strict"; | ||
}); | ||
it("uses init if set as a value", async () => { | ||
const config = { firstName: { type: "value" } }; | ||
function TestComponent() { | ||
const [, setTick] = (0, react_2.useState)(0); | ||
const form = (0, useFormState_1.useFormState)({ | ||
config, | ||
// That's using a raw init value | ||
init: { firstName: "bob" }, | ||
}); | ||
return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("button", { "data-testid": "change", onClick: () => { | ||
// When that value changes | ||
form.firstName.set("fred"); | ||
// And also we re-render the component | ||
setTick(1); | ||
} }), (0, jsx_runtime_1.jsx)("div", { "data-testid": "firstName", children: form.firstName.value })] })); | ||
} | ||
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {})); | ||
expect(r.firstName).toHaveTextContent("bob"); | ||
(0, rtl_utils_1.click)(r.change); | ||
// Then the change didn't get dropped due to init being unstable | ||
expect(r.firstName).toHaveTextContent("fred"); | ||
}); | ||
it("doesn't required an init value", async () => { | ||
@@ -158,3 +136,3 @@ function TestComponent() { | ||
const [data, setData] = (0, react_2.useState)(data1); | ||
const form = (0, useFormState_1.useFormState)({ config, init: { input: data, map: (d) => d } }); | ||
const form = (0, useFormState_1.useFormState)({ config, init: { input: data } }); | ||
function makeLocalChanges() { | ||
@@ -227,3 +205,3 @@ form.firstName.value = "local"; | ||
config, | ||
init: { input: data, map: (d) => d }, | ||
init: { input: data }, | ||
// And the form is read only | ||
@@ -324,3 +302,3 @@ readOnly: true, | ||
config: authorWithBooksConfig, | ||
init: { input: data, map: (d) => d, ifUndefined: { books: [] } }, | ||
init: { input: data, ifUndefined: { books: [] } }, | ||
autoSave, | ||
@@ -356,3 +334,3 @@ }); | ||
config, | ||
init: data, | ||
init: { input: data }, | ||
// And there is reactive business logic in the `autoSave` method | ||
@@ -380,3 +358,3 @@ async autoSave(state) { | ||
config, | ||
init: data, | ||
init: { input: data }, | ||
autoSave: (form) => autoSave(form.changedValue), | ||
@@ -462,3 +440,3 @@ }); | ||
autoSave: (fs) => autoSaveStub(fs.changedValue), | ||
init: { id: "a:1" }, | ||
init: { input: { id: "a:1" } }, | ||
}); | ||
@@ -492,3 +470,3 @@ return (0, jsx_runtime_1.jsx)(FormStateApp_1.TextField, { field: fs.firstName }); | ||
function TestComponent({ data }) { | ||
const form = (0, useFormState_1.useFormState)({ config, init: { input: data, map: (d) => d } }); | ||
const form = (0, useFormState_1.useFormState)({ config, init: { input: data } }); | ||
return (0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => (0, jsx_runtime_1.jsx)("div", { "data-testid": "loading", children: String(form.loading) }) }); | ||
@@ -557,3 +535,3 @@ } | ||
config, | ||
init: { firstName: "f1", lastName: "f1" }, | ||
init: { input: { firstName: "f1", lastName: "f1" } }, | ||
autoSave: async (form) => { | ||
@@ -560,0 +538,0 @@ autoSave(form.changedValue); |
import { ObjectConfig } from "./config"; | ||
import { ObjectState } from "./fields/objectField"; | ||
export type ObjectStateCache<T, I> = Record<string, [ObjectState<T>, I]>; | ||
/** | ||
* The opts has for `useFormStates`. | ||
* | ||
* @typeparam T the form type, which is usually as close as possible to your *GraphQL input* | ||
* @typeparam I the *form input* type, which is usually the *GraphQL output* type, i.e. the type of the response from your GraphQL query | ||
*/ | ||
type UseFormStatesOpts<T, I> = { | ||
@@ -48,3 +54,16 @@ /** | ||
}; | ||
/** | ||
* A hook to manage many "mini-forms" on a single page, typically one form per row | ||
* in a table. | ||
* | ||
* This hook basically provides the page/table with a cache, so each table row naively ask "what's | ||
* the form state for this given row's data?" and get back a new-or-existing `ObjectState` instance | ||
* that, if already existing, still has any of the user's WIP changes. | ||
* | ||
* Each mini-form/row can have its own autoSave calls, independent of the other rows. | ||
* | ||
* @typeparam T the form type, which is usually as close as possible to your *GraphQL input* | ||
* @typeparam I the *form input* type, which is usually the *GraphQL output* type, i.e. the type of the response from your GraphQL query | ||
*/ | ||
export declare function useFormStates<T, I = T>(opts: UseFormStatesOpts<T, I>): UseFormStatesHook<T, I>; | ||
export {}; |
@@ -7,2 +7,15 @@ "use strict"; | ||
const utils_1 = require("./utils"); | ||
/** | ||
* A hook to manage many "mini-forms" on a single page, typically one form per row | ||
* in a table. | ||
* | ||
* This hook basically provides the page/table with a cache, so each table row naively ask "what's | ||
* the form state for this given row's data?" and get back a new-or-existing `ObjectState` instance | ||
* that, if already existing, still has any of the user's WIP changes. | ||
* | ||
* Each mini-form/row can have its own autoSave calls, independent of the other rows. | ||
* | ||
* @typeparam T the form type, which is usually as close as possible to your *GraphQL input* | ||
* @typeparam I the *form input* type, which is usually the *GraphQL output* type, i.e. the type of the response from your GraphQL query | ||
*/ | ||
function useFormStates(opts) { | ||
@@ -58,3 +71,3 @@ const { config, autoSave, getId, map, addRules, readOnly = false } = opts; | ||
if (!form) { | ||
form = (0, objectField_1.createObjectState)(config, (0, utils_1.initValue)(config, map ? { map, input } : input), { | ||
form = (0, objectField_1.createObjectState)(config, (0, utils_1.initValue)(config, { map, input }), { | ||
maybeAutoSave: () => maybeAutoSave(form), | ||
@@ -69,3 +82,3 @@ }); | ||
if (existing && existing[1] !== input) { | ||
form.set((0, utils_1.initValue)(config, map ? { map, input } : input), { | ||
form.set((0, utils_1.initValue)(config, { map, input }), { | ||
refreshing: true, | ||
@@ -72,0 +85,0 @@ }); |
@@ -16,3 +16,3 @@ import { ObjectConfig } from "./config"; | ||
export declare function assertNever(x: never): never; | ||
/** Introspects the `init` prop to see if has a `map` function/etc. and returns the form value. */ | ||
/** Introspects the `init` prop to see if it has a `map` function/etc. and returns the form value. */ | ||
export declare function initValue<T>(config: ObjectConfig<T>, init: any): T; | ||
@@ -19,0 +19,0 @@ export declare function isInput<T, I>(init: UseFormStateOpts<T, I>["init"]): init is InputAndMap<T, I>; |
@@ -18,7 +18,7 @@ "use strict"; | ||
exports.assertNever = assertNever; | ||
/** Introspects the `init` prop to see if has a `map` function/etc. and returns the form value. */ | ||
/** Introspects the `init` prop to see if it has a `map` function/etc. and returns the form value. */ | ||
function initValue(config, init) { | ||
let value; | ||
if (isInput(init)) { | ||
value = init.input ? init.map(init.input) : init.ifUndefined; | ||
value = init.input ? (init.map ? init.map(init.input) : init.input) : init.ifUndefined; | ||
} | ||
@@ -28,5 +28,9 @@ else if (isQuery(init)) { | ||
} | ||
else if (init === undefined) { | ||
// allow completely undefined init | ||
} | ||
else { | ||
value = init; | ||
throw new Error("init must have an input or query key"); | ||
} | ||
// Given our form config, pick out only the subset of fields out of `value` (unless it's a mobx class) | ||
return pickFields(config, value !== null && value !== void 0 ? value : {}); | ||
@@ -36,3 +40,3 @@ } | ||
function isInput(init) { | ||
return !!init && typeof init === "object" && "input" in init && "map" in init; | ||
return !!init && typeof init === "object" && "input" in init; | ||
} | ||
@@ -39,0 +43,0 @@ exports.isInput = isInput; |
{ | ||
"name": "@homebound/form-state", | ||
"version": "2.23.2", | ||
"version": "2.24.0", | ||
"author": "Homebound", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
233897
4822