Skip to content

Commit 0cad904

Browse files
authored
feat: add ajv (#408)
* feat: add ajv without test [WIP] * fix(ajv): fix error parser path * test(ajv): add resolver tests * style(ajv): sort import * fix(ajv): fix require error and add require test * test(ajv): add react test * fix(ajv): add keyword to check empty fields * fix(ajv): remove field empty keyword to make it native * fix(ajv): add ajv mjs build script * docs: add Ajv example
1 parent c560af3 commit 0cad904

File tree

13 files changed

+713
-6
lines changed

13 files changed

+713
-6
lines changed

README.md

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@
2727
## API
2828

2929
```
30-
type Options = {
30+
type Options = {
3131
mode: 'async' | 'sync',
32-
rawValues?: boolean
32+
rawValues?: boolean
3333
}
3434
3535
resolver(schema: object, schemaOptions?: object, resolverOptions: Options)
36-
````
36+
```
3737

3838
| | type | Required | Description |
3939
| --------------- | -------- | -------- | --------------------------------------------- |
@@ -405,6 +405,56 @@ const App = () => {
405405
};
406406
```
407407

408+
### [Ajv](https://github.com/ajv-validator/ajv)
409+
410+
The fastest JSON validator for Node.js and browser
411+
412+
[![npm](https://img.shields.io/bundlephobia/minzip/ajv?style=for-the-badge)](https://bundlephobia.com/result?p=ajv)
413+
414+
```tsx
415+
import { useForm } from 'react-hook-form';
416+
import { ajvResolver } from '@hookform/resolvers/ajv';
417+
418+
// must use `minLength: 1` to implement required field
419+
const schema = {
420+
type: 'object',
421+
properties: {
422+
username: {
423+
type: 'string',
424+
minLength: 1,
425+
errorMessage: { minLength: 'username field is required' },
426+
},
427+
password: {
428+
type: 'string',
429+
minLength: 1,
430+
errorMessage: { minLength: 'password field is required' },
431+
},
432+
},
433+
required: ['username', 'password'],
434+
additionalProperties: false,
435+
};
436+
437+
const App = () => {
438+
const {
439+
register,
440+
handleSubmit,
441+
formState: { errors },
442+
} = useForm({
443+
resolver: ajvResolver(schema),
444+
});
445+
446+
return (
447+
<form onSubmit={handleSubmit((data) => console.log(data))}>
448+
<input {...register('username')} />
449+
{errors.username && <span>{errors.username.message}</span>}
450+
<input {...register('password')} />
451+
{errors.password && <span>{errors.password.message}</span>}
452+
<button type="submit">submit</button>
453+
</form>
454+
);
455+
};
456+
```
457+
408458
## Backers
409459

410460
Thanks goes to all our backers! [[Become a backer](https://opencollective.com/react-hook-form#backer)].

ajv/package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
});

ajv/src/__tests__/Form.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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 };
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+
},
30+
required: ['data'],
31+
},
32+
},
33+
required: ['username', 'password', 'deepObject'],
34+
additionalProperties: false,
35+
};
36+
37+
export const validData: Data = {
38+
username: 'jsun969',
39+
password: 'validPassword',
40+
deepObject: {
41+
data: 'data',
42+
},
43+
};
44+
45+
export const invalidData = {
46+
username: '__',
47+
password: 'invalid-password',
48+
deepObject: {
49+
data: 233,
50+
},
51+
};
52+
53+
export const fields: Record<InternalFieldName, Field['_f']> = {
54+
username: {
55+
ref: { name: 'username' },
56+
name: 'username',
57+
},
58+
password: {
59+
ref: { name: 'password' },
60+
name: 'password',
61+
},
62+
email: {
63+
ref: { name: 'email' },
64+
name: 'email',
65+
},
66+
birthday: {
67+
ref: { name: 'birthday' },
68+
name: 'birthday',
69+
},
70+
};

0 commit comments

Comments
 (0)