Skip to content

Commit 9ba2f89

Browse files
binaryartifexTrent Cox
and
Trent Cox
authored
feat: effect-ts resolver (#676)
* feat: effect-ts resolver * refactor(effect-ts): replace spread operator with explicit assignment * fix(effect-ts): provide build aliases for globals * fix(effect-ts): include effect-ts in node-13-exports config * fix(ci): bumped workflow pnpm action setups to version 9 * docs(effect-ts): add quickstart guide to readme * refactor(effect-ts): optimize imports for better tree shaking, add encode generic, allow for async transforms --------- Co-authored-by: Trent Cox <admin@havenworldtours.com>
1 parent 1bfc6ab commit 9ba2f89

File tree

16 files changed

+5004
-3696
lines changed

16 files changed

+5004
-3696
lines changed

.github/workflows/compressedSize.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
- name: Setup pnpm
1212
uses: pnpm/action-setup@v2
1313
with:
14-
version: 7
14+
version: 9.0
1515
- uses: preactjs/compressed-size-action@v2
1616
with:
1717
repo-token: '${{ secrets.GITHUB_TOKEN }}'

.github/workflows/main.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- name: Setup pnpm
2424
uses: pnpm/action-setup@v2
2525
with:
26-
version: 7
26+
version: 9.0
2727

2828
- name: Use Node.js ${{ matrix.node-version }}
2929
uses: actions/setup-node@v3
@@ -56,7 +56,7 @@ jobs:
5656
- name: Setup pnpm
5757
uses: pnpm/action-setup@v2
5858
with:
59-
version: 7
59+
version: 9.0
6060

6161
- name: Use Node.js ${{ matrix.node-version }}
6262
uses: actions/setup-node@v3

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"prettier.configPath": "./prettier.config.cjs",
99
"editor.formatOnSave": true,
1010
"editor.codeActionsOnSave": {
11-
"source.fixAll.eslint": true
11+
"source.fixAll.eslint": "explicit"
1212
},
1313
"typescript.tsdk": "node_modules/typescript/lib"
1414
}

README.md

