diff --git a/README.md b/README.md
index 2bebf0d1..cba09d80 100644
--- a/README.md
+++ b/README.md
@@ -324,6 +324,40 @@ const App = () => {
export default App;
```
+### [Nope](https://github.com/bvego/nope-validator)
+
+A small, simple, and fast JS validator
+
+[![npm](https://img.shields.io/bundlephobia/minzip/nope-validator?style=for-the-badge)](https://bundlephobia.com/result?p=nope-validator)
+
+```typescript jsx
+import React from 'react';
+import { useForm } from 'react-hook-form';
+import { nopeResolver } from '@hookform/resolvers/nope';
+import Nope from 'nope-validator';
+
+const schema = Nope.object().shape({
+ name: Nope.string().required(),
+ age: Nope.number().required(),
+});
+
+const App = () => {
+ const { register, handleSubmit } = useForm({
+ resolver: nopeResolver(schema),
+ });
+
+ return (
+
+ );
+};
+
+export default App;
+```
+
## Backers
Thanks goes to all our backers! [[Become a backer](https://opencollective.com/react-hook-form#backer)].
diff --git a/config/node-13-exports.js b/config/node-13-exports.js
index 397ec25f..9c80b09c 100644
--- a/config/node-13-exports.js
+++ b/config/node-13-exports.js
@@ -9,6 +9,7 @@ const subRepositories = [
'superstruct',
'class-validator',
'io-ts',
+ 'nope',
];
const copySrc = () => {
diff --git a/nope/package.json b/nope/package.json
new file mode 100644
index 00000000..ded13c24
--- /dev/null
+++ b/nope/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "nope",
+ "amdName": "hookformResolversNope",
+ "version": "1.0.0",
+ "private": true,
+ "description": "React Hook Form validation resolver: nope",
+ "main": "dist/nope.js",
+ "module": "dist/nope.module.js",
+ "umd:main": "dist/nope.umd.js",
+ "source": "src/index.ts",
+ "types": "dist/index.d.ts",
+ "license": "MIT",
+ "peerDependencies": {
+ "react-hook-form": "^7.0.0",
+ "@hookform/resolvers": "^2.0.0",
+ "nope-validator": "^0.12.0"
+ }
+}
diff --git a/nope/src/__tests__/Form.tsx b/nope/src/__tests__/Form.tsx
new file mode 100644
index 00000000..f08f8428
--- /dev/null
+++ b/nope/src/__tests__/Form.tsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import { render, screen, act } from '@testing-library/react';
+import user from '@testing-library/user-event';
+import { useForm } from 'react-hook-form';
+import Nope from 'nope-validator';
+import { nopeResolver } from '..';
+
+const schema = Nope.object().shape({
+ username: Nope.string().required(),
+ password: Nope.string().required(),
+});
+
+interface FormData {
+ unusedProperty: string;
+ username: string;
+ password: string;
+}
+
+interface Props {
+ onSubmit: (data: FormData) => void;
+}
+
+function TestComponent({ onSubmit }: Props) {
+ const {
+ register,
+ formState: { errors },
+ handleSubmit,
+ } = useForm({
+ resolver: nopeResolver(schema), // Useful to check TypeScript regressions
+ });
+
+ return (
+
+ );
+}
+
+test("form's validation with Yup and TypeScript's integration", async () => {
+ const handleSubmit = jest.fn();
+ render();
+
+ expect(screen.queryAllByRole(/alert/i)).toHaveLength(0);
+
+ await act(async () => {
+ user.click(screen.getByText(/submit/i));
+ });
+
+ expect(screen.getAllByText(/This field is required/i)).toHaveLength(2);
+ expect(handleSubmit).not.toHaveBeenCalled();
+});
diff --git a/nope/src/__tests__/__fixtures__/data.ts b/nope/src/__tests__/__fixtures__/data.ts
new file mode 100644
index 00000000..c47fd60f
--- /dev/null
+++ b/nope/src/__tests__/__fixtures__/data.ts
@@ -0,0 +1,70 @@
+import { Field, InternalFieldName } from 'react-hook-form';
+import Nope from 'nope-validator';
+
+export const schema = Nope.object().shape({
+ username: Nope.string().regex(/^\w+$/).min(2).max(30).required(),
+ password: Nope.string()
+ .regex(new RegExp('.*[A-Z].*'), 'One uppercase character')
+ .regex(new RegExp('.*[a-z].*'), 'One lowercase character')
+ .regex(new RegExp('.*\\d.*'), 'One number')
+ .regex(
+ new RegExp('.*[`~<>?,./!@#$%^&*()\\-_+="\'|{}\\[\\];:\\\\].*'),
+ 'One special character',
+ )
+ .min(8, 'Must be at least 8 characters in length')
+ .required('New Password is required'),
+ repeatPassword: Nope.string()
+ .oneOf([Nope.ref('password')], "Passwords don't match")
+ .required(),
+ accessToken: Nope.string(),
+ birthYear: Nope.number().min(1900).max(2013),
+ email: Nope.string().email(),
+ tags: Nope.array().of(Nope.string()).required(),
+ enabled: Nope.boolean(),
+ like: Nope.object().shape({
+ id: Nope.number().required(),
+ name: Nope.string().atLeast(4).required(),
+ }),
+});
+
+export const validData = {
+ username: 'Doe',
+ password: 'Password123_',
+ repeatPassword: 'Password123_',
+ birthYear: 2000,
+ email: 'john@doe.com',
+ tags: ['tag1', 'tag2'],
+ enabled: true,
+ accessToken: 'accessToken',
+ like: {
+ id: 1,
+ name: 'name',
+ },
+};
+
+export const invalidData = {
+ password: '___',
+ email: '',
+ birthYear: 'birthYear',
+ like: { id: 'z' },
+ tags: [1, 2, 3],
+};
+
+export const fields: Record = {
+ username: {
+ ref: { name: 'username' },
+ name: 'username',
+ },
+ password: {
+ ref: { name: 'password' },
+ name: 'password',
+ },
+ email: {
+ ref: { name: 'email' },
+ name: 'email',
+ },
+ birthday: {
+ ref: { name: 'birthday' },
+ name: 'birthday',
+ },
+};
diff --git a/nope/src/__tests__/__snapshots__/nope.ts.snap b/nope/src/__tests__/__snapshots__/nope.ts.snap
new file mode 100644
index 00000000..e5daad6b
--- /dev/null
+++ b/nope/src/__tests__/__snapshots__/nope.ts.snap
@@ -0,0 +1,43 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`nopeResolver should return a single error from nopeResolver when validation fails 1`] = `
+Object {
+ "errors": Object {
+ "birthYear": Object {
+ "message": "The field is not a valid number",
+ "ref": undefined,
+ },
+ "like": Object {
+ "id": Object {
+ "message": "The field is not a valid number",
+ "ref": undefined,
+ },
+ "name": Object {
+ "message": "This field is required",
+ "ref": undefined,
+ },
+ },
+ "password": Object {
+ "message": "One uppercase character",
+ "ref": Object {
+ "name": "password",
+ },
+ },
+ "repeatPassword": Object {
+ "message": "This field is required",
+ "ref": undefined,
+ },
+ "tags": Object {
+ "message": "One or more elements are of invalid type",
+ "ref": undefined,
+ },
+ "username": Object {
+ "message": "This field is required",
+ "ref": Object {
+ "name": "username",
+ },
+ },
+ },
+ "values": Object {},
+}
+`;
diff --git a/nope/src/__tests__/nope.ts b/nope/src/__tests__/nope.ts
new file mode 100644
index 00000000..be8c81b5
--- /dev/null
+++ b/nope/src/__tests__/nope.ts
@@ -0,0 +1,24 @@
+/* eslint-disable no-console, @typescript-eslint/ban-ts-comment */
+import { nopeResolver } from '..';
+import { schema, validData, fields, invalidData } from './__fixtures__/data';
+
+describe('nopeResolver', () => {
+ it('should return values from nopeResolver when validation pass', async () => {
+ const schemaSpy = jest.spyOn(schema, 'validate');
+
+ const result = await nopeResolver(schema)(validData, undefined, {
+ fields,
+ });
+
+ expect(schemaSpy).toHaveBeenCalledTimes(1);
+ expect(result).toEqual({ errors: {}, values: validData });
+ });
+
+ it('should return a single error from nopeResolver when validation fails', async () => {
+ const result = await nopeResolver(schema)(invalidData, undefined, {
+ fields,
+ });
+
+ expect(result).toMatchSnapshot();
+ });
+});
diff --git a/nope/src/index.ts b/nope/src/index.ts
new file mode 100644
index 00000000..70d2e237
--- /dev/null
+++ b/nope/src/index.ts
@@ -0,0 +1,2 @@
+export * from './nope';
+export * from './types';
diff --git a/nope/src/nope.ts b/nope/src/nope.ts
new file mode 100644
index 00000000..dc279b68
--- /dev/null
+++ b/nope/src/nope.ts
@@ -0,0 +1,40 @@
+import type { FieldErrors } from 'react-hook-form';
+import { toNestError } from '@hookform/resolvers';
+import type { ShapeErrors } from 'nope-validator/lib/cjs/types';
+import type { Resolver } from './types';
+
+const parseErrors = (
+ errors: ShapeErrors,
+ parsedErrors: FieldErrors = {},
+ path = '',
+) => {
+ return Object.keys(errors).reduce((acc, key) => {
+ const _path = path ? `${path}.${key}` : key;
+ const error = errors[key];
+
+ if (typeof error === 'string') {
+ acc[_path] = {
+ message: error,
+ };
+ } else {
+ parseErrors(error, acc, _path);
+ }
+
+ return acc;
+ }, parsedErrors);
+};
+
+export const nopeResolver: Resolver = (
+ schema,
+ schemaOptions = {
+ abortEarly: false,
+ },
+) => (values, context, options) => {
+ const result = schema.validate(values, context, schemaOptions) as
+ | ShapeErrors
+ | undefined;
+
+ return result
+ ? { values: {}, errors: toNestError(parseErrors(result), options.fields) }
+ : { values, errors: {} };
+};
diff --git a/nope/src/types.ts b/nope/src/types.ts
new file mode 100644
index 00000000..b1a283fe
--- /dev/null
+++ b/nope/src/types.ts
@@ -0,0 +1,19 @@
+import type {
+ FieldValues,
+ ResolverOptions,
+ ResolverResult,
+ UnpackNestedValue,
+} from 'react-hook-form';
+import type NopeObject from 'nope-validator/lib/cjs/NopeObject';
+
+type ValidateOptions = Parameters[2];
+type Context = Parameters[1];
+
+export type Resolver = (
+ schema: T,
+ schemaOptions?: ValidateOptions,
+) => (
+ values: UnpackNestedValue,
+ context: TContext | undefined,
+ options: ResolverOptions,
+) => ResolverResult;
diff --git a/package.json b/package.json
index d0778761..87ff0324 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "@hookform/resolvers",
"amdName": "hookformResolvers",
"version": "1.3.1",
- "description": "React Hook Form validation resolvers: Yup, Joi, Superstruct, Zod, Vest, Class Validator and etc.",
+ "description": "React Hook Form validation resolvers: Yup, Joi, Superstruct, Zod, Vest, Class Validator, io-ts and Nope.",
"main": "dist/resolvers.js",
"module": "dist/resolvers.module.js",
"umd:main": "dist/resolvers.umd.js",
@@ -57,6 +57,12 @@
"import": "./io-ts/dist/io-ts.mjs",
"require": "./io-ts/dist/io-ts.js"
},
+ "./nope": {
+ "browser": "./nope/dist/nope.module.js",
+ "umd": "./nope/dist/nope.umd.js",
+ "import": "./nope/dist/nope.mjs",
+ "require": "./nope/dist/nope.js"
+ },
"./package.json": "./package.json",
"./": "./"
},
@@ -82,7 +88,10 @@
"class-validator/dist",
"io-ts/package.json",
"io-ts/src",
- "io-ts/dist"
+ "io-ts/dist",
+ "nope/package.json",
+ "nope/src",
+ "nope/dist"
],
"publishConfig": {
"access": "public"
@@ -98,6 +107,7 @@
"build:io-ts": "microbundle --cwd io-ts --globals '@hookform/resolvers=hookformResolvers'",
"build:vest": "microbundle --cwd vest --globals '@hookform/resolvers=hookformResolvers'",
"build:class-validator": "microbundle --cwd class-validator --globals '@hookform/resolvers=hookformResolvers'",
+ "build:nope": "microbundle --cwd nope --globals '@hookform/resolvers=hookformResolvers'",
"postbuild": "node ./config/node-13-exports.js",
"lint": "eslint . --ext .ts,.js --ignore-path .gitignore",
"lint:types": "tsc",
@@ -118,7 +128,8 @@
"zod",
"vest",
"class-validator",
- "io-ts"
+ "io-ts",
+ "nope"
],
"repository": {
"type": "git",
@@ -154,6 +165,7 @@
"microbundle": "^0.13.0",
"monocle-ts": "^2.3.9",
"newtype-ts": "^0.3.4",
+ "nope-validator": "^0.12.2",
"npm-run-all": "^4.1.5",
"prettier": "^2.2.1",
"react": "^17.0.1",
diff --git a/yarn.lock b/yarn.lock
index 7d9c93f5..a5299af8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4961,6 +4961,11 @@ node-releases@^1.1.67:
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.67.tgz#28ebfcccd0baa6aad8e8d4d8fe4cbc49ae239c12"
integrity sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==
+nope-validator@^0.12.2:
+ version "0.12.2"
+ resolved "https://registry.yarnpkg.com/nope-validator/-/nope-validator-0.12.2.tgz#7860c9d1ebed5d5d1d6819d19ae25779206663de"
+ integrity sha512-anjqqIqNKxRlo3mBfm1V4V9ShEkj+65BHEvq9yIESjaeWwAJELQl354uQUG2swjk/k6Gifc9L+RQ0iek6X34/w==
+
normalize-package-data@^2.3.2, normalize-package-data@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"