@hookform/resolvers 2.8.10 → 2.9.2

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.
@@ -0,0 +1,2 @@
1
+ export * from './ajv';
2
+ export * from './types';
@@ -0,0 +1,5 @@
1
+ import { FieldValues, ResolverOptions, ResolverResult, UnpackNestedValue } from 'react-hook-form';
2
+ import * as Ajv from 'ajv';
3
+ export declare type Resolver = <T>(schema: Ajv.JSONSchemaType<T>, schemaOptions?: Ajv.Options, factoryOptions?: {
4
+ mode?: 'async' | 'sync';
5
+ }) => <TFieldValues extends FieldValues, TContext>(values: UnpackNestedValue<TFieldValues>, context: TContext | undefined, options: ResolverOptions<TFieldValues>) => Promise<ResolverResult<TFieldValues>>;
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "ajv",
3
+ "amdName": "hookformResolversAjv",
4
+ "version": "1.0.0",
5
+ "private": true,
6
+ "description": "React Hook Form validation resolver: ajv",
7
+ "main": "dist/ajv.js",
8
+ "module": "dist/ajv.module.js",
9
+ "umd:main": "dist/ajv.umd.js",
10
+ "source": "src/index.ts",
11
+ "types": "dist/index.d.ts",
12
+ "license": "MIT",
13
+ "peerDependencies": {
14
+ "react-hook-form": "^7.0.0",
15
+ "@hookform/resolvers": "^2.0.0"
16
+ }
17
+ }
@@ -0,0 +1,98 @@
1
+ import { act, render, screen } from '@testing-library/react';
2
+ import user from '@testing-library/user-event';
3
+ import { JSONSchemaType } from 'ajv';
4
+ import React from 'react';
5
+ import { useForm } from 'react-hook-form';
6
+ import { ajvResolver } from '..';
7
+
8
+ const USERNAME_REQUIRED_MESSAGE = 'username field is required';
9
+ const PASSWORD_REQUIRED_MESSAGE = 'password field is required';
10
+
11
+ type FormData = { username: string; password: string };
12
+
13
+ const schema: JSONSchemaType<FormData> = {
14
+ type: 'object',
15
+ properties: {
16
+ username: {
17
+ type: 'string',
18
+ minLength: 1,
19
+ errorMessage: { minLength: USERNAME_REQUIRED_MESSAGE },
20
+ },
21
+ password: {
22
+ type: 'string',
23
+ minLength: 1,
24
+ errorMessage: { minLength: PASSWORD_REQUIRED_MESSAGE },
25
+ },
26
+ },
27
+ required: ['username', 'password'],
28
+ additionalProperties: false,
29
+ };
30
+
31
+ interface Props {
32
+ onSubmit: (data: FormData) => void;
33
+ }
34
+
35
+ function TestComponent({ onSubmit }: Props) {
36
+ const { register, handleSubmit } = useForm<FormData>({
37
+ resolver: ajvResolver(schema),
38
+ shouldUseNativeValidation: true,
39
+ });
40
+
41
+ return (
42
+ <form onSubmit={handleSubmit(onSubmit)}>
43
+ <input {...register('username')} placeholder="username" />
44
+
45
+ <input {...register('password')} placeholder="password" />
46
+
47
+ <button type="submit">submit</button>
48
+ </form>
49
+ );
50
+ }
51
+
52
+ test("form's native validation with Ajv", async () => {
53
+ const handleSubmit = jest.fn();
54
+ render(<TestComponent onSubmit={handleSubmit} />);
55
+
56
+ // username
57
+ let usernameField = screen.getByPlaceholderText(
58
+ /username/i,
59
+ ) as HTMLInputElement;
60
+ expect(usernameField.validity.valid).toBe(true);
61
+ expect(usernameField.validationMessage).toBe('');
62
+
63
+ // password
64
+ let passwordField = screen.getByPlaceholderText(
65
+ /password/i,
66
+ ) as HTMLInputElement;
67
+ expect(passwordField.validity.valid).toBe(true);
68
+ expect(passwordField.validationMessage).toBe('');
69
+
70
+ await act(async () => {
71
+ user.click(screen.getByText(/submit/i));
72
+ });
73
+
74
+ // username
75
+ usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement;
76
+ expect(usernameField.validity.valid).toBe(false);
77
+ expect(usernameField.validationMessage).toBe(USERNAME_REQUIRED_MESSAGE);
78
+
79
+ // password
80
+ passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement;
81
+ expect(passwordField.validity.valid).toBe(false);
82
+ expect(passwordField.validationMessage).toBe(PASSWORD_REQUIRED_MESSAGE);
83
+
84
+ await act(async () => {
85
+ user.type(screen.getByPlaceholderText(/username/i), 'joe');
86
+ user.type(screen.getByPlaceholderText(/password/i), 'password');
87
+ });
88
+
89
+ // username
90
+ usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement;
91
+ expect(usernameField.validity.valid).toBe(true);
92
+ expect(usernameField.validationMessage).toBe('');
93
+
94
+ // password
95
+ passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement;
96
+ expect(passwordField.validity.valid).toBe(true);
97
+ expect(passwordField.validationMessage).toBe('');
98
+ });
@@ -0,0 +1,67 @@
1
+ import { act, render, screen } from '@testing-library/react';
2
+ import user from '@testing-library/user-event';
3
+ import { JSONSchemaType } from 'ajv';
4
+ import React from 'react';
5
+ import { useForm } from 'react-hook-form';
6
+ import { ajvResolver } from '..';
7
+
8
+ type FormData = { username: string; password: string };
9
+
10
+ const schema: JSONSchemaType<FormData> = {
11
+ type: 'object',
12
+ properties: {
13
+ username: {
14
+ type: 'string',
15
+ minLength: 1,
16
+ errorMessage: { minLength: 'username field is required' },
17
+ },
18
+ password: {
19
+ type: 'string',
20
+ minLength: 1,
21
+ errorMessage: { minLength: 'password field is required' },
22
+ },
23
+ },
24
+ required: ['username', 'password'],
25
+ additionalProperties: false,
26
+ };
27
+
28
+ interface Props {
29
+ onSubmit: (data: FormData) => void;
30
+ }
31
+
32
+ function TestComponent({ onSubmit }: Props) {
33
+ const {
34
+ register,
35
+ formState: { errors },
36
+ handleSubmit,
37
+ } = useForm<FormData>({
38
+ resolver: ajvResolver(schema), // Useful to check TypeScript regressions
39
+ });
40
+
41
+ return (
42
+ <form onSubmit={handleSubmit(onSubmit)}>
43
+ <input {...register('username')} />
44
+ {errors.username && <span role="alert">{errors.username.message}</span>}
45
+
46
+ <input {...register('password')} />
47
+ {errors.password && <span role="alert">{errors.password.message}</span>}
48
+
49
+ <button type="submit">submit</button>
50
+ </form>
51
+ );
52
+ }
53
+
54
+ test("form's validation with Ajv and TypeScript's integration", async () => {
55
+ const handleSubmit = jest.fn();
56
+ render(<TestComponent onSubmit={handleSubmit} />);
57
+
58
+ expect(screen.queryAllByRole(/alert/i)).toHaveLength(0);
59
+
60
+ await act(async () => {
61
+ user.click(screen.getByText(/submit/i));
62
+ });
63
+
64
+ expect(screen.getByText(/username field is required/i)).toBeInTheDocument();
65
+ expect(screen.getByText(/password field is required/i)).toBeInTheDocument();
66
+ expect(handleSubmit).not.toHaveBeenCalled();
67
+ });
@@ -0,0 +1,80 @@
1
+ import { JSONSchemaType } from 'ajv';
2
+ import { Field, InternalFieldName } from 'react-hook-form';
3
+
4
+ interface Data {
5
+ username: string;
6
+ password: string;
7
+ deepObject: { data: string; twoLayersDeep: { name: string } };
8
+ }
9
+
10
+ export const schema: JSONSchemaType<Data> = {
11
+ type: 'object',
12
+ properties: {
13
+ username: {
14
+ type: 'string',
15
+ minLength: 3,
16
+ },
17
+ password: {
18
+ type: 'string',
19
+ pattern: '.*[A-Z].*',
20
+ errorMessage: {
21
+ pattern: 'One uppercase character',
22
+ },
23
+ },
24
+ deepObject: {
25
+ type: 'object',
26
+ nullable: true,
27
+ properties: {
28
+ data: { type: 'string' },
29
+ twoLayersDeep: {
30
+ type: 'object',
31
+ properties: { name: { type: 'string' } },
32
+ additionalProperties: false,
33
+ required: ['name'],
34
+ },
35
+ },
36
+ required: ['data', 'twoLayersDeep'],
37
+ },
38
+ },
39
+ required: ['username', 'password', 'deepObject'],
40
+ additionalProperties: false,
41
+ };
42
+
43
+ export const validData: Data = {
44
+ username: 'jsun969',
45
+ password: 'validPassword',
46
+ deepObject: {
47
+ twoLayersDeep: {
48
+ name: 'deeper',
49
+ },
50
+ data: 'data',
51
+ },
52
+ };
53
+
54
+ export const invalidData = {
55
+ username: '__',
56
+ password: 'invalid-password',
57
+ deepObject: {
58
+ data: 233,
59
+ twoLayersDeep: { name: 123 }
60
+ },
61
+ };
62
+
63
+ export const fields: Record<InternalFieldName, Field['_f']> = {
64
+ username: {
65
+ ref: { name: 'username' },
66
+ name: 'username',
67
+ },
68
+ password: {
69
+ ref: { name: 'password' },
70
+ name: 'password',
71
+ },
72
+ email: {
73
+ ref: { name: 'email' },
74
+ name: 'email',
75
+ },
76
+ birthday: {
77
+ ref: { name: 'birthday' },
78
+ name: 'birthday',
79
+ },
80
+ };
@@ -0,0 +1,223 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`ajvResolver should return all the error messages from ajvResolver when requirement fails and validateAllFieldCriteria set to true 1`] = `
4
+ Object {
5
+ "errors": Object {
6
+ "deepObject": Object {
7
+ "message": "must have required property 'deepObject'",
8
+ "ref": undefined,
9
+ "type": "required",
10
+ },
11
+ "password": Object {
12
+ "message": "must have required property 'password'",
13
+ "ref": Object {
14
+ "name": "password",
15
+ },
16
+ "type": "required",
17
+ },
18
+ "username": Object {
19
+ "message": "must have required property 'username'",
20
+ "ref": Object {
21
+ "name": "username",
22
+ },
23
+ "type": "required",
24
+ },
25
+ },
26
+ "values": Object {},
27
+ }
28
+ `;
29
+
30
+ exports[`ajvResolver should return all the error messages from ajvResolver when requirement fails and validateAllFieldCriteria set to true and \`mode: sync\` 1`] = `
31
+ Object {
32
+ "errors": Object {
33
+ "deepObject": Object {
34
+ "message": "must have required property 'deepObject'",
35
+ "ref": undefined,
36
+ "type": "required",
37
+ },
38
+ "password": Object {
39
+ "message": "must have required property 'password'",
40
+ "ref": Object {
41
+ "name": "password",
42
+ },
43
+ "type": "required",
44
+ },
45
+ "username": Object {
46
+ "message": "must have required property 'username'",
47
+ "ref": Object {
48
+ "name": "username",
49
+ },
50
+ "type": "required",
51
+ },
52
+ },
53
+ "values": Object {},
54
+ }
55
+ `;
56
+
57
+ exports[`ajvResolver should return all the error messages from ajvResolver when validation fails and validateAllFieldCriteria set to true 1`] = `
58
+ Object {
59
+ "errors": Object {
60
+ "deepObject": Object {
61
+ "data": Object {
62
+ "message": "must be string",
63
+ "ref": undefined,
64
+ "type": "type",
65
+ "types": Object {
66
+ "type": "must be string",
67
+ },
68
+ },
69
+ "twoLayersDeep": Object {
70
+ "name": Object {
71
+ "message": "must be string",
72
+ "ref": undefined,
73
+ "type": "type",
74
+ "types": Object {
75
+ "type": "must be string",
76
+ },
77
+ },
78
+ },
79
+ },
80
+ "password": Object {
81
+ "message": "One uppercase character",
82
+ "ref": Object {
83
+ "name": "password",
84
+ },
85
+ "type": "errorMessage",
86
+ "types": Object {
87
+ "errorMessage": "One uppercase character",
88
+ },
89
+ },
90
+ "username": Object {
91
+ "message": "must NOT have fewer than 3 characters",
92
+ "ref": Object {
93
+ "name": "username",
94
+ },
95
+ "type": "minLength",
96
+ "types": Object {
97
+ "minLength": "must NOT have fewer than 3 characters",
98
+ },
99
+ },
100
+ },
101
+ "values": Object {},
102
+ }
103
+ `;
104
+
105
+ exports[`ajvResolver should return all the error messages from ajvResolver when validation fails and validateAllFieldCriteria set to true and \`mode: sync\` 1`] = `
106
+ Object {
107
+ "errors": Object {
108
+ "deepObject": Object {
109
+ "data": Object {
110
+ "message": "must be string",
111
+ "ref": undefined,
112
+ "type": "type",
113
+ "types": Object {
114
+ "type": "must be string",
115
+ },
116
+ },
117
+ "twoLayersDeep": Object {
118
+ "name": Object {
119
+ "message": "must be string",
120
+ "ref": undefined,
121
+ "type": "type",
122
+ "types": Object {
123
+ "type": "must be string",
124
+ },
125
+ },
126
+ },
127
+ },
128
+ "password": Object {
129
+ "message": "One uppercase character",
130
+ "ref": Object {
131
+ "name": "password",
132
+ },
133
+ "type": "errorMessage",
134
+ "types": Object {
135
+ "errorMessage": "One uppercase character",
136
+ },
137
+ },
138
+ "username": Object {
139
+ "message": "must NOT have fewer than 3 characters",
140
+ "ref": Object {
141
+ "name": "username",
142
+ },
143
+ "type": "minLength",
144
+ "types": Object {
145
+ "minLength": "must NOT have fewer than 3 characters",
146
+ },
147
+ },
148
+ },
149
+ "values": Object {},
150
+ }
151
+ `;
152
+
153
+ exports[`ajvResolver should return single error message from ajvResolver when validation fails and validateAllFieldCriteria set to false 1`] = `
154
+ Object {
155
+ "errors": Object {
156
+ "deepObject": Object {
157
+ "data": Object {
158
+ "message": "must be string",
159
+ "ref": undefined,
160
+ "type": "type",
161
+ },
162
+ "twoLayersDeep": Object {
163
+ "name": Object {
164
+ "message": "must be string",
165
+ "ref": undefined,
166
+ "type": "type",
167
+ },
168
+ },
169
+ },
170
+ "password": Object {
171
+ "message": "One uppercase character",
172
+ "ref": Object {
173
+ "name": "password",
174
+ },
175
+ "type": "errorMessage",
176
+ },
177
+ "username": Object {
178
+ "message": "must NOT have fewer than 3 characters",
179
+ "ref": Object {
180
+ "name": "username",
181
+ },
182
+ "type": "minLength",
183
+ },
184
+ },
185
+ "values": Object {},
186
+ }
187
+ `;
188
+
189
+ exports[`ajvResolver should return single error message from ajvResolver when validation fails and validateAllFieldCriteria set to false and \`mode: sync\` 1`] = `
190
+ Object {
191
+ "errors": Object {
192
+ "deepObject": Object {
193
+ "data": Object {
194
+ "message": "must be string",
195
+ "ref": undefined,
196
+ "type": "type",
197
+ },
198
+ "twoLayersDeep": Object {
199
+ "name": Object {
200
+ "message": "must be string",
201
+ "ref": undefined,
202
+ "type": "type",
203
+ },
204
+ },
205
+ },
206
+ "password": Object {
207
+ "message": "One uppercase character",
208
+ "ref": Object {
209
+ "name": "password",
210
+ },
211
+ "type": "errorMessage",
212
+ },
213
+ "username": Object {
214
+ "message": "must NOT have fewer than 3 characters",
215
+ "ref": Object {
216
+ "name": "username",
217
+ },
218
+ "type": "minLength",
219
+ },
220
+ },
221
+ "values": Object {},
222
+ }
223
+ `;
@@ -0,0 +1,84 @@
1
+ import { ajvResolver } from '..';
2
+ import { fields, invalidData, schema, validData } from './__fixtures__/data';
3
+
4
+ const shouldUseNativeValidation = false;
5
+
6
+ describe('ajvResolver', () => {
7
+ it('should return values from ajvResolver when validation pass', async () => {
8
+ expect(
9
+ await ajvResolver(schema)(validData, undefined, {
10
+ fields,
11
+ shouldUseNativeValidation,
12
+ }),
13
+ ).toEqual({
14
+ values: validData,
15
+ errors: {},
16
+ });
17
+ });
18
+
19
+ it('should return values from ajvResolver with `mode: sync` when validation pass', async () => {
20
+ expect(
21
+ await ajvResolver(schema, undefined, {
22
+ mode: 'sync',
23
+ })(validData, undefined, { fields, shouldUseNativeValidation }),
24
+ ).toEqual({
25
+ values: validData,
26
+ errors: {},
27
+ });
28
+ });
29
+
30
+ it('should return single error message from ajvResolver when validation fails and validateAllFieldCriteria set to false', async () => {
31
+ expect(
32
+ await ajvResolver(schema)(invalidData, undefined, {
33
+ fields,
34
+ shouldUseNativeValidation,
35
+ }),
36
+ ).toMatchSnapshot();
37
+ });
38
+
39
+ it('should return single error message from ajvResolver when validation fails and validateAllFieldCriteria set to false and `mode: sync`', async () => {
40
+ expect(
41
+ await ajvResolver(schema, undefined, {
42
+ mode: 'sync',
43
+ })(invalidData, undefined, { fields, shouldUseNativeValidation }),
44
+ ).toMatchSnapshot();
45
+ });
46
+
47
+ it('should return all the error messages from ajvResolver when validation fails and validateAllFieldCriteria set to true', async () => {
48
+ expect(
49
+ await ajvResolver(schema)(
50
+ invalidData,
51
+ {},
52
+ { fields, criteriaMode: 'all', shouldUseNativeValidation },
53
+ ),
54
+ ).toMatchSnapshot();
55
+ });
56
+
57
+ it('should return all the error messages from ajvResolver when validation fails and validateAllFieldCriteria set to true and `mode: sync`', async () => {
58
+ expect(
59
+ await ajvResolver(schema, undefined, { mode: 'sync' })(
60
+ invalidData,
61
+ {},
62
+ { fields, criteriaMode: 'all', shouldUseNativeValidation },
63
+ ),
64
+ ).toMatchSnapshot();
65
+ });
66
+
67
+ it('should return all the error messages from ajvResolver when requirement fails and validateAllFieldCriteria set to true', async () => {
68
+ expect(
69
+ await ajvResolver(schema)({}, undefined, {
70
+ fields,
71
+ shouldUseNativeValidation,
72
+ }),
73
+ ).toMatchSnapshot();
74
+ });
75
+
76
+ it('should return all the error messages from ajvResolver when requirement fails and validateAllFieldCriteria set to true and `mode: sync`', async () => {
77
+ expect(
78
+ await ajvResolver(schema, undefined, { mode: 'sync' })({}, undefined, {
79
+ fields,
80
+ shouldUseNativeValidation,
81
+ }),
82
+ ).toMatchSnapshot();
83
+ });
84
+ });
package/ajv/src/ajv.ts ADDED
@@ -0,0 +1,84 @@
1
+ import { toNestError, validateFieldsNatively } from '@hookform/resolvers';
2
+ import Ajv, { DefinedError } from 'ajv';
3
+ import ajvErrors from 'ajv-errors';
4
+ import { appendErrors, FieldError } from 'react-hook-form';
5
+ import { Resolver } from './types';
6
+
7
+ const parseErrorSchema = (
8
+ ajvErrors: DefinedError[],
9
+ validateAllFieldCriteria: boolean,
10
+ ) => {
11
+ // Ajv will return empty instancePath when require error
12
+ ajvErrors.forEach((error) => {
13
+ if (error.keyword === 'required') {
14
+ error.instancePath = '/' + error.params.missingProperty;
15
+ }
16
+ });
17
+
18
+ return ajvErrors.reduce<Record<string, FieldError>>((previous, error) => {
19
+ // `/deepObject/data` -> `deepObject.data`
20
+ const path = error.instancePath.substring(1).replace(/\//g, '.');
21
+
22
+ if (!previous[path]) {
23
+ previous[path] = {
24
+ message: error.message,
25
+ type: error.keyword,
26
+ };
27
+ }
28
+
29
+ if (validateAllFieldCriteria) {
30
+ const types = previous[path].types;
31
+ const messages = types && types[error.keyword];
32
+
33
+ previous[path] = appendErrors(
34
+ path,
35
+ validateAllFieldCriteria,
36
+ previous,
37
+ error.keyword,
38
+ messages
39
+ ? ([] as string[]).concat(messages as string[], error.message || '')
40
+ : error.message,
41
+ ) as FieldError;
42
+ }
43
+
44
+ return previous;
45
+ }, {});
46
+ };
47
+
48
+ export const ajvResolver: Resolver =
49
+ (schema, schemaOptions, resolverOptions = {}) =>
50
+ async (values, _, options) => {
51
+ const ajv = new Ajv({
52
+ allErrors: true,
53
+ validateSchema: true,
54
+ ...schemaOptions,
55
+ });
56
+
57
+ ajvErrors(ajv);
58
+
59
+ const validate = ajv.compile(
60
+ Object.assign({ $async: resolverOptions?.mode === 'async' }, schema),
61
+ );
62
+ const valid = validate(values);
63
+
64
+ if (!valid) {
65
+ return {
66
+ values: {},
67
+ errors: toNestError(
68
+ parseErrorSchema(
69
+ validate.errors as DefinedError[],
70
+ !options.shouldUseNativeValidation &&
71
+ options.criteriaMode === 'all',
72
+ ),
73
+ options,
74
+ ),
75
+ };
76
+ }
77
+
78
+ options.shouldUseNativeValidation && validateFieldsNatively({}, options);
79
+
80
+ return {
81
+ values,
82
+ errors: {},
83
+ };
84
+ };
@@ -0,0 +1,2 @@
1
+ export * from './ajv';
2
+ export * from './types';
@@ -0,0 +1,17 @@
1
+ import {
2
+ FieldValues,
3
+ ResolverOptions,
4
+ ResolverResult,
5
+ UnpackNestedValue,
6
+ } from 'react-hook-form';
7
+ import * as Ajv from 'ajv';
8
+
9
+ export type Resolver = <T>(
10
+ schema: Ajv.JSONSchemaType<T>,
11
+ schemaOptions?: Ajv.Options,
12
+ factoryOptions?: { mode?: 'async' | 'sync' },
13
+ ) => <TFieldValues extends FieldValues, TContext>(
14
+ values: UnpackNestedValue<TFieldValues>,
15
+ context: TContext | undefined,
16
+ options: ResolverOptions<TFieldValues>,
17
+ ) => Promise<ResolverResult<TFieldValues>>;