Lines changed: 52 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+
- [effect-ts](#effect-ts)
4849
- [Backers](#backers)
4950
- [Sponsors](#sponsors)
5051
- [Contributors](#contributors)
@@ -576,6 +577,57 @@ const App = () => {
576577
};
577578
```
578579

580+
### [effect-ts](https://github.com/Effect-TS/effect)
581+
582+
A powerful TypeScript framework that provides a fully-fledged functional effect system with a rich standard library.
583+
584+
[![npm](https://img.shields.io/bundlephobia/minzip/effect?style=for-the-badge)]
585+
586+
```typescript jsx
587+
import React from 'react';
588+
import { useForm } from 'react-hook-form';
589+
import { effectTsResolver } from '@hookform/resolvers/effect-ts';
590+
import { Schema } from '@effect/schema';
591+
592+
const schema = Schema.Struct({
593+
username: Schema.String.pipe(
594+
Schema.nonEmpty({ message: () => 'username required' }),
595+
),
596+
password: Schema.String.pipe(
597+
Schema.nonEmpty({ message: () => 'password required' }),
598+
),
599+
});
600+
601+
type FormData = Schema.Schema.Type<typeof schema>;
602+
603+
interface Props {
604+
onSubmit: (data: FormData) => void;
605+
}
606+
607+
function TestComponent({ onSubmit }: Props) {
608+
const {
609+
register,
610+
handleSubmit,
611+
formState: { errors },
612+
// provide generic if TS has issues inferring types
613+
} = useForm<FormData>({
614+
resolver: effectTsResolver(schema),
615+
});
616+
617+
return (
618+
<form onSubmit={handleSubmit(onSubmit)}>
619+
<input {...register('username')} />
620+
{errors.username && <span role="alert">{errors.username.message}</span>}
621+
622+
<input {...register('password')} />
623+
{errors.password && <span role="alert">{errors.password.message}</span>}
624+
625+
<button type="submit">submit</button>
626+
</form>
627+
);
628+
}
629+
```
630+
579631
## Backers
580632

581633
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+
'effect-ts',
1920
];
2021

2122
const copySrc = () => {

effect-ts/package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "@hookform/resolvers/effect-ts",
3+
"amdName": "hookformResolversEffectTs",
4+
"version": "1.0.0",
5+
"private": true,
6+
"description": "React Hook Form validation resolver: effect-ts",
7+
"main": "dist/effect-ts.js",
8+
"module": "dist/effect-ts.module.js",
9+
"umd:main": "dist/effect-ts.umd.js",
10+
"source": "src/index.ts",
11+
"types": "dist/index.d.ts",
12+
"license": "MIT",
13+
"peerDependencies": {
14+
"@hookform/resolvers": "^2.0.0",
15+
"@effect/schema": "^0.66.14",
16+
"effect": "^3.1.2",
17+
"react-hook-form": "^7.0.0"
18+
}
19+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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 { effectTsResolver } from '..';
6+
import { Schema } from '@effect/schema';
7+
8+
const USERNAME_REQUIRED_MESSAGE = 'username field is required';
9+
const PASSWORD_REQUIRED_MESSAGE = 'password field is required';
10+
11+
const schema = Schema.Struct({
12+
username: Schema.String.pipe(
13+
Schema.nonEmpty({ message: () => USERNAME_REQUIRED_MESSAGE }),
14+
),
15+
password: Schema.String.pipe(
16+
Schema.nonEmpty({ message: () => PASSWORD_REQUIRED_MESSAGE }),
17+
),
18+
});
19+
20+
interface FormData {
21+
username: string;
22+
password: string;
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: effectTsResolver(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 effect-ts", async () => {
47+
const handleSubmit = vi.fn();
48+
render(<TestComponent onSubmit={handleSubmit} />);
49+
50+
// username
51+
let usernameField = screen.getByPlaceholderText(
52+
/username/i,
53+
) as HTMLInputElement;
54+
expect(usernameField.validity.valid).toBe(true);
55+
expect(usernameField.validationMessage).toBe('');
56+
57+
// password
58+
let passwordField = screen.getByPlaceholderText(
59+
/password/i,
60+
) as HTMLInputElement;
61+
expect(passwordField.validity.valid).toBe(true);
62+
expect(passwordField.validationMessage).toBe('');
63+
64+
await user.click(screen.getByText(/submit/i));
65+
66+
// username
67+
usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement;
68+
expect(usernameField.validity.valid).toBe(false);
69+
expect(usernameField.validationMessage).toBe(USERNAME_REQUIRED_MESSAGE);
70+
71+
// password
72+
passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement;
73+
expect(passwordField.validity.valid).toBe(false);
74+
expect(passwordField.validationMessage).toBe(PASSWORD_REQUIRED_MESSAGE);
75+
76+
await user.type(screen.getByPlaceholderText(/username/i), 'joe');
77+
await user.type(screen.getByPlaceholderText(/password/i), 'password');
78+
79+
// username
80+
usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement;
81+
expect(usernameField.validity.valid).toBe(true);
82+
expect(usernameField.validationMessage).toBe('');
83+
84+
// password
85+
passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement;
86+
expect(passwordField.validity.valid).toBe(true);
87+
expect(passwordField.validationMessage).toBe('');
88+
});

effect-ts/src/__tests__/Form.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 { effectTsResolver } from '..';
6+
import { Schema } from '@effect/schema';
7+
8+
const USERNAME_REQUIRED_MESSAGE = 'username field is required';
9+
const PASSWORD_REQUIRED_MESSAGE = 'password field is required';
10+
11+
const schema = Schema.Struct({
12+
username: Schema.String.pipe(
13+
Schema.nonEmpty({ message: () => USERNAME_REQUIRED_MESSAGE }),
14+
),
15+
password: Schema.String.pipe(
16+
Schema.nonEmpty({ message: () => PASSWORD_REQUIRED_MESSAGE }),
17+
),
18+
});
19+
20+
type FormData = Schema.Schema.Type<typeof schema>;
21+
22+
interface Props {
23+
onSubmit: (data: FormData) => void;
24+
}
25+
26+
function TestComponent({ onSubmit }: Props) {
27+
const {
28+
register,
29+
handleSubmit,
30+
formState: { errors },
31+
} = useForm({
32+
resolver: effectTsResolver(schema),
33+
});
34+
35+
return (
36+
<form onSubmit={handleSubmit(onSubmit)}>
37+
<input {...register('username')} />
38+
{errors.username && <span role="alert">{errors.username.message}</span>}
39+
40+
<input {...register('password')} />
41+
{errors.password && <span role="alert">{errors.password.message}</span>}
42+
43+
<button type="submit">submit</button>
44+
</form>
45+
);
46+
}
47+
48+
test("form's validation with Zod and TypeScript's integration", async () => {
49+
const handleSubmit = vi.fn();
50+
render(<TestComponent onSubmit={handleSubmit} />);
51+
52+
expect(screen.queryAllByRole('alert')).toHaveLength(0);
53+
54+
await user.click(screen.getByText(/submit/i));
55+
56+
expect(screen.getByText(/username field is required/i)).toBeInTheDocument();
57+
expect(screen.getByText(/password field is required/i)).toBeInTheDocument();
58+
expect(handleSubmit).not.toHaveBeenCalled();
59+
});

0 commit comments

Comments
 (0)