Skip to content

Commit

Permalink
feat(typeboxResolver): make TypeBox resolver work with compiled schema (
Browse files Browse the repository at this point in the history
#674)

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

* docs(typeboxResolver): add docs for using TypeBox resolver with compiled schema

* chore: cleanup wrong indentation format
  • Loading branch information
huynhducduy authored Jul 2, 2024
1 parent c0d528b commit e8e0f80
Show file tree
Hide file tree
Showing 9 changed files with 412 additions and 5 deletions.
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)
- [effect-ts](#effect-ts)
Expand Down Expand Up @@ -488,6 +490,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 @@ -513,6 +517,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

0 comments on commit e8e0f80

Please sign in to comment.