Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(typeboxResolver): make TypeBox resolver work with compiled schema #674

Merged
merged 3 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
- [typanion](#typanion)
- [Ajv](#ajv)
- [TypeBox](#typebox)
- [With `ValueCheck`](#with-valuecheck)
- [With `TypeCompiler`](#with-typecompiler)
- [ArkType](#arktype)
- [Valibot](#valibot)
- [Backers](#backers)
Expand Down Expand Up @@ -486,6 +488,8 @@ JSON Schema Type Builder with Static Type Resolution for TypeScript

[![npm](https://img.shields.io/bundlephobia/minzip/@sinclair/typebox?style=for-the-badge)](https://bundlephobia.com/result?p=@sinclair/typebox)

#### With `ValueCheck`

```typescript jsx
import { useForm } from 'react-hook-form';
import { typeboxResolver } from '@hookform/resolvers/typebox';
Expand All @@ -511,6 +515,38 @@ const App = () => {
};
```

#### With `TypeCompiler`

A high-performance JIT of `TypeBox`, [read more](https://github.com/sinclairzx81/typebox#typecompiler)

```typescript jsx
import { useForm } from 'react-hook-form';
import { typeboxResolver } from '@hookform/resolvers/typebox';
import { Type } from '@sinclair/typebox';
import { TypeCompiler } from '@sinclair/typebox/compiler';

const schema = Type.Object({
username: Type.String({ minLength: 1 }),
password: Type.String({ minLength: 1 }),
});

const typecheck = TypeCompiler.Compile(schema);

const App = () => {
const { register, handleSubmit } = useForm({
resolver: typeboxResolver(typecheck),
});

return (
<form onSubmit={handleSubmit((d) => console.log(d))}>
<input {...register('username')} />
<input type="password" {...register('password')} />
<input type="submit" />
</form>
);
};
```

### [ArkType](https://github.com/arktypeio/arktype)

TypeScript's 1:1 validator, optimized from editor to runtime
Expand Down
57 changes: 57 additions & 0 deletions typebox/src/__tests__/Form-compiler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import user from '@testing-library/user-event';
import { useForm } from 'react-hook-form';
import { typeboxResolver } from '..';
import { Type, Static } from '@sinclair/typebox';
import { TypeCompiler } from '@sinclair/typebox/compiler';

const schema = Type.Object({
username: Type.String({ minLength: 1 }),
password: Type.String({ minLength: 1 }),
});

const typecheck = TypeCompiler.Compile(schema)

type FormData = Static<typeof schema> & { unusedProperty: string };

interface Props {
onSubmit: (data: FormData) => void;
}

function TestComponent({ onSubmit }: Props) {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: typeboxResolver(typecheck), // Useful to check TypeScript regressions
});

return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('username')} />
{errors.username && <span role="alert">{errors.username.message}</span>}

<input {...register('password')} />
{errors.password && <span role="alert">{errors.password.message}</span>}

<button type="submit">submit</button>
</form>
);
}

test("form's validation with Typebox (with compiler) and TypeScript's integration", async () => {
const handleSubmit = vi.fn();
render(<TestComponent onSubmit={handleSubmit} />);

expect(screen.queryAllByRole('alert')).toHaveLength(0);

await user.click(screen.getByText(/submit/i));

expect(
screen.getAllByText(/Expected string length greater or equal to 1/i),
).toHaveLength(2);

expect(handleSubmit).not.toHaveBeenCalled();
});
86 changes: 86 additions & 0 deletions typebox/src/__tests__/Form-native-validation-compiler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { render, screen } from '@testing-library/react';
import user from '@testing-library/user-event';
import { typeboxResolver } from '..';

import { Type, Static } from '@sinclair/typebox';
import { TypeCompiler } from '@sinclair/typebox/compiler';

const schema = Type.Object({
username: Type.String({ minLength: 1 }),
password: Type.String({ minLength: 1 }),
});

const typecheck = TypeCompiler.Compile(schema)

type FormData = Static<typeof schema>;

interface Props {
onSubmit: (data: FormData) => void;
}

function TestComponent({ onSubmit }: Props) {
const { register, handleSubmit } = useForm<FormData>({
resolver: typeboxResolver(typecheck),
shouldUseNativeValidation: true,
});

return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('username')} placeholder="username" />

<input {...register('password')} placeholder="password" />

<button type="submit">submit</button>
</form>
);
}

test("form's native validation with Typebox (with compiler)", async () => {
const handleSubmit = vi.fn();
render(<TestComponent onSubmit={handleSubmit} />);

// username
let usernameField = screen.getByPlaceholderText(
/username/i,
) as HTMLInputElement;
expect(usernameField.validity.valid).toBe(true);
expect(usernameField.validationMessage).toBe('');

// password
let passwordField = screen.getByPlaceholderText(
/password/i,
) as HTMLInputElement;
expect(passwordField.validity.valid).toBe(true);
expect(passwordField.validationMessage).toBe('');

await user.click(screen.getByText(/submit/i));

// username
usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement;
expect(usernameField.validity.valid).toBe(false);
expect(usernameField.validationMessage).toBe(
'Expected string length greater or equal to 1',
);

// password
passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement;
expect(passwordField.validity.valid).toBe(false);
expect(passwordField.validationMessage).toBe(
'Expected string length greater or equal to 1',
);

await user.type(screen.getByPlaceholderText(/username/i), 'joe');
await user.type(screen.getByPlaceholderText(/password/i), 'password');

// username
usernameField = screen.getByPlaceholderText(/username/i) as HTMLInputElement;
expect(usernameField.validity.valid).toBe(true);
expect(usernameField.validationMessage).toBe('');

// password
passwordField = screen.getByPlaceholderText(/password/i) as HTMLInputElement;
expect(passwordField.validity.valid).toBe(true);
expect(passwordField.validationMessage).toBe('');
});
2 changes: 1 addition & 1 deletion typebox/src/__tests__/Form-native-validation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function TestComponent({ onSubmit }: Props) {
);
}

test("form's native validation with Zod", async () => {
test("form's native validation with Typebox", async () => {
const handleSubmit = vi.fn();
render(<TestComponent onSubmit={handleSubmit} />);

Expand Down
2 changes: 1 addition & 1 deletion typebox/src/__tests__/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function TestComponent({ onSubmit }: Props) {
);
}

test("form's validation with Zod and TypeScript's integration", async () => {
test("form's validation with Typebox and TypeScript's integration", async () => {
const handleSubmit = vi.fn();
render(<TestComponent onSubmit={handleSubmit} />);

Expand Down
Loading
Loading