remix-validated-form 3.0.0-beta.1 → 3.2.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/.turbo/turbo-build.log +2 -2
- package/.turbo/turbo-test.log +10 -35
- package/README.md +41 -13
- package/browser/ValidatedForm.d.ts +6 -1
- package/browser/ValidatedForm.js +52 -1
- package/browser/hooks.d.ts +25 -1
- package/browser/hooks.js +44 -13
- package/browser/internal/MultiValueMap.d.ts +0 -0
- package/browser/internal/MultiValueMap.js +1 -0
- package/browser/internal/SingleTypeMultiValueMap.d.ts +8 -0
- package/browser/internal/SingleTypeMultiValueMap.js +40 -0
- package/browser/internal/formContext.d.ts +25 -1
- package/browser/internal/formContext.js +5 -0
- package/browser/internal/getInputProps.d.ts +28 -0
- package/browser/internal/getInputProps.js +38 -0
- package/{build/validation/validation.test.d.ts → browser/internal/test.d.ts} +0 -0
- package/browser/internal/test.js +10 -0
- package/browser/server.d.ts +1 -1
- package/browser/server.js +11 -1
- package/browser/validation/types.d.ts +2 -0
- package/build/ValidatedForm.d.ts +6 -1
- package/build/ValidatedForm.js +52 -1
- package/build/hooks.d.ts +25 -1
- package/build/hooks.js +43 -12
- package/build/internal/SingleTypeMultiValueMap.d.ts +8 -0
- package/build/internal/SingleTypeMultiValueMap.js +45 -0
- package/build/internal/formContext.d.ts +25 -1
- package/build/internal/formContext.js +5 -0
- package/build/internal/getInputProps.d.ts +28 -0
- package/build/internal/getInputProps.js +42 -0
- package/build/internal/test.d.ts +1 -0
- package/build/internal/test.js +12 -0
- package/build/server.d.ts +1 -1
- package/build/server.js +11 -1
- package/build/validation/types.d.ts +2 -0
- package/package.json +1 -3
- package/src/ValidatedForm.tsx +73 -0
- package/src/hooks.ts +77 -9
- package/src/internal/SingleTypeMultiValueMap.ts +37 -0
- package/src/internal/formContext.ts +30 -1
- package/src/internal/getInputProps.ts +79 -0
- package/src/server.ts +18 -2
- package/src/validation/types.ts +8 -0
- package/build/test-data/testFormData.d.ts +0 -15
- package/build/test-data/testFormData.js +0 -50
- package/build/validation/validation.test.js +0 -295
- package/build/validation/withYup.d.ts +0 -6
- package/build/validation/withYup.js +0 -44
- package/build/validation/withZod.d.ts +0 -6
- package/build/validation/withZod.js +0 -57
package/src/hooks.ts
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
import get from "lodash/get";
|
2
2
|
import toPath from "lodash/toPath";
|
3
|
-
import { useContext, useMemo } from "react";
|
3
|
+
import { useContext, useEffect, useMemo } from "react";
|
4
4
|
import { FormContext } from "./internal/formContext";
|
5
|
+
import {
|
6
|
+
createGetInputProps,
|
7
|
+
GetInputProps,
|
8
|
+
ValidationBehaviorOptions,
|
9
|
+
} from "./internal/getInputProps";
|
5
10
|
|
6
11
|
export type FieldProps = {
|
7
12
|
/**
|
@@ -20,17 +25,59 @@ export type FieldProps = {
|
|
20
25
|
* The default value of the field, if there is one.
|
21
26
|
*/
|
22
27
|
defaultValue?: any;
|
28
|
+
/**
|
29
|
+
* Whether or not the field has been touched.
|
30
|
+
*/
|
31
|
+
touched: boolean;
|
32
|
+
/**
|
33
|
+
* Helper to set the touched state of the field.
|
34
|
+
*/
|
35
|
+
setTouched: (touched: boolean) => void;
|
36
|
+
/**
|
37
|
+
* Helper to get all the props necessary for a regular input.
|
38
|
+
*/
|
39
|
+
getInputProps: GetInputProps;
|
23
40
|
};
|
24
41
|
|
25
42
|
/**
|
26
43
|
* Provides the data and helpers necessary to set up a field.
|
27
44
|
*/
|
28
|
-
export const useField = (
|
29
|
-
|
30
|
-
|
45
|
+
export const useField = (
|
46
|
+
name: string,
|
47
|
+
options?: {
|
48
|
+
/**
|
49
|
+
* Allows you to configure a custom function that will be called
|
50
|
+
* when the input needs to receive focus due to a validation error.
|
51
|
+
* This is useful for custom components that use a hidden input.
|
52
|
+
*/
|
53
|
+
handleReceiveFocus?: () => void;
|
54
|
+
/**
|
55
|
+
* Allows you to specify when a field gets validated (when using getInputProps)
|
56
|
+
*/
|
57
|
+
validationBehavior?: Partial<ValidationBehaviorOptions>;
|
58
|
+
}
|
59
|
+
): FieldProps => {
|
60
|
+
const {
|
61
|
+
fieldErrors,
|
62
|
+
clearError,
|
63
|
+
validateField,
|
64
|
+
defaultValues,
|
65
|
+
registerReceiveFocus,
|
66
|
+
touchedFields,
|
67
|
+
setFieldTouched,
|
68
|
+
hasBeenSubmitted,
|
69
|
+
} = useContext(FormContext);
|
70
|
+
|
71
|
+
const isTouched = !!touchedFields[name];
|
72
|
+
const { handleReceiveFocus } = options ?? {};
|
73
|
+
|
74
|
+
useEffect(() => {
|
75
|
+
if (handleReceiveFocus)
|
76
|
+
return registerReceiveFocus(name, handleReceiveFocus);
|
77
|
+
}, [handleReceiveFocus, name, registerReceiveFocus]);
|
31
78
|
|
32
|
-
const field = useMemo<FieldProps>(
|
33
|
-
|
79
|
+
const field = useMemo<FieldProps>(() => {
|
80
|
+
const helpers = {
|
34
81
|
error: fieldErrors[name],
|
35
82
|
clearError: () => {
|
36
83
|
clearError(name);
|
@@ -39,9 +86,30 @@ export const useField = (name: string): FieldProps => {
|
|
39
86
|
defaultValue: defaultValues
|
40
87
|
? get(defaultValues, toPath(name), undefined)
|
41
88
|
: undefined,
|
42
|
-
|
43
|
-
|
44
|
-
|
89
|
+
touched: isTouched,
|
90
|
+
setTouched: (touched: boolean) => setFieldTouched(name, touched),
|
91
|
+
};
|
92
|
+
const getInputProps = createGetInputProps({
|
93
|
+
...helpers,
|
94
|
+
name,
|
95
|
+
hasBeenSubmitted,
|
96
|
+
validationBehavior: options?.validationBehavior,
|
97
|
+
});
|
98
|
+
return {
|
99
|
+
...helpers,
|
100
|
+
getInputProps,
|
101
|
+
};
|
102
|
+
}, [
|
103
|
+
fieldErrors,
|
104
|
+
name,
|
105
|
+
defaultValues,
|
106
|
+
isTouched,
|
107
|
+
hasBeenSubmitted,
|
108
|
+
options?.validationBehavior,
|
109
|
+
clearError,
|
110
|
+
validateField,
|
111
|
+
setFieldTouched,
|
112
|
+
]);
|
45
113
|
|
46
114
|
return field;
|
47
115
|
};
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import { useRef } from "react";
|
2
|
+
|
3
|
+
export class MultiValueMap<Key, Value> {
|
4
|
+
private dict: Map<Key, Value[]> = new Map();
|
5
|
+
|
6
|
+
add = (key: Key, value: Value) => {
|
7
|
+
this.dict.set(key, [...(this.dict.get(key) ?? []), value]);
|
8
|
+
if (this.dict.has(key)) {
|
9
|
+
this.dict.get(key)!.push(value);
|
10
|
+
} else {
|
11
|
+
this.dict.set(key, [value]);
|
12
|
+
}
|
13
|
+
};
|
14
|
+
|
15
|
+
remove = (key: Key, value: Value) => {
|
16
|
+
if (!this.dict.has(key)) return;
|
17
|
+
const array = this.dict.get(key)!;
|
18
|
+
const index = array.indexOf(value);
|
19
|
+
if (index !== -1) array.splice(index, 1);
|
20
|
+
if (array.length === 0) this.dict.delete(key);
|
21
|
+
};
|
22
|
+
|
23
|
+
getAll = (key: Key): Value[] => {
|
24
|
+
return this.dict.get(key) ?? [];
|
25
|
+
};
|
26
|
+
|
27
|
+
has = (key: Key): boolean => this.dict.has(key);
|
28
|
+
}
|
29
|
+
|
30
|
+
export const useMultiValueMap = <Key, Value>() => {
|
31
|
+
const ref = useRef<MultiValueMap<Key, Value> | null>(null);
|
32
|
+
return () => {
|
33
|
+
if (ref.current) return ref.current;
|
34
|
+
ref.current = new MultiValueMap();
|
35
|
+
return ref.current;
|
36
|
+
};
|
37
|
+
};
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { createContext } from "react";
|
2
|
-
import { FieldErrors } from "../validation/types";
|
2
|
+
import { FieldErrors, TouchedFields } from "../validation/types";
|
3
3
|
|
4
4
|
export type FormContextValue = {
|
5
5
|
/**
|
@@ -22,10 +22,34 @@ export type FormContextValue = {
|
|
22
22
|
* Whether or not the form is submitting.
|
23
23
|
*/
|
24
24
|
isSubmitting: boolean;
|
25
|
+
/**
|
26
|
+
* Whether or not a submission has been attempted.
|
27
|
+
* This is true once the form has been submitted, even if there were validation errors.
|
28
|
+
* Resets to false when the form is reset.
|
29
|
+
*/
|
30
|
+
hasBeenSubmitted: boolean;
|
31
|
+
/**
|
32
|
+
* Whether or not the form is valid.
|
33
|
+
* This is a shortcut for `Object.keys(fieldErrors).length === 0`.
|
34
|
+
*/
|
35
|
+
isValid: boolean;
|
25
36
|
/**
|
26
37
|
* The default values of the form.
|
27
38
|
*/
|
28
39
|
defaultValues?: { [fieldName: string]: any };
|
40
|
+
/**
|
41
|
+
* Register a custom focus handler to be used when
|
42
|
+
* the field needs to receive focus due to a validation error.
|
43
|
+
*/
|
44
|
+
registerReceiveFocus: (fieldName: string, handler: () => void) => () => void;
|
45
|
+
/**
|
46
|
+
* Any fields that have been touched by the user.
|
47
|
+
*/
|
48
|
+
touchedFields: TouchedFields;
|
49
|
+
/**
|
50
|
+
* Change the touched state of the specified field.
|
51
|
+
*/
|
52
|
+
setFieldTouched: (fieldName: string, touched: boolean) => void;
|
29
53
|
};
|
30
54
|
|
31
55
|
export const FormContext = createContext<FormContextValue>({
|
@@ -33,4 +57,9 @@ export const FormContext = createContext<FormContextValue>({
|
|
33
57
|
clearError: () => {},
|
34
58
|
validateField: () => {},
|
35
59
|
isSubmitting: false,
|
60
|
+
hasBeenSubmitted: false,
|
61
|
+
isValid: true,
|
62
|
+
registerReceiveFocus: () => () => {},
|
63
|
+
touchedFields: {},
|
64
|
+
setFieldTouched: () => {},
|
36
65
|
});
|
@@ -0,0 +1,79 @@
|
|
1
|
+
export type ValidationBehavior = "onBlur" | "onChange" | "onSubmit";
|
2
|
+
|
3
|
+
export type ValidationBehaviorOptions = {
|
4
|
+
initial: ValidationBehavior;
|
5
|
+
whenTouched: ValidationBehavior;
|
6
|
+
whenSubmitted: ValidationBehavior;
|
7
|
+
};
|
8
|
+
|
9
|
+
export type CreateGetInputPropsOptions = {
|
10
|
+
clearError: () => void;
|
11
|
+
validate: () => void;
|
12
|
+
defaultValue?: any;
|
13
|
+
touched: boolean;
|
14
|
+
setTouched: (touched: boolean) => void;
|
15
|
+
hasBeenSubmitted: boolean;
|
16
|
+
validationBehavior?: Partial<ValidationBehaviorOptions>;
|
17
|
+
name: string;
|
18
|
+
};
|
19
|
+
|
20
|
+
export type MinimalInputProps = {
|
21
|
+
onChange?: (...args: any[]) => void;
|
22
|
+
onBlur?: (...args: any[]) => void;
|
23
|
+
};
|
24
|
+
|
25
|
+
export type MinimalResult = {
|
26
|
+
name: string;
|
27
|
+
onChange: (...args: any[]) => void;
|
28
|
+
onBlur: (...args: any[]) => void;
|
29
|
+
defaultValue?: any;
|
30
|
+
};
|
31
|
+
|
32
|
+
export type GetInputProps = <T extends {}>(
|
33
|
+
props?: T & MinimalInputProps
|
34
|
+
) => T & MinimalResult;
|
35
|
+
|
36
|
+
const defaultValidationBehavior: ValidationBehaviorOptions = {
|
37
|
+
initial: "onBlur",
|
38
|
+
whenTouched: "onChange",
|
39
|
+
whenSubmitted: "onChange",
|
40
|
+
};
|
41
|
+
|
42
|
+
export const createGetInputProps = ({
|
43
|
+
clearError,
|
44
|
+
validate,
|
45
|
+
defaultValue,
|
46
|
+
touched,
|
47
|
+
setTouched,
|
48
|
+
hasBeenSubmitted,
|
49
|
+
validationBehavior,
|
50
|
+
name,
|
51
|
+
}: CreateGetInputPropsOptions): GetInputProps => {
|
52
|
+
const validationBehaviors = {
|
53
|
+
...defaultValidationBehavior,
|
54
|
+
...validationBehavior,
|
55
|
+
};
|
56
|
+
|
57
|
+
return (props = {} as any) => {
|
58
|
+
const behavior = hasBeenSubmitted
|
59
|
+
? validationBehaviors.whenSubmitted
|
60
|
+
: touched
|
61
|
+
? validationBehaviors.whenTouched
|
62
|
+
: validationBehaviors.initial;
|
63
|
+
return {
|
64
|
+
...props,
|
65
|
+
onChange: (...args) => {
|
66
|
+
if (behavior === "onChange") validate();
|
67
|
+
else clearError();
|
68
|
+
return props?.onChange?.(...args);
|
69
|
+
},
|
70
|
+
onBlur: (...args) => {
|
71
|
+
if (behavior === "onBlur") validate();
|
72
|
+
setTouched(true);
|
73
|
+
return props?.onBlur?.(...args);
|
74
|
+
},
|
75
|
+
defaultValue,
|
76
|
+
name,
|
77
|
+
};
|
78
|
+
};
|
79
|
+
};
|
package/src/server.ts
CHANGED
@@ -6,5 +6,21 @@ import { FieldErrors } from "./validation/types";
|
|
6
6
|
* The `ValidatedForm` on the frontend will automatically display the errors
|
7
7
|
* if this is returned from the action.
|
8
8
|
*/
|
9
|
-
export const validationError = (
|
10
|
-
|
9
|
+
export const validationError = (
|
10
|
+
errors: FieldErrors,
|
11
|
+
submittedData?: unknown
|
12
|
+
) => {
|
13
|
+
if (submittedData) {
|
14
|
+
return json(
|
15
|
+
{
|
16
|
+
fieldErrors: {
|
17
|
+
...errors,
|
18
|
+
_submittedData: submittedData,
|
19
|
+
},
|
20
|
+
},
|
21
|
+
{ status: 422 }
|
22
|
+
);
|
23
|
+
}
|
24
|
+
|
25
|
+
return json({ fieldErrors: errors }, { status: 422 });
|
26
|
+
};
|
package/src/validation/types.ts
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
export type FieldErrors = Record<string, string>;
|
2
2
|
|
3
|
+
export type TouchedFields = Record<string, boolean>;
|
4
|
+
|
3
5
|
export type FieldErrorsWithData = FieldErrors & { _submittedData: any };
|
4
6
|
|
5
7
|
export type GenericObject = { [key: string]: any };
|
@@ -26,3 +28,9 @@ export type Validator<DataType> = {
|
|
26
28
|
field: string
|
27
29
|
) => ValidateFieldResult;
|
28
30
|
};
|
31
|
+
|
32
|
+
export type ValidatorData<T extends Validator<any>> = T extends Validator<
|
33
|
+
infer U
|
34
|
+
>
|
35
|
+
? U
|
36
|
+
: never;
|
@@ -1,15 +0,0 @@
|
|
1
|
-
export declare class TestFormData implements FormData {
|
2
|
-
private _params;
|
3
|
-
constructor(body?: string);
|
4
|
-
append(name: string, value: string | Blob, fileName?: string): void;
|
5
|
-
delete(name: string): void;
|
6
|
-
get(name: string): FormDataEntryValue | null;
|
7
|
-
getAll(name: string): FormDataEntryValue[];
|
8
|
-
has(name: string): boolean;
|
9
|
-
set(name: string, value: string | Blob, fileName?: string): void;
|
10
|
-
forEach(callbackfn: (value: FormDataEntryValue, key: string, parent: FormData) => void, thisArg?: any): void;
|
11
|
-
entries(): IterableIterator<[string, FormDataEntryValue]>;
|
12
|
-
keys(): IterableIterator<string>;
|
13
|
-
values(): IterableIterator<FormDataEntryValue>;
|
14
|
-
[Symbol.iterator](): IterableIterator<[string, FormDataEntryValue]>;
|
15
|
-
}
|
@@ -1,50 +0,0 @@
|
|
1
|
-
"use strict";
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
exports.TestFormData = void 0;
|
4
|
-
// Copied from remix to use in tests
|
5
|
-
// https://github.com/remix-run/remix/blob/a69a631cb5add72d5fb24211ab2a0be367b6f2fd/packages/remix-node/form-data.ts
|
6
|
-
class TestFormData {
|
7
|
-
constructor(body) {
|
8
|
-
this._params = new URLSearchParams(body);
|
9
|
-
}
|
10
|
-
append(name, value, fileName) {
|
11
|
-
if (typeof value !== "string") {
|
12
|
-
throw new Error("formData.append can only accept a string");
|
13
|
-
}
|
14
|
-
this._params.append(name, value);
|
15
|
-
}
|
16
|
-
delete(name) {
|
17
|
-
this._params.delete(name);
|
18
|
-
}
|
19
|
-
get(name) {
|
20
|
-
return this._params.get(name);
|
21
|
-
}
|
22
|
-
getAll(name) {
|
23
|
-
return this._params.getAll(name);
|
24
|
-
}
|
25
|
-
has(name) {
|
26
|
-
return this._params.has(name);
|
27
|
-
}
|
28
|
-
set(name, value, fileName) {
|
29
|
-
if (typeof value !== "string") {
|
30
|
-
throw new Error("formData.set can only accept a string");
|
31
|
-
}
|
32
|
-
this._params.set(name, value);
|
33
|
-
}
|
34
|
-
forEach(callbackfn, thisArg) {
|
35
|
-
this._params.forEach(callbackfn, thisArg);
|
36
|
-
}
|
37
|
-
entries() {
|
38
|
-
return this._params.entries();
|
39
|
-
}
|
40
|
-
keys() {
|
41
|
-
return this._params.keys();
|
42
|
-
}
|
43
|
-
values() {
|
44
|
-
return this._params.values();
|
45
|
-
}
|
46
|
-
*[Symbol.iterator]() {
|
47
|
-
yield* this._params;
|
48
|
-
}
|
49
|
-
}
|
50
|
-
exports.TestFormData = TestFormData;
|
@@ -1,295 +0,0 @@
|
|
1
|
-
"use strict";
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
-
if (k2 === undefined) k2 = k;
|
4
|
-
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
5
|
-
}) : (function(o, m, k, k2) {
|
6
|
-
if (k2 === undefined) k2 = k;
|
7
|
-
o[k2] = m[k];
|
8
|
-
}));
|
9
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
10
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
11
|
-
}) : function(o, v) {
|
12
|
-
o["default"] = v;
|
13
|
-
});
|
14
|
-
var __importStar = (this && this.__importStar) || function (mod) {
|
15
|
-
if (mod && mod.__esModule) return mod;
|
16
|
-
var result = {};
|
17
|
-
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
18
|
-
__setModuleDefault(result, mod);
|
19
|
-
return result;
|
20
|
-
};
|
21
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
22
|
-
const yup = __importStar(require("yup"));
|
23
|
-
const zod_1 = require("zod");
|
24
|
-
const __1 = require("..");
|
25
|
-
const flatten_1 = require("../internal/flatten");
|
26
|
-
const testFormData_1 = require("../test-data/testFormData");
|
27
|
-
const withZod_1 = require("./withZod");
|
28
|
-
const validationTestCases = [
|
29
|
-
{
|
30
|
-
name: "yup",
|
31
|
-
validator: (0, __1.withYup)(yup.object({
|
32
|
-
firstName: yup.string().required(),
|
33
|
-
lastName: yup.string().required(),
|
34
|
-
age: yup.number(),
|
35
|
-
address: yup
|
36
|
-
.object({
|
37
|
-
streetAddress: yup.string().required(),
|
38
|
-
city: yup.string().required(),
|
39
|
-
country: yup.string().required(),
|
40
|
-
})
|
41
|
-
.required(),
|
42
|
-
pets: yup.array().of(yup.object({
|
43
|
-
animal: yup.string().required(),
|
44
|
-
name: yup.string().required(),
|
45
|
-
})),
|
46
|
-
})),
|
47
|
-
},
|
48
|
-
{
|
49
|
-
name: "zod",
|
50
|
-
validator: (0, withZod_1.withZod)(zod_1.z.object({
|
51
|
-
firstName: zod_1.z.string().nonempty(),
|
52
|
-
lastName: zod_1.z.string().nonempty(),
|
53
|
-
age: zod_1.z.optional(zod_1.z.number()),
|
54
|
-
address: zod_1.z.preprocess((value) => (value == null ? {} : value), zod_1.z.object({
|
55
|
-
streetAddress: zod_1.z.string().nonempty(),
|
56
|
-
city: zod_1.z.string().nonempty(),
|
57
|
-
country: zod_1.z.string().nonempty(),
|
58
|
-
})),
|
59
|
-
pets: zod_1.z
|
60
|
-
.object({
|
61
|
-
animal: zod_1.z.string().nonempty(),
|
62
|
-
name: zod_1.z.string().nonempty(),
|
63
|
-
})
|
64
|
-
.array()
|
65
|
-
.optional(),
|
66
|
-
})),
|
67
|
-
},
|
68
|
-
];
|
69
|
-
// Not going to enforce exact error strings here
|
70
|
-
const anyString = expect.any(String);
|
71
|
-
describe("Validation", () => {
|
72
|
-
describe.each(validationTestCases)("Adapter for $name", ({ validator }) => {
|
73
|
-
describe("validate", () => {
|
74
|
-
it("should return the data when valid", () => {
|
75
|
-
const person = {
|
76
|
-
firstName: "John",
|
77
|
-
lastName: "Doe",
|
78
|
-
age: 30,
|
79
|
-
address: {
|
80
|
-
streetAddress: "123 Main St",
|
81
|
-
city: "Anytown",
|
82
|
-
country: "USA",
|
83
|
-
},
|
84
|
-
pets: [{ animal: "dog", name: "Fido" }],
|
85
|
-
};
|
86
|
-
expect(validator.validate(person)).toEqual({
|
87
|
-
data: person,
|
88
|
-
error: undefined,
|
89
|
-
});
|
90
|
-
});
|
91
|
-
it("should return field errors when invalid", () => {
|
92
|
-
const obj = { age: "hi!", pets: [{ animal: "dog" }] };
|
93
|
-
expect(validator.validate(obj)).toEqual({
|
94
|
-
data: undefined,
|
95
|
-
error: {
|
96
|
-
firstName: anyString,
|
97
|
-
lastName: anyString,
|
98
|
-
age: anyString,
|
99
|
-
"address.city": anyString,
|
100
|
-
"address.country": anyString,
|
101
|
-
"address.streetAddress": anyString,
|
102
|
-
"pets[0].name": anyString,
|
103
|
-
_submittedData: obj,
|
104
|
-
},
|
105
|
-
});
|
106
|
-
});
|
107
|
-
it("should unflatten data when validating", () => {
|
108
|
-
const data = {
|
109
|
-
firstName: "John",
|
110
|
-
lastName: "Doe",
|
111
|
-
age: 30,
|
112
|
-
"address.streetAddress": "123 Main St",
|
113
|
-
"address.city": "Anytown",
|
114
|
-
"address.country": "USA",
|
115
|
-
"pets[0].animal": "dog",
|
116
|
-
"pets[0].name": "Fido",
|
117
|
-
};
|
118
|
-
expect(validator.validate(data)).toEqual({
|
119
|
-
data: {
|
120
|
-
firstName: "John",
|
121
|
-
lastName: "Doe",
|
122
|
-
age: 30,
|
123
|
-
address: {
|
124
|
-
streetAddress: "123 Main St",
|
125
|
-
city: "Anytown",
|
126
|
-
country: "USA",
|
127
|
-
},
|
128
|
-
pets: [{ animal: "dog", name: "Fido" }],
|
129
|
-
},
|
130
|
-
error: undefined,
|
131
|
-
});
|
132
|
-
});
|
133
|
-
it("should accept FormData directly and return errors", () => {
|
134
|
-
const formData = new testFormData_1.TestFormData();
|
135
|
-
formData.set("firstName", "John");
|
136
|
-
formData.set("lastName", "Doe");
|
137
|
-
formData.set("address.streetAddress", "123 Main St");
|
138
|
-
formData.set("address.country", "USA");
|
139
|
-
formData.set("pets[0].animal", "dog");
|
140
|
-
expect(validator.validate(formData)).toEqual({
|
141
|
-
data: undefined,
|
142
|
-
error: {
|
143
|
-
"address.city": anyString,
|
144
|
-
"pets[0].name": anyString,
|
145
|
-
_submittedData: (0, flatten_1.objectFromPathEntries)([...formData.entries()]),
|
146
|
-
},
|
147
|
-
});
|
148
|
-
});
|
149
|
-
it("should accept FormData directly and return valid data", () => {
|
150
|
-
const formData = new testFormData_1.TestFormData();
|
151
|
-
formData.set("firstName", "John");
|
152
|
-
formData.set("lastName", "Doe");
|
153
|
-
formData.set("address.streetAddress", "123 Main St");
|
154
|
-
formData.set("address.country", "USA");
|
155
|
-
formData.set("address.city", "Anytown");
|
156
|
-
formData.set("pets[0].animal", "dog");
|
157
|
-
formData.set("pets[0].name", "Fido");
|
158
|
-
expect(validator.validate(formData)).toEqual({
|
159
|
-
data: {
|
160
|
-
firstName: "John",
|
161
|
-
lastName: "Doe",
|
162
|
-
address: {
|
163
|
-
streetAddress: "123 Main St",
|
164
|
-
country: "USA",
|
165
|
-
city: "Anytown",
|
166
|
-
},
|
167
|
-
pets: [{ animal: "dog", name: "Fido" }],
|
168
|
-
},
|
169
|
-
error: undefined,
|
170
|
-
});
|
171
|
-
});
|
172
|
-
});
|
173
|
-
describe("validateField", () => {
|
174
|
-
it("should not return an error if field is valid", () => {
|
175
|
-
const person = {
|
176
|
-
firstName: "John",
|
177
|
-
lastName: {}, // invalid, but we should only be validating firstName
|
178
|
-
};
|
179
|
-
expect(validator.validateField(person, "firstName")).toEqual({
|
180
|
-
error: undefined,
|
181
|
-
});
|
182
|
-
});
|
183
|
-
it("should not return an error if a nested field is valid", () => {
|
184
|
-
const person = {
|
185
|
-
firstName: "John",
|
186
|
-
lastName: {},
|
187
|
-
address: {
|
188
|
-
streetAddress: "123 Main St",
|
189
|
-
city: "Anytown",
|
190
|
-
country: "USA",
|
191
|
-
},
|
192
|
-
pets: [{ animal: "dog", name: "Fido" }],
|
193
|
-
};
|
194
|
-
expect(validator.validateField(person, "address.streetAddress")).toEqual({
|
195
|
-
error: undefined,
|
196
|
-
});
|
197
|
-
expect(validator.validateField(person, "address.city")).toEqual({
|
198
|
-
error: undefined,
|
199
|
-
});
|
200
|
-
expect(validator.validateField(person, "address.country")).toEqual({
|
201
|
-
error: undefined,
|
202
|
-
});
|
203
|
-
expect(validator.validateField(person, "pets[0].animal")).toEqual({
|
204
|
-
error: undefined,
|
205
|
-
});
|
206
|
-
expect(validator.validateField(person, "pets[0].name")).toEqual({
|
207
|
-
error: undefined,
|
208
|
-
});
|
209
|
-
});
|
210
|
-
it("should return an error if field is invalid", () => {
|
211
|
-
const person = {
|
212
|
-
firstName: "John",
|
213
|
-
lastName: {},
|
214
|
-
address: {
|
215
|
-
streetAddress: "123 Main St",
|
216
|
-
city: 1234,
|
217
|
-
},
|
218
|
-
};
|
219
|
-
expect(validator.validateField(person, "lastName")).toEqual({
|
220
|
-
error: anyString,
|
221
|
-
});
|
222
|
-
});
|
223
|
-
it("should return an error if a nested field is invalid", () => {
|
224
|
-
const person = {
|
225
|
-
firstName: "John",
|
226
|
-
lastName: {},
|
227
|
-
address: {
|
228
|
-
streetAddress: "123 Main St",
|
229
|
-
city: 1234,
|
230
|
-
},
|
231
|
-
pets: [{ animal: "dog" }],
|
232
|
-
};
|
233
|
-
expect(validator.validateField(person, "address.country")).toEqual({
|
234
|
-
error: anyString,
|
235
|
-
});
|
236
|
-
expect(validator.validateField(person, "pets[0].name")).toEqual({
|
237
|
-
error: anyString,
|
238
|
-
});
|
239
|
-
});
|
240
|
-
});
|
241
|
-
});
|
242
|
-
});
|
243
|
-
describe("withZod", () => {
|
244
|
-
it("returns coherent errors for complex schemas", () => {
|
245
|
-
const schema = zod_1.z.union([
|
246
|
-
zod_1.z.object({
|
247
|
-
type: zod_1.z.literal("foo"),
|
248
|
-
foo: zod_1.z.string(),
|
249
|
-
}),
|
250
|
-
zod_1.z.object({
|
251
|
-
type: zod_1.z.literal("bar"),
|
252
|
-
bar: zod_1.z.string(),
|
253
|
-
}),
|
254
|
-
]);
|
255
|
-
const obj = {
|
256
|
-
type: "foo",
|
257
|
-
bar: 123,
|
258
|
-
foo: 123,
|
259
|
-
};
|
260
|
-
expect((0, withZod_1.withZod)(schema).validate(obj)).toEqual({
|
261
|
-
data: undefined,
|
262
|
-
error: {
|
263
|
-
type: anyString,
|
264
|
-
bar: anyString,
|
265
|
-
foo: anyString,
|
266
|
-
_submittedData: obj,
|
267
|
-
},
|
268
|
-
});
|
269
|
-
});
|
270
|
-
it("returns errors for fields that are unions", () => {
|
271
|
-
const schema = zod_1.z.object({
|
272
|
-
field1: zod_1.z.union([zod_1.z.literal("foo"), zod_1.z.literal("bar")]),
|
273
|
-
field2: zod_1.z.union([zod_1.z.literal("foo"), zod_1.z.literal("bar")]),
|
274
|
-
});
|
275
|
-
const obj = {
|
276
|
-
field1: "a value",
|
277
|
-
// field2 missing
|
278
|
-
};
|
279
|
-
const validator = (0, withZod_1.withZod)(schema);
|
280
|
-
expect(validator.validate(obj)).toEqual({
|
281
|
-
data: undefined,
|
282
|
-
error: {
|
283
|
-
field1: anyString,
|
284
|
-
field2: anyString,
|
285
|
-
_submittedData: obj,
|
286
|
-
},
|
287
|
-
});
|
288
|
-
expect(validator.validateField(obj, "field1")).toEqual({
|
289
|
-
error: anyString,
|
290
|
-
});
|
291
|
-
expect(validator.validateField(obj, "field2")).toEqual({
|
292
|
-
error: anyString,
|
293
|
-
});
|
294
|
-
});
|
295
|
-
});
|
@@ -1,6 +0,0 @@
|
|
1
|
-
import type { AnyObjectSchema, InferType } from "yup";
|
2
|
-
import { Validator } from "./types";
|
3
|
-
/**
|
4
|
-
* Create a `Validator` using a `yup` schema.
|
5
|
-
*/
|
6
|
-
export declare const withYup: <Schema extends AnyObjectSchema>(validationSchema: Schema) => Validator<InferType<Schema>>;
|