Skip to content

Commit 81e95d1

Browse files
committed
feat: add valita
1 parent ded1746 commit 81e95d1

12 files changed

+788
-2
lines changed

README.md

+33
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ Install your preferred validation library alongside `@hookform/resolvers`.
5353
| vine || `firstError | all` |
5454
| yup || `firstError | all` |
5555
| zod || `firstError | all` |
56+
| valita || `firstError` |
5657
</details>
5758

5859
## Links
@@ -87,6 +88,7 @@ Install your preferred validation library alongside `@hookform/resolvers`.
8788
- [VineJS](#vinejs)
8889
- [fluentvalidation-ts](#fluentvalidation-ts)
8990
- [standard-schema](#standard-schema)
91+
- [valita](#valita)
9092
- [Backers](#backers)
9193
- [Sponsors](#sponsors)
9294
- [Contributors](#contributors)
@@ -879,6 +881,37 @@ const App = () => {
879881
};
880882
```
881883

884+
### [valita](https://github.com/badrap/valita)
885+
886+
A typesafe validation & parsing library for TypeScript.
887+
888+
[![npm](https://img.shields.io/bundlephobia/minzip/@badrap/valita?style=for-the-badge)](https://bundlephobia.com/result?p=@badrap/valita)
889+
890+
```typescript jsx
891+
import { useForm } from 'react-hook-form';
892+
import { valitaResolver } from '@hookform/resolvers/valita';
893+
import * as v from '@badrap/valita';
894+
895+
const schema = v.object({
896+
username: v.string(),
897+
password: v.string(),
898+
});
899+
900+
const App = () => {
901+
const { register, handleSubmit } = useForm({
902+
resolver: valitaResolver(schema),
903+
});
904+
905+
return (
906+
<form onSubmit={handleSubmit((d) => console.log(d))}>
907+
<input {...register('username')} />
908+
<input type="password" {...register('password')} />
909+
<input type="submit" />
910+
</form>
911+
);
912+
};
913+
```
914+
882915
## Backers
883916

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

bun.lock

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"@standard-schema/utils": "^0.3.0",
88
},
99
"devDependencies": {
10+
"@badrap/valita": "^0.4.3",
1011
"@sinclair/typebox": "^0.34.15",
1112
"@standard-schema/spec": "^1.0.0",
1213
"@testing-library/dom": "^10.4.0",
@@ -274,6 +275,8 @@
274275

275276
"@babel/types": ["@babel/types@7.26.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg=="],
276277

278+
"@badrap/valita": ["@badrap/valita@0.4.3", "", {}, "sha512-C9iZSrVlTb610dxZ2oatK5LwefaHv0Q9eYfVDH3co846x7WinhCfc8jCDTE55yM8WxlmOfX2ckKmsSr7KzZ/gg=="],
279+
277280
"@csstools/color-helpers": ["@csstools/color-helpers@5.0.1", "", {}, "sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA=="],
278281

279282
"@csstools/css-calc": ["@csstools/css-calc@2.1.1", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3" } }, "sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag=="],

config/node-13-exports.js

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const subRepositories = [
2121
'vine',
2222
'fluentvalidation-ts',
2323
'standard-schema',
24+
'valita',
2425
];
2526

2627
const copySrc = () => {

package.json

+14-2
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@
129129
"import": "./standard-schema/dist/standard-schema.mjs",
130130
"require": "./standard-schema/dist/standard-schema.js"
131131
},
132+
"./valita": {
133+
"types": "./valita/dist/index.d.ts",
134+
"umd": "./valita/dist/valita.umd.js",
135+
"import": "./valita/dist/valita.mjs",
136+
"require": "./valita/dist/valita.js"
137+
},
132138
"./package.json": "./package.json",
133139
"./*": "./*"
134140
},
@@ -193,7 +199,10 @@
193199
"fluentvalidation-ts/dist",
194200
"standard-schema/package.json",
195201
"standard-schema/src",
196-
"standard-schema/dist"
202+
"standard-schema/dist",
203+
"valita/package.json",
204+
"valita/src",
205+
"valita/dist"
197206
],
198207
"publishConfig": {
199208
"access": "public"
@@ -221,6 +230,7 @@
221230
"build:vine": "microbundle --cwd vine --globals @hookform/resolvers=hookformResolvers,react-hook-form=ReactHookForm,@vinejs/vine=vine",
222231
"build:fluentvalidation-ts": "microbundle --cwd fluentvalidation-ts --globals @hookform/resolvers=hookformResolvers,react-hook-form=ReactHookForm",
223232
"build:standard-schema": "microbundle --cwd standard-schema --globals @hookform/resolvers=hookformResolvers,react-hook-form=ReactHookForm,@standard-schema/spec=standardSchema",
233+
"build:valita": "microbundle --cwd valita --globals @hookform/resolvers=hookformResolvers,react-hook-form=ReactHookForm,@badrap/valita=valita",
224234
"postbuild": "node ./config/node-13-exports.js && check-export-map",
225235
"lint": "bunx @biomejs/biome check --write --vcs-use-ignore-file=true .",
226236
"lint:types": "tsc",
@@ -254,7 +264,8 @@
254264
"typeschema",
255265
"vine",
256266
"fluentvalidation-ts",
257-
"standard-schema"
267+
"standard-schema",
268+
"valita"
258269
],
259270
"repository": {
260271
"type": "git",
@@ -267,6 +278,7 @@
267278
},
268279
"homepage": "https://react-hook-form.com",
269280
"devDependencies": {
281+
"@badrap/valita": "^0.4.3",
270282
"@sinclair/typebox": "^0.34.15",
271283
"@standard-schema/spec": "^1.0.0",
272284
"@testing-library/dom": "^10.4.0",

valita/package.json

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "@hookform/resolvers/valita",
3+
"amdName": "hookformResolversValita",
4+
"version": "1.0.0",
5+
"private": true,
6+
"description": "React Hook Form validation resolver: valita",
7+
"main": "dist/valita.js",
8+
"module": "dist/valita.module.js",
9+
"umd:main": "dist/valita.umd.js",
10+
"source": "src/index.ts",
11+
"types": "dist/index.d.ts",
12+
"license": "MIT",
13+
"peerDependencies": {
14+
"@badrap/valita": "0.4.x",
15+
"@hookform/resolvers": "^2.0.0",
16+
"react-hook-form": "^7.0.0"
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import * as v from '@badrap/valita';
2+
import { render, screen } from '@testing-library/react';
3+
import user from '@testing-library/user-event';
4+
import React from 'react';
5+
import { useForm } from 'react-hook-form';
6+
import { valitaResolver } from '..';
7+
8+
const USERNAME_REQUIRED_MESSAGE = 'username field is required';
9+
const PASSWORD_REQUIRED_MESSAGE = 'password field is required';
10+
const USERNAME_LENGTH_TOO_SHORT = 'username is too short';
11+
12+
function strRequired(message: string) {
13+
return (value: string) => {
14+
if (value === '') {
15+
return v.err(message);
16+
}
17+
return v.ok(value);
18+
};
19+
}
20+
21+
function strMinLength(min: number) {
22+
return (value: string) => {
23+
if (value.length < min) {
24+
return v.err(USERNAME_LENGTH_TOO_SHORT);
25+
}
26+
return v.ok(value);
27+
};
28+
}
29+
30+
const schema = v.object({
31+
username: v
32+
.string()
33+
.chain(strRequired(USERNAME_REQUIRED_MESSAGE))
34+
.chain(strMinLength(2)),
35+
password: v
36+
.string()
37+
.chain(strRequired(PASSWORD_REQUIRED_MESSAGE))
38+
.chain(strMinLength(2)),
39+
});
40+
41+
type FormData = { username: string; password: string };
42+
43+
interface Props {
44+
onSubmit: (data: FormData) => void;
45+
}
46+
47+
function TestComponent({ onSubmit }: Props) {
48+
const { register, handleSubmit } = useForm<FormData>({
49+
resolver: valitaResolver(schema),
50+
shouldUseNativeValidation: true,
51+
});
52+
53+
return (
54+
<form onSubmit={handleSubmit(onSubmit)}>
55+
<input {...register('username')} placeholder="username" />
56+
57+
<input {...register('password')} placeholder="password" />
58+
59+
<button type="submit">submit</button>
60+
</form>
61+
);
62+
}
63+
64+
test("form's native validation with valita", async () => {
65+
const handleSubmit = vi.fn();
66+
render(<TestComponent onSubmit={handleSubmit} />);
67+
68+
// username
69+
let usernameField = screen.getByPlaceholderText(
70+
/username/i,
71+
) as HTMLInputElement;
72+
expect(usernameField.validity.valid).toBe(true);
73+
expect(usernameField.validationMessage).toBe('');
74+
75+
// password
76+
let passwordField = screen.getByPlaceholderText(
77+
/password/i,
78+
) as HTMLInputElement;
79+
expect(passwordField.validity.valid).toBe(true);
80+
expect(passwordField.validationMessage).toBe('');
81+
82+
await user.click(screen.getByText(/submit/i));
83+
84+
// username
85+
usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement;
86+
expect(usernameField.validity.valid).toBe(false);
87+
expect(usernameField.validationMessage).toBe(USERNAME_REQUIRED_MESSAGE);
88+
89+
// password
90+
passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement;
91+
expect(passwordField.validity.valid).toBe(false);
92+
expect(passwordField.validationMessage).toBe(PASSWORD_REQUIRED_MESSAGE);
93+
94+
await user.type(screen.getByPlaceholderText(/password/i), 'password');
95+
96+
// password
97+
passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement;
98+
expect(passwordField.validity.valid).toBe(true);
99+
expect(passwordField.validationMessage).toBe('');
100+
});

valita/src/__tests__/Form.tsx

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import * as v from '@badrap/valita';
2+
import { render, screen } from '@testing-library/react';
3+
import user from '@testing-library/user-event';
4+
import React from 'react';
5+
import { useForm } from 'react-hook-form';
6+
import { valitaResolver } from '..';
7+
8+
const USERNAME_REQUIRED_MESSAGE = 'username field is required';
9+
const PASSWORD_REQUIRED_MESSAGE = 'password field is required';
10+
const USERNAME_LENGTH_TOO_SHORT = 'username is too short';
11+
12+
function strRequired(message: string) {
13+
return (value: string) => {
14+
if (value === '') {
15+
return v.err(message);
16+
}
17+
return v.ok(value);
18+
};
19+
}
20+
21+
function strMinLength(min: number) {
22+
return (value: string) => {
23+
if (value.length < min) {
24+
return v.err(USERNAME_LENGTH_TOO_SHORT);
25+
}
26+
return v.ok(value);
27+
};
28+
}
29+
30+
const schema = v.object({
31+
username: v
32+
.string()
33+
.chain(strRequired(USERNAME_REQUIRED_MESSAGE))
34+
.chain(strMinLength(2)),
35+
password: v
36+
.string()
37+
.chain(strRequired(PASSWORD_REQUIRED_MESSAGE))
38+
.chain(strMinLength(2)),
39+
});
40+
41+
type FormData = { username: string; password: string };
42+
43+
interface Props {
44+
onSubmit: (data: FormData) => void;
45+
}
46+
47+
function TestComponent({ onSubmit }: Props) {
48+
const {
49+
register,
50+
handleSubmit,
51+
formState: { errors },
52+
} = useForm<FormData>({
53+
resolver: valitaResolver(schema),
54+
});
55+
56+
return (
57+
<form onSubmit={handleSubmit(onSubmit)}>
58+
<input {...register('username')} placeholder="username" />
59+
{errors.username && <span role="alert">{errors.username.message}</span>}
60+
61+
<input {...register('password')} />
62+
{errors.password && <span role="alert">{errors.password.message}</span>}
63+
64+
<button type="submit">submit</button>
65+
</form>
66+
);
67+
}
68+
69+
describe('valita form validation errors', () => {
70+
test('ensure custom validation messages are shown', async () => {
71+
const handleSubmit = vi.fn();
72+
render(<TestComponent onSubmit={handleSubmit} />);
73+
74+
expect(screen.queryAllByRole('alert')).toHaveLength(0);
75+
76+
await user.type(screen.getByPlaceholderText('username'), 'a');
77+
await user.click(screen.getByText(/submit/i));
78+
79+
expect(screen.getByText(/username is too short/i)).toBeInTheDocument();
80+
expect(screen.getByText(/password field is required/i)).toBeInTheDocument();
81+
expect(handleSubmit).not.toHaveBeenCalled();
82+
});
83+
});
84+
85+
export function TestComponentManualType({
86+
onSubmit,
87+
}: {
88+
onSubmit: (data: FormData) => void;
89+
}) {
90+
const {
91+
register,
92+
handleSubmit,
93+
formState: { errors },
94+
} = useForm<v.Infer<typeof schema>, undefined, FormData>({
95+
resolver: valitaResolver(schema),
96+
});
97+
98+
return (
99+
<form onSubmit={handleSubmit(onSubmit)}>
100+
<input {...register('username')} />
101+
{errors.username && <span role="alert">{errors.username.message}</span>}
102+
103+
<input {...register('password')} />
104+
{errors.password && <span role="alert">{errors.password.message}</span>}
105+
106+
<button type="submit">submit</button>
107+
</form>
108+
);
109+
}

0 commit comments

Comments
 (0)