@homebound/form-state 2.9.0 → 2.10.1
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/config.d.ts +66 -0
- package/dist/config.js +2 -0
- package/dist/fields/listField.d.ts +11 -0
- package/dist/fields/listField.js +205 -0
- package/dist/fields/objectField.d.ts +54 -0
- package/dist/fields/objectField.js +180 -0
- package/dist/fields/valueField.d.ts +58 -0
- package/dist/fields/valueField.js +149 -0
- package/dist/formState.test.js +97 -60
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -3
- package/dist/rules.d.ts +14 -0
- package/dist/rules.js +11 -0
- package/dist/useFormState.d.ts +2 -1
- package/dist/useFormState.js +2 -2
- package/dist/useFormState.test.js +4 -4
- package/dist/useFormStates.d.ts +11 -3
- package/dist/useFormStates.js +33 -28
- package/dist/useFormStates.test.js +48 -10
- package/dist/utils.d.ts +24 -1
- package/dist/utils.js +41 -1
- package/dist/utils.test.js +3 -3
- package/package.json +1 -1
- package/dist/formState.d.ts +0 -192
- package/dist/formState.js +0 -573
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.useFormStates = exports.useFormState = exports.required = exports.createObjectState = void 0;
|
|
4
|
-
var
|
|
5
|
-
Object.defineProperty(exports, "createObjectState", { enumerable: true, get: function () { return
|
|
6
|
-
|
|
4
|
+
var objectField_1 = require("./fields/objectField");
|
|
5
|
+
Object.defineProperty(exports, "createObjectState", { enumerable: true, get: function () { return objectField_1.createObjectState; } });
|
|
6
|
+
var rules_1 = require("./rules");
|
|
7
|
+
Object.defineProperty(exports, "required", { enumerable: true, get: function () { return rules_1.required; } });
|
|
7
8
|
var useFormState_1 = require("./useFormState");
|
|
8
9
|
Object.defineProperty(exports, "useFormState", { enumerable: true, get: function () { return useFormState_1.useFormState; } });
|
|
9
10
|
var useFormStates_1 = require("./useFormStates");
|
package/dist/rules.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ObjectState } from "./fields/objectField";
|
|
2
|
+
declare type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N;
|
|
3
|
+
/** A validation rule, given the value and name, return the error string if valid, or undefined if valid. */
|
|
4
|
+
export declare type Rule<T, V> = (opts: {
|
|
5
|
+
value: V;
|
|
6
|
+
key: string;
|
|
7
|
+
originalValue: V;
|
|
8
|
+
object: IfAny<T, any, ObjectState<T>>;
|
|
9
|
+
}) => string | undefined;
|
|
10
|
+
/** A rule that validates `value` is not `undefined`, `null`, or empty string. */
|
|
11
|
+
export declare const required: <V>({ value: v }: {
|
|
12
|
+
value: V;
|
|
13
|
+
}) => string | undefined;
|
|
14
|
+
export {};
|
package/dist/rules.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.required = void 0;
|
|
4
|
+
const mobx_1 = require("mobx");
|
|
5
|
+
/** A rule that validates `value` is not `undefined`, `null`, or empty string. */
|
|
6
|
+
// We pre-emptively make this a mobx action so that it's identity doesn't change when proxied
|
|
7
|
+
// and breaks our ability to do `rules.some(r => r === required)`.
|
|
8
|
+
exports.required = mobx_1.action(({ value: v }) => {
|
|
9
|
+
const isEmptyString = typeof v === "string" ? v.trim() === "" : false;
|
|
10
|
+
return v !== undefined && v !== null && !isEmptyString ? undefined : "Required";
|
|
11
|
+
});
|
package/dist/useFormState.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { ObjectConfig
|
|
1
|
+
import { ObjectConfig } from "./config";
|
|
2
|
+
import { ObjectState } from "./fields/objectField";
|
|
2
3
|
export declare type UseFormStateOpts<T, I> = {
|
|
3
4
|
/** The form configuration, should be a module-level const or useMemo'd. */
|
|
4
5
|
config: ObjectConfig<T>;
|
package/dist/useFormState.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.useFormState = void 0;
|
|
4
4
|
const is_plain_object_1 = require("is-plain-object");
|
|
5
5
|
const react_1 = require("react");
|
|
6
|
-
const
|
|
6
|
+
const objectField_1 = require("./fields/objectField");
|
|
7
7
|
const utils_1 = require("./utils");
|
|
8
8
|
// If the user's autoSave hook makes some last-minute `.set` calls to sneak
|
|
9
9
|
// in some business logic right before their GraphQL mutation call, ignore it
|
|
@@ -60,7 +60,7 @@ function useFormState(opts) {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
const value = firstRunRef.current ? firstInitValue : utils_1.initValue(config, init);
|
|
63
|
-
const form =
|
|
63
|
+
const form = objectField_1.createObjectState(config, value, { maybeAutoSave });
|
|
64
64
|
form.readOnly = readOnly;
|
|
65
65
|
// The identity of `addRules` is not stable, but assume that it is for better UX.
|
|
66
66
|
(addRules || (() => { }))(form);
|
|
@@ -7,7 +7,7 @@ const mobx_1 = require("mobx");
|
|
|
7
7
|
const mobx_react_1 = require("mobx-react");
|
|
8
8
|
const react_2 = require("react");
|
|
9
9
|
const FormStateApp_1 = require("./FormStateApp");
|
|
10
|
-
const
|
|
10
|
+
const rules_1 = require("./rules");
|
|
11
11
|
const useFormState_1 = require("./useFormState");
|
|
12
12
|
describe("useFormState", () => {
|
|
13
13
|
it("calls init.map if init.input is defined", async () => {
|
|
@@ -468,7 +468,7 @@ const authorWithBooksConfig = {
|
|
|
468
468
|
type: "list",
|
|
469
469
|
config: {
|
|
470
470
|
id: { type: "value" },
|
|
471
|
-
title: { type: "value", rules: [
|
|
471
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
472
472
|
classification: { type: "value" },
|
|
473
473
|
},
|
|
474
474
|
},
|
|
@@ -481,7 +481,7 @@ const authorWithAddressAndBooksConfig = {
|
|
|
481
481
|
type: "object",
|
|
482
482
|
config: {
|
|
483
483
|
id: { type: "value" },
|
|
484
|
-
street: { type: "value", rules: [
|
|
484
|
+
street: { type: "value", rules: [rules_1.required] },
|
|
485
485
|
city: { type: "value" },
|
|
486
486
|
},
|
|
487
487
|
},
|
|
@@ -489,7 +489,7 @@ const authorWithAddressAndBooksConfig = {
|
|
|
489
489
|
type: "list",
|
|
490
490
|
config: {
|
|
491
491
|
id: { type: "value" },
|
|
492
|
-
title: { type: "value", rules: [
|
|
492
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
493
493
|
classification: { type: "value" },
|
|
494
494
|
},
|
|
495
495
|
},
|
package/dist/useFormStates.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { ObjectConfig
|
|
1
|
+
import { ObjectConfig } from "./config";
|
|
2
|
+
import { ObjectState } from "./fields/objectField";
|
|
2
3
|
export declare type ObjectStateCache<T, I> = Record<string, [ObjectState<T>, I]>;
|
|
3
4
|
declare type UseFormStatesOpts<T, I> = {
|
|
4
5
|
/**
|
|
@@ -34,8 +35,15 @@ declare type UseFormStatesOpts<T, I> = {
|
|
|
34
35
|
* Does not need to be stable/useMemo'd.
|
|
35
36
|
*/
|
|
36
37
|
map?: (input: Exclude<I, null | undefined>) => T;
|
|
38
|
+
/**
|
|
39
|
+
* Sets all `ObjectState`s to readOnly.
|
|
40
|
+
*/
|
|
41
|
+
readOnly?: boolean;
|
|
37
42
|
};
|
|
38
|
-
|
|
39
|
-
getFormState: (input: I
|
|
43
|
+
declare type UseFormStatesHook<T, I> = {
|
|
44
|
+
getFormState: (input: I, opts?: {
|
|
45
|
+
readOnly?: boolean;
|
|
46
|
+
}) => ObjectState<T>;
|
|
40
47
|
};
|
|
48
|
+
export declare function useFormStates<T, I = T>(opts: UseFormStatesOpts<T, I>): UseFormStatesHook<T, I>;
|
|
41
49
|
export {};
|
package/dist/useFormStates.js
CHANGED
|
@@ -2,50 +2,54 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.useFormStates = void 0;
|
|
4
4
|
const react_1 = require("react");
|
|
5
|
-
const
|
|
5
|
+
const objectField_1 = require("./fields/objectField");
|
|
6
6
|
const utils_1 = require("./utils");
|
|
7
7
|
function useFormStates(opts) {
|
|
8
|
-
const { config, autoSave, getId, map, addRules } = opts;
|
|
8
|
+
const { config, autoSave, getId, map, addRules, readOnly = false } = opts;
|
|
9
9
|
const objectStateCache = react_1.useMemo(() => ({}),
|
|
10
10
|
// Force resetting the cache if config changes
|
|
11
11
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
12
12
|
[config]);
|
|
13
13
|
// Keep track of ObjectStates that triggered auto-save when a save was already in progress.
|
|
14
|
-
const pendingAutoSaves = react_1.useRef(
|
|
14
|
+
const pendingAutoSaves = react_1.useRef(new Set());
|
|
15
15
|
// Use a ref so our memo'ized `autoSave` always see the latest value
|
|
16
16
|
const autoSaveRef = react_1.useRef(autoSave);
|
|
17
17
|
autoSaveRef.current = autoSave;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
pendingAutoSaves.current.splice(i, 1);
|
|
18
|
+
// Use a ref b/c we're memod
|
|
19
|
+
const readOnlyRef = react_1.useRef(readOnly);
|
|
20
|
+
readOnlyRef.current = readOnly;
|
|
21
|
+
const getFormState = react_1.useCallback((input, opts = {}) => {
|
|
22
|
+
const existing = objectStateCache[getId(input)];
|
|
23
|
+
let form = existing === null || existing === void 0 ? void 0 : existing[0];
|
|
24
|
+
async function maybeAutoSave(form) {
|
|
25
|
+
// Don't use canSave() because we don't want to set touched for all the fields
|
|
26
|
+
if (autoSaveRef.current && form.dirty && form.valid) {
|
|
27
|
+
const { current: pending } = pendingAutoSaves;
|
|
28
|
+
if (isAutoSaving) {
|
|
29
|
+
pending.add(form);
|
|
30
|
+
return;
|
|
32
31
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
try {
|
|
33
|
+
isAutoSaving = true;
|
|
34
|
+
// See if we have any reactions that want to run (i.e. added by addRules hooks)
|
|
35
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
36
|
+
// If a reaction re-queued our form during the ^ wait, remove it
|
|
37
|
+
pending.delete(form);
|
|
38
|
+
await autoSaveRef.current(form);
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
isAutoSaving = false;
|
|
42
|
+
if (pending.size > 0) {
|
|
43
|
+
const first = pending.values().next().value;
|
|
44
|
+
pending.delete(first);
|
|
45
|
+
await maybeAutoSave(first);
|
|
46
|
+
}
|
|
39
47
|
}
|
|
40
48
|
}
|
|
41
49
|
}
|
|
42
|
-
}
|
|
43
|
-
const getFormState = react_1.useCallback((input) => {
|
|
44
|
-
const existing = objectStateCache[getId(input)];
|
|
45
|
-
let form = existing === null || existing === void 0 ? void 0 : existing[0];
|
|
46
50
|
// If it didn't exist, then add to the cache.
|
|
47
51
|
if (!form) {
|
|
48
|
-
form =
|
|
52
|
+
form = objectField_1.createObjectState(config, utils_1.initValue(config, map ? { map, input } : input), {
|
|
49
53
|
maybeAutoSave: () => maybeAutoSave(form),
|
|
50
54
|
});
|
|
51
55
|
if (addRules) {
|
|
@@ -58,6 +62,7 @@ function useFormStates(opts) {
|
|
|
58
62
|
form.set(utils_1.initValue(config, map ? { map, input } : input), { refreshing: true });
|
|
59
63
|
existing[1] = input;
|
|
60
64
|
}
|
|
65
|
+
form.readOnly = readOnlyRef.current || !!opts.readOnly;
|
|
61
66
|
return form;
|
|
62
67
|
},
|
|
63
68
|
// Allow the user to not stable-ize getId, map, addRules, and autoSave
|
|
@@ -4,8 +4,8 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
4
4
|
const rtl_utils_1 = require("@homebound/rtl-utils");
|
|
5
5
|
const mobx_1 = require("mobx");
|
|
6
6
|
const react_1 = require("react");
|
|
7
|
-
const formState_1 = require("./formState");
|
|
8
7
|
const FormStateApp_1 = require("./FormStateApp");
|
|
8
|
+
const rules_1 = require("./rules");
|
|
9
9
|
const useFormStates_1 = require("./useFormStates");
|
|
10
10
|
describe("useFormStates", () => {
|
|
11
11
|
it("can lazily create form states", async () => {
|
|
@@ -114,7 +114,7 @@ describe("useFormStates", () => {
|
|
|
114
114
|
const originalConfig = { id: { type: "value" }, firstName: { type: "value" } };
|
|
115
115
|
const updatedConfig = {
|
|
116
116
|
id: { type: "value" },
|
|
117
|
-
firstName: { type: "value", rules: [
|
|
117
|
+
firstName: { type: "value", rules: [rules_1.required] },
|
|
118
118
|
};
|
|
119
119
|
function TestComponent() {
|
|
120
120
|
const [config, setConfig] = react_1.useState(originalConfig);
|
|
@@ -148,7 +148,7 @@ describe("useFormStates", () => {
|
|
|
148
148
|
jsx_runtime_1.jsx(ChildComponent, { os: getFormState({ id: "a:1", firstName: "Brandon" }) }, void 0)] }, void 0));
|
|
149
149
|
}
|
|
150
150
|
// When we render
|
|
151
|
-
|
|
151
|
+
await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, {}, void 0));
|
|
152
152
|
// Then addRules was only called once
|
|
153
153
|
expect(addRules).toHaveBeenCalledTimes(1);
|
|
154
154
|
});
|
|
@@ -156,18 +156,12 @@ describe("useFormStates", () => {
|
|
|
156
156
|
// Given a user wants to use auto save
|
|
157
157
|
const autoSave = jest.fn();
|
|
158
158
|
function TestComponent() {
|
|
159
|
-
const config = {
|
|
160
|
-
id: { type: "value" },
|
|
161
|
-
firstName: { type: "value" },
|
|
162
|
-
lastName: { type: "value" },
|
|
163
|
-
};
|
|
164
159
|
const { getFormState } = useFormStates_1.useFormStates({
|
|
165
160
|
config,
|
|
166
161
|
getId: (o) => o.id,
|
|
167
162
|
addRules(fs) {
|
|
168
163
|
// And they have a reactive true that calculates last name
|
|
169
164
|
mobx_1.reaction(() => fs.firstName.value, (curr) => {
|
|
170
|
-
console.log({ curr });
|
|
171
165
|
fs.lastName.set(curr);
|
|
172
166
|
});
|
|
173
167
|
},
|
|
@@ -185,7 +179,51 @@ describe("useFormStates", () => {
|
|
|
185
179
|
expect(autoSave).toHaveBeenCalledTimes(1);
|
|
186
180
|
expect(autoSave).toHaveBeenCalledWith({ id: "a:1", firstName: "first", lastName: "first" });
|
|
187
181
|
});
|
|
182
|
+
it("can set readOnly via the hook opt", async () => {
|
|
183
|
+
// Given a test component
|
|
184
|
+
function TestComponent({ readOnly }) {
|
|
185
|
+
const { getFormState } = useFormStates_1.useFormStates({
|
|
186
|
+
config,
|
|
187
|
+
getId: (o) => o.id,
|
|
188
|
+
// And it passes readOnly directly to useFormStates
|
|
189
|
+
readOnly,
|
|
190
|
+
});
|
|
191
|
+
return jsx_runtime_1.jsx(ChildComponent, { os: getFormState({ id: "a:1", firstName: "Brandon" }) }, void 0);
|
|
192
|
+
}
|
|
193
|
+
// When we render
|
|
194
|
+
const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, { readOnly: true }, void 0));
|
|
195
|
+
// Then it's read only
|
|
196
|
+
expect(r.firstName()).toHaveAttribute("data-readonly", "true");
|
|
197
|
+
// And when we rerender
|
|
198
|
+
await r.rerender(jsx_runtime_1.jsx(TestComponent, { readOnly: false }, void 0));
|
|
199
|
+
// Then it's not read only
|
|
200
|
+
expect(r.firstName()).toHaveAttribute("data-readonly", "false");
|
|
201
|
+
});
|
|
202
|
+
it("can set readOnly via the getFormState function", async () => {
|
|
203
|
+
// Given a test component
|
|
204
|
+
function TestComponent({ readOnly }) {
|
|
205
|
+
const { getFormState } = useFormStates_1.useFormStates({
|
|
206
|
+
config,
|
|
207
|
+
getId: (o) => o.id,
|
|
208
|
+
});
|
|
209
|
+
// And it passes readOnly directly to getFormState
|
|
210
|
+
return jsx_runtime_1.jsx(ChildComponent, { os: getFormState({ id: "a:1", firstName: "Brandon" }, { readOnly }) }, void 0);
|
|
211
|
+
}
|
|
212
|
+
// When we render
|
|
213
|
+
const r = await rtl_utils_1.render(jsx_runtime_1.jsx(TestComponent, { readOnly: true }, void 0));
|
|
214
|
+
// Then it's read only
|
|
215
|
+
expect(r.firstName()).toHaveAttribute("data-readonly", "true");
|
|
216
|
+
// And when we rerender
|
|
217
|
+
await r.rerender(jsx_runtime_1.jsx(TestComponent, { readOnly: false }, void 0));
|
|
218
|
+
// Then it's not read only
|
|
219
|
+
expect(r.firstName()).toHaveAttribute("data-readonly", "false");
|
|
220
|
+
});
|
|
188
221
|
});
|
|
222
|
+
const config = {
|
|
223
|
+
id: { type: "value" },
|
|
224
|
+
firstName: { type: "value" },
|
|
225
|
+
lastName: { type: "value" },
|
|
226
|
+
};
|
|
189
227
|
function ChildComponent({ os }) {
|
|
190
|
-
return jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "firstName" }, { children: os.firstName.value }), void 0);
|
|
228
|
+
return (jsx_runtime_1.jsx("div", Object.assign({ "data-testid": "firstName", "data-readonly": os.firstName.readOnly }, { children: os.firstName.value }), void 0));
|
|
191
229
|
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import { ObjectConfig } from "./
|
|
1
|
+
import { ObjectConfig } from "./config";
|
|
2
|
+
export declare type Builtin = Date | Function | Uint8Array | string | number | boolean;
|
|
3
|
+
export declare type Primitive = undefined | null | boolean | string | number | Function | Date | {
|
|
4
|
+
toJSON(): any;
|
|
5
|
+
};
|
|
6
|
+
/** Makes the keys in `T` required while keeping the values undefined. */
|
|
7
|
+
export declare type DeepRequired<T> = T extends Primitive ? T : {
|
|
8
|
+
[P in keyof Required<T>]: T[P] extends Array<infer U> ? Array<DeepRequired<U>> : T[P] extends ReadonlyArray<infer U2> ? ReadonlyArray<DeepRequired<U2>> : DeepRequired<T[P]>;
|
|
9
|
+
};
|
|
2
10
|
export declare function fail(message?: string): never;
|
|
3
11
|
export declare function assertNever(x: never): never;
|
|
4
12
|
/** Introspects the `init` prop to see if has a `map` function/etc. and returns the form value. */
|
|
@@ -15,3 +23,18 @@ export declare function initValue<T>(config: ObjectConfig<T>, init: any): T;
|
|
|
15
23
|
export declare function pickFields<T, I>(formConfig: ObjectConfig<T>, instance: I): {
|
|
16
24
|
[K in keyof T]: K extends keyof I ? I[K] : never;
|
|
17
25
|
};
|
|
26
|
+
export declare function isNotUndefined<T>(value: T | undefined): value is T;
|
|
27
|
+
export declare function isEmpty(value: any): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* An equals that does deep-ish equality.
|
|
30
|
+
*
|
|
31
|
+
* We only do non-identity equals for:
|
|
32
|
+
*
|
|
33
|
+
* - "plain" objects that have no custom prototype/i.e. are object literals
|
|
34
|
+
* - objects that implement `toJSON`
|
|
35
|
+
*
|
|
36
|
+
*/
|
|
37
|
+
export declare function areEqual<T>(a?: T, b?: T): boolean;
|
|
38
|
+
export declare function hasToJSON(o?: unknown): o is {
|
|
39
|
+
toJSON(): void;
|
|
40
|
+
};
|
package/dist/utils.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.pickFields = exports.initValue = exports.assertNever = exports.fail = void 0;
|
|
6
|
+
exports.hasToJSON = exports.areEqual = exports.isEmpty = exports.isNotUndefined = exports.pickFields = exports.initValue = exports.assertNever = exports.fail = void 0;
|
|
7
|
+
const fast_deep_equal_1 = __importDefault(require("fast-deep-equal"));
|
|
4
8
|
const is_plain_object_1 = require("is-plain-object");
|
|
5
9
|
const mobx_1 = require("mobx");
|
|
6
10
|
function fail(message) {
|
|
@@ -68,3 +72,39 @@ function pickFields(formConfig, instance) {
|
|
|
68
72
|
}));
|
|
69
73
|
}
|
|
70
74
|
exports.pickFields = pickFields;
|
|
75
|
+
function isNotUndefined(value) {
|
|
76
|
+
return value !== undefined;
|
|
77
|
+
}
|
|
78
|
+
exports.isNotUndefined = isNotUndefined;
|
|
79
|
+
function isEmpty(value) {
|
|
80
|
+
return value === undefined || value === null || value === "";
|
|
81
|
+
}
|
|
82
|
+
exports.isEmpty = isEmpty;
|
|
83
|
+
/**
|
|
84
|
+
* An equals that does deep-ish equality.
|
|
85
|
+
*
|
|
86
|
+
* We only do non-identity equals for:
|
|
87
|
+
*
|
|
88
|
+
* - "plain" objects that have no custom prototype/i.e. are object literals
|
|
89
|
+
* - objects that implement `toJSON`
|
|
90
|
+
*
|
|
91
|
+
*/
|
|
92
|
+
function areEqual(a, b) {
|
|
93
|
+
if (is_plain_object_1.isPlainObject(a)) {
|
|
94
|
+
return fast_deep_equal_1.default(mobx_1.toJS(a), mobx_1.toJS(b));
|
|
95
|
+
}
|
|
96
|
+
if (hasToJSON(a) || hasToJSON(b)) {
|
|
97
|
+
const a1 = hasToJSON(a) ? a.toJSON() : a;
|
|
98
|
+
const b1 = hasToJSON(b) ? b.toJSON() : b;
|
|
99
|
+
return fast_deep_equal_1.default(a1, b1);
|
|
100
|
+
}
|
|
101
|
+
if (a && b && a instanceof Array && b instanceof Array) {
|
|
102
|
+
return fast_deep_equal_1.default(a, b);
|
|
103
|
+
}
|
|
104
|
+
return a === b;
|
|
105
|
+
}
|
|
106
|
+
exports.areEqual = areEqual;
|
|
107
|
+
function hasToJSON(o) {
|
|
108
|
+
return !!(o && typeof o === "object" && "toJSON" in o);
|
|
109
|
+
}
|
|
110
|
+
exports.hasToJSON = hasToJSON;
|
package/dist/utils.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const mobx_1 = require("mobx");
|
|
4
|
-
const
|
|
4
|
+
const rules_1 = require("./rules");
|
|
5
5
|
const utils_1 = require("./utils");
|
|
6
6
|
describe("utils", () => {
|
|
7
7
|
describe("pickFields", () => {
|
|
@@ -82,7 +82,7 @@ const authorWithAddressConfig = {
|
|
|
82
82
|
address: {
|
|
83
83
|
type: "object",
|
|
84
84
|
config: {
|
|
85
|
-
street: { type: "value", rules: [
|
|
85
|
+
street: { type: "value", rules: [rules_1.required] },
|
|
86
86
|
city: { type: "value" },
|
|
87
87
|
},
|
|
88
88
|
},
|
|
@@ -95,7 +95,7 @@ const authorWithBooksConfig = {
|
|
|
95
95
|
type: "list",
|
|
96
96
|
config: {
|
|
97
97
|
id: { type: "value" },
|
|
98
|
-
title: { type: "value", rules: [
|
|
98
|
+
title: { type: "value", rules: [rules_1.required] },
|
|
99
99
|
classification: { type: "value" },
|
|
100
100
|
},
|
|
101
101
|
},
|
package/package.json
CHANGED
package/dist/formState.d.ts
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wraps a given input/on-the-wire type `T` for editing in a form.
|
|
3
|
-
*
|
|
4
|
-
* We basically mimic every field in `T` (i.e. `firstName`, `lastName`, etc.) but decorate them
|
|
5
|
-
* with form-specific state like `touched`, `dirty`, `errors`, etc.
|
|
6
|
-
*
|
|
7
|
-
* The intent is that, after ensuring all fields are `valid`/etc., callers can take the
|
|
8
|
-
* result of this `objectState.value` (or `objectState.originalValue` for the non-proxy version) and
|
|
9
|
-
* have exactly the on-the-wire type `T` that they need to submit to the backend, without doing the
|
|
10
|
-
* manual mapping of "data that was in the form controls" into "data that the backend wants".
|
|
11
|
-
*
|
|
12
|
-
* Note that this can be hierarchical by having by having a field of `ListFieldState` that
|
|
13
|
-
* themselves each wrap an `ObjectState`, i.e.:
|
|
14
|
-
*
|
|
15
|
-
* ```
|
|
16
|
-
* ObjectState for author
|
|
17
|
-
* - firstName: FieldState
|
|
18
|
-
* - lastName: FieldState
|
|
19
|
-
* - rows: ListFieldState
|
|
20
|
-
* - [0]: ObjectState for book 1
|
|
21
|
-
* - title: FieldState
|
|
22
|
-
* - [1]: ObjectState for book 2
|
|
23
|
-
* - title: FieldState
|
|
24
|
-
* ```
|
|
25
|
-
*/
|
|
26
|
-
export declare type ObjectState<T, P = any> = FieldStates<T> & FieldState<P, T> & {
|
|
27
|
-
/** Sets the state of fields in `state`. */
|
|
28
|
-
set(state: Partial<T>, opts?: SetOpts): void;
|
|
29
|
-
/** Returns whether the object can be saved, i.e. is valid, but also as a side-effect marks touched. */
|
|
30
|
-
canSave(): boolean;
|
|
31
|
-
};
|
|
32
|
-
export declare type Builtin = Date | Function | Uint8Array | string | number | boolean;
|
|
33
|
-
/** For a given input type `T`, decorate each field into the "field state" type that holds our form-relevant state, i.e. valid/touched/etc. */
|
|
34
|
-
declare type FieldStates<T> = {
|
|
35
|
-
[K in keyof T]-?: T[K] extends Array<infer U> | null | undefined ? [U] extends [Builtin] ? FieldState<T, T[K]> : ListFieldState<T, U> : T[K] extends Builtin | null | undefined ? FieldState<T, T[K]> : ObjectState<T[K], T>;
|
|
36
|
-
};
|
|
37
|
-
declare type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N;
|
|
38
|
-
/** A validation rule, given the value and name, return the error string if valid, or undefined if valid. */
|
|
39
|
-
export declare type Rule<T, V> = (opts: {
|
|
40
|
-
value: V;
|
|
41
|
-
key: string;
|
|
42
|
-
originalValue: V;
|
|
43
|
-
object: IfAny<T, any, ObjectState<T>>;
|
|
44
|
-
}) => string | undefined;
|
|
45
|
-
/** A rule that validates `value` is not `undefined`, `null`, or empty string. */
|
|
46
|
-
export declare const required: <V>({ value: v }: {
|
|
47
|
-
value: V;
|
|
48
|
-
}) => string | undefined;
|
|
49
|
-
/**
|
|
50
|
-
* Form state for a primitive field in the form, i.e. its value but also touched/validation/etc. state.
|
|
51
|
-
*
|
|
52
|
-
* This API also provides hooks for form elements to call into, i.e. `blur()` and `set(...)` that will
|
|
53
|
-
* update the field state and re-render, i.e. when including in an `ObjectState`-typed literal that is
|
|
54
|
-
* an mobx `useLocalObservable`/observable.
|
|
55
|
-
*
|
|
56
|
-
* Note that `V` will always have `null | undefined` added to it by `FieldStates`, b/c most form fields
|
|
57
|
-
* i.e. text boxes, can always be cleared out/deleted.
|
|
58
|
-
*/
|
|
59
|
-
export interface FieldState<T, V> {
|
|
60
|
-
readonly key: string;
|
|
61
|
-
value: V;
|
|
62
|
-
readonly originalValue: V;
|
|
63
|
-
touched: boolean;
|
|
64
|
-
readOnly: boolean;
|
|
65
|
-
readonly required: boolean;
|
|
66
|
-
readonly dirty: boolean;
|
|
67
|
-
readonly valid: boolean;
|
|
68
|
-
readonly isNewEntity: boolean;
|
|
69
|
-
rules: Rule<T, V>[];
|
|
70
|
-
readonly errors: string[];
|
|
71
|
-
/** Returns a subset of V with only the changed values. Currently not observable. */
|
|
72
|
-
readonly changedValue: V;
|
|
73
|
-
/** Focuses the field. Disables changes from `ObjectState.set` calls. */
|
|
74
|
-
focus(): void;
|
|
75
|
-
/** Blur marks the field as touched and triggers an auto-save. */
|
|
76
|
-
blur(): void;
|
|
77
|
-
/** Triggers an auto-save; the caller (i.e. useFormState) should still dirty & valid check. */
|
|
78
|
-
maybeAutoSave(): void;
|
|
79
|
-
set(value: V, opts?: SetOpts): void;
|
|
80
|
-
/** Reverts back to the original value and resets dirty/touched. */
|
|
81
|
-
reset(): void;
|
|
82
|
-
/** Accepts the current changed value (if any) as the original and resets dirty/touched. */
|
|
83
|
-
save(): void;
|
|
84
|
-
}
|
|
85
|
-
/** Public options for our `set` command. */
|
|
86
|
-
export interface SetOpts {
|
|
87
|
-
/** Whether this `set` should trigger an auto-save; defaults to true. */
|
|
88
|
-
autoSave?: boolean;
|
|
89
|
-
}
|
|
90
|
-
/** Internal `.set` opts for conditionally that form-state internally percolates. */
|
|
91
|
-
interface InternalSetOpts extends SetOpts {
|
|
92
|
-
/** `resetting` is the code calling `.reset()` to revert to original values. */
|
|
93
|
-
resetting?: boolean;
|
|
94
|
-
/** `refreshing` is when `useFormState` sees a new value. */
|
|
95
|
-
refreshing?: boolean;
|
|
96
|
-
}
|
|
97
|
-
export interface FieldStateInternal<T, V> extends FieldState<T, V> {
|
|
98
|
-
set(value: V, opts?: InternalSetOpts): void;
|
|
99
|
-
_isIdKey: boolean;
|
|
100
|
-
_isDeleteKey: boolean;
|
|
101
|
-
_isReadOnlyKey: boolean;
|
|
102
|
-
_focused: boolean;
|
|
103
|
-
}
|
|
104
|
-
export declare type ObjectStateInternal<T, P = any> = ObjectState<T, P> & {
|
|
105
|
-
set(value: P, opts?: InternalSetOpts): void;
|
|
106
|
-
};
|
|
107
|
-
/** Form state for list of children, i.e. `U` is a `Book` in a form with a `books: Book[]`. */
|
|
108
|
-
export interface ListFieldState<T, U> extends Omit<FieldState<T, U[]>, "originalValue"> {
|
|
109
|
-
readonly rows: ReadonlyArray<ObjectState<U>>;
|
|
110
|
-
add(value: U, index?: number): void;
|
|
111
|
-
remove(indexOrValue: number | U): void;
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Config rules for each field in `T` that we're editing in a form.
|
|
115
|
-
*
|
|
116
|
-
* Basically every field is either a value/primitive or a list, and this `ObjectConfig` lets
|
|
117
|
-
* the caller define field-specific behavior, i.e. validation rules.
|
|
118
|
-
*/
|
|
119
|
-
export declare type ObjectConfig<T> = {
|
|
120
|
-
[P in keyof OmitIf<T, Function>]: T[P] extends Array<infer U> | null | undefined ? U extends Builtin ? ValueFieldConfig<T, T[P]> : ListFieldConfig<T, U> : ValueFieldConfig<T, T[P]> | ObjectFieldConfig<T[P]>;
|
|
121
|
-
};
|
|
122
|
-
declare type OmitIf<Base, Condition> = Pick<Base, {
|
|
123
|
-
[Key in keyof Base]: Base[Key] extends Condition ? never : Key;
|
|
124
|
-
}[keyof Base]>;
|
|
125
|
-
/** Field configuration for primitive values, i.e. strings/numbers/Dates/user-defined types. */
|
|
126
|
-
export declare type ValueFieldConfig<T, V> = {
|
|
127
|
-
type: "value";
|
|
128
|
-
rules?: Rule<T, V | null | undefined>[];
|
|
129
|
-
/**
|
|
130
|
-
* If true, marks this field as the id, which will be used for things like "always include in changedValue".
|
|
131
|
-
*
|
|
132
|
-
* Defaults to true for fields named `id`.
|
|
133
|
-
*/
|
|
134
|
-
isIdKey?: boolean;
|
|
135
|
-
/** If true, and this value is used on an entity in a list, the entity won't count towards the list validity. */
|
|
136
|
-
isDeleteKey?: boolean;
|
|
137
|
-
/** If true, the entity that contains this value will be treated as read only. */
|
|
138
|
-
isReadOnlyKey?: boolean;
|
|
139
|
-
/**
|
|
140
|
-
* Marks a field as being backed by a mobx class computed field.
|
|
141
|
-
*
|
|
142
|
-
* Note that it might still be settable (some computed have setters), but we do
|
|
143
|
-
* exclude from the `reset` operation, i.e. we assume resetting other non-computed fields
|
|
144
|
-
* will effectively reset this field as well.
|
|
145
|
-
*/
|
|
146
|
-
computed?: boolean;
|
|
147
|
-
/** Marks a field as being initiallyread-only, i.e. `field.readOnly = true/false` can change this default. */
|
|
148
|
-
readOnly?: boolean;
|
|
149
|
-
};
|
|
150
|
-
/** Field configuration for list values, i.e. `U` is `Book` in a form with `books: Book[]`. */
|
|
151
|
-
export declare type ListFieldConfig<T, U> = {
|
|
152
|
-
type: "list";
|
|
153
|
-
/** Rules that can run on the full list of children. */
|
|
154
|
-
rules?: Rule<T, readonly ObjectState<U>[]>[];
|
|
155
|
-
/** Config for each child's form state, i.e. each book. */
|
|
156
|
-
config: ObjectConfig<U>;
|
|
157
|
-
/**
|
|
158
|
-
* What the server-side update behavior is for this collection.
|
|
159
|
-
*
|
|
160
|
-
* When exhaustive, we include all rows in `changedValue` so that we
|
|
161
|
-
* don't orhan unchanged rows.
|
|
162
|
-
*
|
|
163
|
-
* When incremental, we only include changed rows in `changedValue`.
|
|
164
|
-
*
|
|
165
|
-
* Defaults to `exhaustive` b/c that is the safest and also Joist's
|
|
166
|
-
* default behavior.
|
|
167
|
-
*/
|
|
168
|
-
update?: "exhaustive" | "incremental";
|
|
169
|
-
};
|
|
170
|
-
export declare type ObjectFieldConfig<U> = {
|
|
171
|
-
type: "object";
|
|
172
|
-
/** Config for the child's form state, i.e. each book. */
|
|
173
|
-
config: ObjectConfig<U>;
|
|
174
|
-
};
|
|
175
|
-
/**
|
|
176
|
-
* Creates a new `ObjectState` for a given form object `T` given config rules in `config`.
|
|
177
|
-
*
|
|
178
|
-
* The returned `ObjectState` can be used in a mobx `useLocalObservable` to driven an
|
|
179
|
-
* interactive form that tracks the current valid/touched/etc. state of both each
|
|
180
|
-
* individual fields as well as the top-level form/object itself.
|
|
181
|
-
*/
|
|
182
|
-
export declare function createObjectState<T>(config: ObjectConfig<T>, instance: T, opts?: {
|
|
183
|
-
maybeAutoSave?: () => void;
|
|
184
|
-
}): ObjectState<T>;
|
|
185
|
-
declare type Primitive = undefined | null | boolean | string | number | Function | Date | {
|
|
186
|
-
toJSON(): any;
|
|
187
|
-
};
|
|
188
|
-
/** Makes the keys in `T` required while keeping the values undefined. */
|
|
189
|
-
export declare type DeepRequired<T> = T extends Primitive ? T : {
|
|
190
|
-
[P in keyof Required<T>]: T[P] extends Array<infer U> ? Array<DeepRequired<U>> : T[P] extends ReadonlyArray<infer U2> ? ReadonlyArray<DeepRequired<U2>> : DeepRequired<T[P]>;
|
|
191
|
-
};
|
|
192
|
-
export {};
|