Skip to content

Commit 2b35b22

Browse files
committed
feat(typeschema): add typeschema resolver
1 parent 1bfc6ab commit 2b35b22

File tree

13 files changed

+769
-2
lines changed

13 files changed

+769
-2
lines changed

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
- [TypeBox](#typebox)
4646
- [ArkType](#arktype)
4747
- [Valibot](#valibot)
48+
- [TypeSchema](#typeschema)
4849
- [Backers](#backers)
4950
- [Sponsors](#sponsors)
5051
- [Contributors](#contributors)
@@ -576,6 +577,38 @@ const App = () => {
576577
};
577578
```
578579

580+
### [TypeSchema](https://typeschema.com)
581+
582+
Universal adapter for schema validation, compatible with [any validation library](https://typeschema.com/#coverage)
583+
584+
[![npm](https://img.shields.io/bundlephobia/minzip/@typeschema/main?style=for-the-badge)](https://bundlephobia.com/result?p=@typeschema/main)
585+
586+
```typescript jsx
587+
import { useForm } from 'react-hook-form';
588+
import { typeschemaResolver } from '@hookform/resolvers/typeschema';
589+
import * as z from 'zod';
590+
591+
// Use your favorite validation library
592+
const schema = z.object({
593+
username: z.string().min(1, { message: 'Required' }),
594+
password: z.number().min(1, { message: 'Required' }),
595+
});
596+
597+
const App = () => {
598+
const { register, handleSubmit } = useForm({
599+
resolver: typeschemaResolver(schema),
600+
});
601+
602+
return (
603+
<form onSubmit={handleSubmit((d) => console.log(d))}>
604+
<input {...register('username')} />
605+
<input type="password" {...register('password')} />
606+
<input type="submit" />
607+
</form>
608+
);
609+
};
610+
```
611+
579612
## Backers
580613

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

config/node-13-exports.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const subRepositories = [
1616
'typebox',
1717
'arktype',
1818
'valibot',
19+
'typeschema',
1920
];
2021

2122
const copySrc = () => {

package.json

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@
9999
"import": "./valibot/dist/valibot.mjs",
100100
"require": "./valibot/dist/valibot.js"
101101
},
102+
"./typeschema": {
103+
"types": "./typeschema/dist/index.d.ts",
104+
"umd": "./typeschema/dist/typeschema.umd.js",
105+
"import": "./typeschema/dist/typeschema.mjs",
106+
"require": "./typeschema/dist/typeschema.js"
107+
},
102108
"./package.json": "./package.json",
103109
"./*": "./*"
104110
},
@@ -145,7 +151,10 @@
145151
"arktype/dist",
146152
"valibot/package.json",
147153
"valibot/src",
148-
"valibot/dist"
154+
"valibot/dist",
155+
"typeschema/package.json",
156+
"typeschema/src",
157+
"typeschema/dist"
149158
],
150159
"publishConfig": {
151160
"access": "public"
@@ -168,6 +177,7 @@
168177
"build:typebox": "microbundle --cwd typebox --globals @hookform/resolvers=hookformResolvers,react-hook-form=ReactHookForm,@sinclair/typebox/value=value",
169178
"build:arktype": "microbundle --cwd arktype --globals @hookform/resolvers=hookformResolvers,react-hook-form=ReactHookForm",
170179
"build:valibot": "microbundle --cwd valibot --globals @hookform/resolvers=hookformResolvers,react-hook-form=ReactHookForm",
180+
"build:typeschema": "microbundle --cwd typeschema --globals @hookform/resolvers=hookformResolvers,react-hook-form=ReactHookForm",
171181
"postbuild": "node ./config/node-13-exports.js",
172182
"lint": "eslint . --ext .ts,.js --ignore-path .gitignore",
173183
"lint:types": "tsc",
@@ -196,7 +206,8 @@
196206
"typanion",
197207
"ajv",
198208
"TypeBox",
199-
"arktype"
209+
"arktype",
210+
"typeschema"
200211
],
201212
"repository": {
202213
"type": "git",
@@ -216,6 +227,9 @@
216227
"@testing-library/user-event": "^14.4.3",
217228
"@types/node": "^20.5.2",
218229
"@types/react": "^18.2.20",
230+
"@typeschema/core": "^0.13.2",
231+
"@typeschema/main": "^0.13.7",
232+
"@typeschema/zod": "^0.13.3",
219233
"@typescript-eslint/eslint-plugin": "^6.4.1",
220234
"@typescript-eslint/parser": "^6.4.1",
221235
"@vitejs/plugin-react": "^4.0.4",

pnpm-lock.yaml

Lines changed: 97 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

typeschema/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/typeschema",
3+
"amdName": "hookformResolversTypeschema",
4+
"version": "1.0.0",
5+
"private": true,
6+
"description": "React Hook Form validation resolver: typeschema",
7+
"main": "dist/typeschema.js",
8+
"module": "dist/typeschema.module.js",
9+
"umd:main": "dist/typeschema.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+
"@typeschema/main": "^0.13.7"
17+
}
18+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React from 'react';
2+
import { useForm } from 'react-hook-form';
3+
import { render, screen } from '@testing-library/react';
4+
import user from '@testing-library/user-event';
5+
import type { Infer } from '@typeschema/main';
6+
import { z } from 'zod';
7+
import { typeschemaResolver } from '..';
8+
9+
const USERNAME_REQUIRED_MESSAGE = 'username field is required';
10+
const PASSWORD_REQUIRED_MESSAGE = 'password field is required';
11+
12+
const schema = z.object({
13+
username: z.string().min(1, { message: USERNAME_REQUIRED_MESSAGE }),
14+
password: z.string().min(1, { message: PASSWORD_REQUIRED_MESSAGE }),
15+
});
16+
17+
type FormData = Infer<typeof schema>;
18+
19+
interface Props {
20+
onSubmit: (data: FormData) => void;
21+
}
22+
23+
function TestComponent({ onSubmit }: Props) {
24+
const { register, handleSubmit } = useForm<FormData>({
25+
resolver: typeschemaResolver(schema),
26+
shouldUseNativeValidation: true,
27+
});
28+
29+
return (
30+
<form onSubmit={handleSubmit(onSubmit)}>
31+
<input {...register('username')} placeholder="username" />
32+
33+
<input {...register('password')} placeholder="password" />
34+
35+
<button type="submit">submit</button>
36+
</form>
37+
);
38+
}
39+
40+
test("form's native validation with TypeSchema", async () => {
41+
const handleSubmit = vi.fn();
42+
render(<TestComponent onSubmit={handleSubmit} />);
43+
44+
// username
45+
let usernameField = screen.getByPlaceholderText(
46+
/username/i,
47+
) as HTMLInputElement;
48+
expect(usernameField.validity.valid).toBe(true);
49+
expect(usernameField.validationMessage).toBe('');
50+
51+
// password
52+
let passwordField = screen.getByPlaceholderText(
53+
/password/i,
54+
) as HTMLInputElement;
55+
expect(passwordField.validity.valid).toBe(true);
56+
expect(passwordField.validationMessage).toBe('');
57+
58+
await user.click(screen.getByText(/submit/i));
59+
60+
// username
61+
usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement;
62+
expect(usernameField.validity.valid).toBe(false);
63+
expect(usernameField.validationMessage).toBe(USERNAME_REQUIRED_MESSAGE);
64+
65+
// password
66+
passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement;
67+
expect(passwordField.validity.valid).toBe(false);
68+
expect(passwordField.validationMessage).toBe(PASSWORD_REQUIRED_MESSAGE);
69+
70+
await user.type(screen.getByPlaceholderText(/username/i), 'joe');
71+
await user.type(screen.getByPlaceholderText(/password/i), 'password');
72+
73+
// username
74+
usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement;
75+
expect(usernameField.validity.valid).toBe(true);
76+
expect(usernameField.validationMessage).toBe('');
77+
78+
// password
79+
passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement;
80+
expect(passwordField.validity.valid).toBe(true);
81+
expect(passwordField.validationMessage).toBe('');
82+
});

typeschema/src/__tests__/Form.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import user from '@testing-library/user-event';
4+
import { useForm } from 'react-hook-form';
5+
import type { Infer } from '@typeschema/main';
6+
import { z } from 'zod';
7+
import { typeschemaResolver } from '..';
8+
9+
const schema = z.object({
10+
username: z.string().min(1, { message: 'username field is required' }),
11+
password: z.string().min(1, { message: 'password field is required' }),
12+
});
13+
14+
type FormData = Infer<typeof schema> & { unusedProperty: string };
15+
16+
interface Props {
17+
onSubmit: (data: FormData) => void;
18+
}
19+
20+
function TestComponent({ onSubmit }: Props) {
21+
const {
22+
register,
23+
handleSubmit,
24+
formState: { errors },
25+
} = useForm<FormData>({
26+
resolver: typeschemaResolver(schema), // Useful to check TypeScript regressions
27+
});
28+
29+
return (
30+
<form onSubmit={handleSubmit(onSubmit)}>
31+
<input {...register('username')} />
32+
{errors.username && <span role="alert">{errors.username.message}</span>}
33+
34+
<input {...register('password')} />
35+
{errors.password && <span role="alert">{errors.password.message}</span>}
36+
37+
<button type="submit">submit</button>
38+
</form>
39+
);
40+
}
41+
42+
test("form's validation with TypeSchema and TypeScript's integration", async () => {
43+
const handleSubmit = vi.fn();
44+
render(<TestComponent onSubmit={handleSubmit} />);
45+
46+
expect(screen.queryAllByRole('alert')).toHaveLength(0);
47+
48+
await user.click(screen.getByText(/submit/i));
49+
50+
expect(screen.getByText(/username field is required/i)).toBeInTheDocument();
51+
expect(screen.getByText(/password field is required/i)).toBeInTheDocument();
52+
expect(handleSubmit).not.toHaveBeenCalled();
53+
});

0 commit comments

Comments
 (0)