Skip to content

Commit ad89a20

Browse files
feat: add ata-validator resolver (#845)
* feat: add ata-validator resolver Add a new resolver for ata-validator, an ultra-fast JSON Schema validator for Node.js. * fix: add ata-validator to node-13-exports and export map check * perf: move Validator instantiation to factory scope Avoid recompiling the schema on every validation call by constructing the Validator once when the resolver is created instead of on each form submit.
1 parent 02286db commit ad89a20

12 files changed

Lines changed: 787 additions & 2 deletions

File tree

ata-validator/package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "@hookform/resolvers/ata-validator",
3+
"amdName": "hookformResolversAtaValidator",
4+
"version": "1.0.0",
5+
"private": true,
6+
"description": "React Hook Form validation resolver: ata-validator",
7+
"main": "dist/ata-validator.js",
8+
"module": "dist/ata-validator.module.js",
9+
"umd:main": "dist/ata-validator.umd.js",
10+
"source": "src/index.ts",
11+
"types": "dist/index.d.ts",
12+
"license": "MIT",
13+
"peerDependencies": {
14+
"react-hook-form": "^7.55.0",
15+
"@hookform/resolvers": "^2.0.0",
16+
"ata-validator": "^0.7.0"
17+
}
18+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { render, screen } from '@testing-library/react';
2+
import user from '@testing-library/user-event';
3+
import React from 'react';
4+
import { useForm } from 'react-hook-form';
5+
import { ataResolver } from '..';
6+
7+
type FormData = { username: string; password: string };
8+
9+
const schema = {
10+
type: 'object',
11+
properties: {
12+
username: {
13+
type: 'string',
14+
minLength: 1,
15+
},
16+
password: {
17+
type: 'string',
18+
minLength: 1,
19+
},
20+
},
21+
required: ['username', 'password'],
22+
additionalProperties: false,
23+
};
24+
25+
interface Props {
26+
onSubmit: (data: FormData) => void;
27+
}
28+
29+
function TestComponent({ onSubmit }: Props) {
30+
const { register, handleSubmit } = useForm<FormData>({
31+
resolver: ataResolver(schema),
32+
shouldUseNativeValidation: true,
33+
});
34+
35+
return (
36+
<form onSubmit={handleSubmit(onSubmit)}>
37+
<input {...register('username')} placeholder="username" />
38+
39+
<input {...register('password')} placeholder="password" />
40+
41+
<button type="submit">submit</button>
42+
</form>
43+
);
44+
}
45+
46+
test("form's native validation with ata-validator", async () => {
47+
const handleSubmit = vi.fn();
48+
render(<TestComponent onSubmit={handleSubmit} />);
49+
50+
let usernameField = screen.getByPlaceholderText(
51+
/username/i,
52+
) as HTMLInputElement;
53+
expect(usernameField.validity.valid).toBe(true);
54+
expect(usernameField.validationMessage).toBe('');
55+
56+
let passwordField = screen.getByPlaceholderText(
57+
/password/i,
58+
) as HTMLInputElement;
59+
expect(passwordField.validity.valid).toBe(true);
60+
expect(passwordField.validationMessage).toBe('');
61+
62+
await user.click(screen.getByText(/submit/i));
63+
64+
usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement;
65+
expect(usernameField.validity.valid).toBe(false);
66+
67+
passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement;
68+
expect(passwordField.validity.valid).toBe(false);
69+
70+
await user.type(screen.getByPlaceholderText(/username/i), 'joe');
71+
await user.type(screen.getByPlaceholderText(/password/i), 'password');
72+
73+
usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement;
74+
expect(usernameField.validity.valid).toBe(true);
75+
expect(usernameField.validationMessage).toBe('');
76+
77+
passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement;
78+
expect(passwordField.validity.valid).toBe(true);
79+
expect(passwordField.validationMessage).toBe('');
80+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { render, screen } from '@testing-library/react';
2+
import user from '@testing-library/user-event';
3+
import React from 'react';
4+
import { useForm } from 'react-hook-form';
5+
import { ataResolver } from '..';
6+
7+
type FormData = { username: string; password: string };
8+
9+
const schema = {
10+
type: 'object',
11+
properties: {
12+
username: {
13+
type: 'string',
14+
minLength: 1,
15+
},
16+
password: {
17+
type: 'string',
18+
minLength: 1,
19+
},
20+
},
21+
required: ['username', 'password'],
22+
additionalProperties: false,
23+
};
24+
25+
interface Props {
26+
onSubmit: (data: FormData) => void;
27+
}
28+
29+
function TestComponent({ onSubmit }: Props) {
30+
const {
31+
register,
32+
formState: { errors },
33+
handleSubmit,
34+
} = useForm<FormData>({
35+
resolver: ataResolver(schema),
36+
});
37+
38+
return (
39+
<form onSubmit={handleSubmit(onSubmit)}>
40+
<input {...register('username')} />
41+
{errors.username && <span role="alert">{errors.username.message}</span>}
42+
43+
<input {...register('password')} />
44+
{errors.password && <span role="alert">{errors.password.message}</span>}
45+
46+
<button type="submit">submit</button>
47+
</form>
48+
);
49+
}
50+
51+
test("form's validation with ata-validator and TypeScript's integration", async () => {
52+
const handleSubmit = vi.fn();
53+
render(<TestComponent onSubmit={handleSubmit} />);
54+
55+
expect(screen.queryAllByRole('alert')).toHaveLength(0);
56+
57+
await user.click(screen.getByText(/submit/i));
58+
59+
expect(screen.queryAllByRole('alert').length).toBeGreaterThan(0);
60+
expect(handleSubmit).not.toHaveBeenCalled();
61+
});
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { Field, InternalFieldName } from 'react-hook-form';
2+
3+
interface Data {
4+
username: string;
5+
password: string;
6+
email?: string;
7+
birthday?: number;
8+
tags: string[];
9+
enabled: boolean;
10+
url: string;
11+
like?: { id: number; name: string }[];
12+
deepObject: { data: string; twoLayersDeep: { name: string } };
13+
}
14+
15+
export const schema = {
16+
type: 'object',
17+
properties: {
18+
username: {
19+
type: 'string',
20+
minLength: 3,
21+
maxLength: 30,
22+
pattern: '^\\w+$',
23+
},
24+
password: {
25+
type: 'string',
26+
minLength: 8,
27+
pattern: '.*[A-Z].*',
28+
},
29+
email: {
30+
type: 'string',
31+
format: 'email',
32+
},
33+
birthday: {
34+
type: 'integer',
35+
minimum: 1900,
36+
maximum: 2013,
37+
},
38+
tags: {
39+
type: 'array',
40+
items: { type: 'string' },
41+
},
42+
enabled: {
43+
type: 'boolean',
44+
},
45+
url: {
46+
type: 'string',
47+
format: 'uri',
48+
},
49+
like: {
50+
type: 'array',
51+
items: {
52+
type: 'object',
53+
properties: {
54+
id: { type: 'number' },
55+
name: { type: 'string', minLength: 4, maxLength: 4 },
56+
},
57+
required: ['id', 'name'],
58+
},
59+
},
60+
deepObject: {
61+
type: 'object',
62+
properties: {
63+
data: { type: 'string' },
64+
twoLayersDeep: {
65+
type: 'object',
66+
properties: { name: { type: 'string' } },
67+
additionalProperties: false,
68+
required: ['name'],
69+
},
70+
},
71+
required: ['data', 'twoLayersDeep'],
72+
},
73+
},
74+
required: ['username', 'password', 'tags', 'enabled', 'deepObject'],
75+
additionalProperties: false,
76+
};
77+
78+
export const validData: Data = {
79+
username: 'Doe',
80+
password: 'Password123_',
81+
email: 'john@doe.com',
82+
birthday: 2000,
83+
tags: ['tag1', 'tag2'],
84+
enabled: true,
85+
url: 'https://react-hook-form.com/',
86+
like: [{ id: 1, name: 'name' }],
87+
deepObject: {
88+
data: 'data',
89+
twoLayersDeep: { name: 'deeper' },
90+
},
91+
};
92+
93+
export const invalidData = {
94+
username: '__',
95+
password: 'invalid',
96+
email: '',
97+
birthday: 'birthYear',
98+
like: [{ id: 'z' }],
99+
url: 'abc',
100+
deepObject: {
101+
data: 233,
102+
twoLayersDeep: { name: 123 },
103+
},
104+
};
105+
106+
export const invalidDataWithUndefined = {
107+
username: 'jsun969',
108+
password: undefined,
109+
deepObject: {
110+
twoLayersDeep: {
111+
name: 'deeper',
112+
},
113+
data: undefined,
114+
},
115+
};
116+
117+
export const fields: Record<InternalFieldName, Field['_f']> = {
118+
username: {
119+
ref: { name: 'username' },
120+
name: 'username',
121+
},
122+
password: {
123+
ref: { name: 'password' },
124+
name: 'password',
125+
},
126+
email: {
127+
ref: { name: 'email' },
128+
name: 'email',
129+
},
130+
birthday: {
131+
ref: { name: 'birthday' },
132+
name: 'birthday',
133+
},
134+
};

0 commit comments

Comments
 (0)