├── .eslintrc.json
├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── jest.config.js
├── package-lock.json
├── package.json
├── src
├── __tests__
│ ├── ContactForm.tsx
│ ├── __snapshots__
│ │ ├── context.spec.tsx.snap
│ │ └── map.spec.tsx.snap
│ ├── context.spec.tsx
│ └── map.spec.tsx
├── context.tsx
└── index.ts
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true
4 | },
5 | "extends": [
6 | "plugin:react/recommended"
7 | ],
8 | "parser": "@typescript-eslint/parser",
9 | "parserOptions": {
10 | "project": "tsconfig.json",
11 | "sourceType": "module"
12 | },
13 | "ignorePatterns": [
14 | "src/graphql/generated.ts",
15 | "src/assets/locale/generated.ts",
16 | "src/__testutils__/*.ts",
17 | "src/**/__tests__/*"
18 | ],
19 | "plugins": [
20 | "import",
21 | "prefer-arrow",
22 | "react"
23 | ],
24 | "settings": {
25 | "react": {
26 | "pragma": "React",
27 | "version": "detect"
28 | }
29 | },
30 | "rules": {
31 | "arrow-body-style": "error",
32 | "arrow-parens": [
33 | "error",
34 | "always"
35 | ],
36 | "camelcase": "error",
37 | "comma-dangle": "off",
38 | "complexity": "off",
39 | "constructor-super": "error",
40 | "curly": "error",
41 | "default-case": "error",
42 | "dot-notation": "error",
43 | "eol-last": "error",
44 | "eqeqeq": [
45 | "error",
46 | "smart"
47 | ],
48 | "guard-for-in": "error",
49 | "id-blacklist": [
50 | "error",
51 | "any",
52 | "Number",
53 | "number",
54 | "String",
55 | "string",
56 | "Boolean",
57 | "boolean",
58 | "Undefined",
59 | "undefined"
60 | ],
61 | "id-match": "error",
62 | "import/order": [
63 | "error",
64 | {
65 | "alphabetize": {
66 | "order": "asc",
67 | "caseInsensitive": true
68 | }
69 | }
70 | ],
71 | "max-classes-per-file": [
72 | "error",
73 | 1
74 | ],
75 | "max-len": [
76 | "error",
77 | {
78 | "code": 140
79 | }
80 | ],
81 | "new-parens": "error",
82 | "no-bitwise": "error",
83 | "no-caller": "error",
84 | "no-cond-assign": "error",
85 | "no-console": [
86 | "error",
87 | {
88 | "allow": [
89 | "warn",
90 | "dir",
91 | "timeLog",
92 | "assert",
93 | "clear",
94 | "count",
95 | "countReset",
96 | "group",
97 | "groupEnd",
98 | "table",
99 | "dirxml",
100 | "groupCollapsed",
101 | "Console",
102 | "profile",
103 | "profileEnd",
104 | "timeStamp",
105 | "context"
106 | ]
107 | }
108 | ],
109 | "no-debugger": "error",
110 | "no-empty": "error",
111 | "no-eval": "error",
112 | "no-fallthrough": "error",
113 | "no-invalid-this": "off",
114 | "no-multiple-empty-lines": "error",
115 | "no-new-wrappers": "error",
116 | "no-redeclare": "error",
117 | "no-shadow": [
118 | "error",
119 | {
120 | "hoist": "all"
121 | }
122 | ],
123 | "no-throw-literal": "error",
124 | "no-trailing-spaces": "off",
125 | "no-undef-init": "error",
126 | "no-underscore-dangle": "off",
127 | "no-unsafe-finally": "error",
128 | "no-unused-expressions": "error",
129 | "no-unused-labels": "error",
130 | "no-var": "error",
131 | "object-shorthand": "error",
132 | "one-var": [
133 | "error",
134 | "never"
135 | ],
136 | "prefer-arrow/prefer-arrow-functions": "warn",
137 | "prefer-const": "error",
138 | "quote-props": [
139 | "error",
140 | "consistent-as-needed"
141 | ],
142 | "radix": "error",
143 | "react/prop-types": "off",
144 | "space-before-function-paren": [
145 | "error",
146 | {
147 | "anonymous": "never",
148 | "asyncArrow": "always",
149 | "named": "never"
150 | }
151 | ],
152 | "spaced-comment": "error",
153 | "use-isnan": "error",
154 | "valid-typeof": "off"
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 | defaults:
3 | run:
4 | shell: bash
5 | env:
6 | ACTIONS_ALLOW_UNSECURE_COMMANDS: true
7 | on:
8 | push:
9 | branches:
10 | - master
11 | pull_request: {}
12 | release:
13 | types: [created]
14 | jobs:
15 |
16 | build:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: actions/checkout@v2
20 | - name: Use Node.js
21 | uses: actions/setup-node@v2
22 | with:
23 | node-version: 17.x
24 | - run: npm ci
25 | - run: npm run build
26 | - run: npm run lint
27 | - run: npm test
28 | - uses: actions/upload-artifact@v2
29 | with:
30 | name: build-artifacts
31 | path: |
32 | coverage/
33 | dist/
34 | .npmignore
35 | package.json
36 | package-lock.json
37 | LICENSE
38 | README.md
39 |
40 | coverage:
41 | needs: build
42 | runs-on: ubuntu-latest
43 | steps:
44 | - if: success()
45 | uses: actions/download-artifact@v2
46 | id: download
47 | with:
48 | name: build-artifacts
49 | - uses: coverallsapp/github-action@1.1.3
50 | with:
51 | github-token: ${{ secrets.GITHUB_TOKEN }}
52 | path-to-lcov: coverage/lcov.info
53 |
54 | publish:
55 | needs: build
56 | if: ${{ github.event_name == 'release' }}
57 | runs-on: ubuntu-latest
58 | steps:
59 | - if: success()
60 | uses: actions/download-artifact@v2
61 | id: download
62 | with:
63 | name: build-artifacts
64 | - uses: actions/setup-node@v2
65 | with:
66 | node-version: 17.x
67 | - run: npm publish
68 | env:
69 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
70 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 | coverage/
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .github/
3 | src/
4 | node_modules/
5 | coverage/
6 |
7 | CODE_OF_CONDUCT.md
8 | CONTRIBUTING.md
9 |
10 | .eslintrc.json
11 | .gitignore
12 | .travis.yml
13 | tsconfig.json
14 |
15 | jest.config.js
16 | jest.setup.ts
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Robin Schultz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-class-validator
2 | Easy-to-use React hook for validating forms with the [class-validator](https://github.com/typestack/class-validator) library.
3 |
4 | [](https://github.com/anigenero/react-class-validator/actions/workflows/build.yml)
5 | [](https://codecov.io/gh/anigenero/react-class-validator)
6 |
7 | ## Installation
8 |
9 | ```bash
10 | npm install --save react-class-validator
11 | ```
12 |
13 | ```typescript jsx
14 |
15 | const validatorOptions: ValidatorContextOptions = {
16 | onErrorMessage: (error): string => {
17 | // custom error message handling (localization, etc)
18 | },
19 | resultType: 'boolean' // default, can also be set to 'map'
20 | }
21 |
22 | render((
23 |
24 |
25 |
26 | ), document.getElementById('root'))
27 | ```
28 |
29 | ## Default onErrorMessage behavior
30 | The default behavior is to flatten all error constraints for each attribute.
31 | ```typescript
32 | const getDefaultContextOptions = (): ValidatorContextOptions => ({
33 | onErrorMessage: (error) => Object.keys(error.constraints).map((key) => error.constraints[key])
34 | });
35 | ```
36 |
37 | ### react-intl
38 | When using libraries such as [react-intl](https://github.com/formatjs/formatjs), you don't have to modify the existing
39 | `onErrorMessage` handler. Decorators are handled at source load, so you only need to include the `intl.formatMessage` in your message definition.
40 |
41 | ```typescript
42 | class Person {
43 |
44 | @IsEmail({}, {
45 | message: intl.formatMessage(defineMessage({defaultMessage: 'Invalid email'}))
46 | })
47 | @IsNotEmpty({
48 | message: intl.formatMessage(defineMessage({defaultMessage: 'Email cannot be empty'}))
49 | })
50 | public email: string;
51 |
52 | }
53 |
54 | ```
55 |
56 | ## Usage
57 |
58 | Create a class using validation decorators from `class-validator`.
59 |
60 | ```typescript
61 | import { IsNotEmpty } from "class-validator";
62 |
63 | class LoginValidation {
64 |
65 | @IsNotEmpty({
66 | message: 'username cannot be empty'
67 | })
68 | public username: string;
69 |
70 | @IsNotEmpty({
71 | message: 'password cannot be empty'
72 | })
73 | public password: string;
74 |
75 | }
76 | ```
77 |
78 | Set up your form component to validate using your validation class.
79 |
80 | ```typescript jsx
81 | const MyComponent = () => {
82 |
83 | const [username, setUsername] = useState('');
84 | const [password, setPassword] = useState('');
85 |
86 | const [validate, errors] = useValidation(LoginValidation);
87 |
88 | return (
89 |
112 | );
113 |
114 | };
115 | ```
116 |
117 | ## Usage With Formik
118 |
119 | `react-class-validator` easily integrates with [Formik](https://formik.org/). You can simply use the `validate`
120 | function returned from `useValidation`, so long as the Formik fields are named the same as the keys in your validation
121 | class. Individual fields will have to be validated with `onBlur` functionality.
122 |
123 | ### Formik error messages
124 |
125 | To display error messages without custom handling, messages will need to be outputted as a map upon validation.
126 | Do this by overriding the default `resultType` (you can also do this at the component-level).
127 |
128 | ```typescript
129 | const options: ValidatorContextOptions = {
130 | resultType: 'map'
131 | };
132 | ```
133 |
134 | Then you can simply integrate with the default Formik flow.
135 |
136 | ```typescript jsx
137 | export const Login: FunctionComponent = () => {
138 |
139 | const [validate] = useValidation(LoginValidation);
140 |
141 | return (
142 |
146 | {({values, touched, errors, handleChange, handleBlur}) => (
147 |
163 | )}
164 |
165 | );
166 | };
167 | ```
168 |
169 | ## Contributors
170 | Library built and maintained by [Robin Schultz](http://anigenero.com)
171 |
172 | If you would like to contribute (aka buy me a beer), you can send funds via PayPal at the link below.
173 |
174 | [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=SLT7SZ2XFNEUQ)
175 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | coverageReporters: [
3 | 'lcov',
4 | 'html'
5 | ],
6 | coveragePathIgnorePatterns: [
7 | 'node_modules',
8 | '/src/__tests__'
9 | ],
10 | moduleFileExtensions: [
11 | 'ts',
12 | 'tsx',
13 | 'js'
14 | ],
15 | preset: 'ts-jest',
16 | transform: {
17 | '^.+\\.(js|jsx)?$': 'babel-jest'
18 | },
19 | transformIgnorePatterns: ['/node_modules/'],
20 | testEnvironment: 'jest-environment-jsdom-global',
21 | testMatch: [
22 | '**/?(*.)+(spec|test).[jt]s?(x)'
23 | ]
24 | };
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-class-validator",
3 | "version": "1.5.0",
4 | "description": "React hook for validation with class-validator",
5 | "main": "dist/index.js",
6 | "scripts": {
7 | "build": "tsc",
8 | "lint": "eslint src --ext .ts,.tsx",
9 | "test": "jest --coverage"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/anigenero/react-class-validator.git"
14 | },
15 | "keywords": [
16 | "class-validator",
17 | "validation",
18 | "form"
19 | ],
20 | "author": {
21 | "name": "Robin Schultz",
22 | "email": "robin@anigenero.com",
23 | "url": "https://github.com/anigenero"
24 | },
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/anigenero/react-class-validator/issues"
28 | },
29 | "homepage": "https://github.com/anigenero/react-class-validator#readme",
30 | "peerDependencies": {
31 | "react": "^18.0.0"
32 | },
33 | "devDependencies": {
34 | "@babel/core": "^7.12.13",
35 | "@types/jest": "^29.5.5",
36 | "@types/react": "^18.0.0",
37 | "@types/react-test-renderer": "^18.0.0",
38 | "@types/validator": "^13.7.2",
39 | "@typescript-eslint/parser": "^6.7.5",
40 | "babel-jest": "^29.7.0",
41 | "babel-loader": "^9.1.3",
42 | "eslint": "^8.14.0",
43 | "eslint-plugin-import": "^2.22.0",
44 | "eslint-plugin-prefer-arrow": "^1.2.3",
45 | "eslint-plugin-react": "^7.22.0",
46 | "jest": "^29.7.0",
47 | "jest-cli": "^29.7.0",
48 | "jest-environment-jsdom": "^29.7.0",
49 | "jest-environment-jsdom-global": "^4.0.0",
50 | "react": "^18.0.0",
51 | "react-dom": "^18.0.0",
52 | "react-test-renderer": "^18.0.0",
53 | "reflect-metadata": "^0.1.13",
54 | "ts-jest": "^29.1.1",
55 | "typescript": "^5.2.2"
56 | },
57 | "dependencies": {
58 | "class-validator": "^0.14.0"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/__tests__/ContactForm.tsx:
--------------------------------------------------------------------------------
1 | import {IsNotEmpty} from "class-validator";
2 | import React, {FunctionComponent, useState} from "react";
3 | import {useValidation} from "../index";
4 |
5 | class ContactFormValidation {
6 |
7 | @IsNotEmpty({
8 | message: 'First name cannot be empty'
9 | })
10 | public firstName: string;
11 |
12 | @IsNotEmpty({
13 | message: 'Last name cannot be empty'
14 | })
15 | public lastName: string;
16 |
17 | }
18 |
19 | export const ContactForm: FunctionComponent = () => {
20 |
21 | const [firstName, setFirstName] = useState('');
22 | const [lastName, setLastName] = useState('');
23 |
24 | const [validate, errorMessages] = useValidation(ContactFormValidation);
25 |
26 | return (
27 |
42 | );
43 |
44 | };
45 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/context.spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`context provider should mount correctly 1`] = `
4 |
21 | `;
22 |
23 | exports[`context validation error custom handler 1`] = `
24 |
47 | `;
48 |
49 | exports[`context validation error on blur field 1`] = `
50 |
70 | `;
71 |
72 | exports[`context validation error on form submit 1`] = `
73 |
93 | `;
94 |
95 | exports[`context validation success on form submit 1`] = `
96 |
113 | `;
114 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/map.spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`context provider should mount correctly 1`] = `
4 |
21 | `;
22 |
23 | exports[`context validation error custom handler 1`] = `
24 |
47 | `;
48 |
49 | exports[`context validation error on blur field 1`] = `
50 |
70 | `;
71 |
72 | exports[`context validation error on form submit 1`] = `
73 |
93 | `;
94 |
95 | exports[`context validation success on form submit 1`] = `
96 |
113 | `;
114 |
--------------------------------------------------------------------------------
/src/__tests__/context.spec.tsx:
--------------------------------------------------------------------------------
1 | import 'reflect-metadata';
2 |
3 | import React from "react";
4 | import {act, create} from "react-test-renderer";
5 | import {ValidatorProvider} from "../index";
6 | import {ContactForm} from "./ContactForm";
7 |
8 | describe('context', () => {
9 |
10 | it('provider should mount correctly', () => {
11 |
12 | const tree = create(
13 |
14 |
15 |
16 | ).toJSON();
17 |
18 | expect(tree).toMatchSnapshot();
19 |
20 | });
21 |
22 | it('validation success on form submit', async () => {
23 |
24 | const wrapper = create(
25 |
26 |
27 |
28 | );
29 |
30 | const firstNameInput = wrapper.root.findByProps({id: 'fname-input'});
31 | await act(() =>
32 | firstNameInput.props.onChange({target: {value: 'Nick'}})
33 | );
34 |
35 | const lastNameInput = wrapper.root.findByProps({id: 'lname-input'});
36 | await act(() =>
37 | lastNameInput.props.onChange({target: {value: 'Fury'}})
38 | );
39 |
40 | const form = wrapper.root.findByType('form');
41 | await act(() =>
42 | form.props.onSubmit({
43 | preventDefault: jest.fn()
44 | })
45 | );
46 |
47 | expect(wrapper.toJSON()).toMatchSnapshot();
48 |
49 | });
50 |
51 | it('validation error on form submit', async () => {
52 |
53 | const wrapper = create(
54 |
55 |
56 |
57 | );
58 |
59 | const firstNameInput = wrapper.root.findByProps({id: 'fname-input'});
60 | await act(() =>
61 | firstNameInput.props.onChange({target: {value: 'Nick'}})
62 | );
63 |
64 | const form = wrapper.root.findByType('form');
65 | await act(() =>
66 | form.props.onSubmit({
67 | preventDefault: jest.fn()
68 | })
69 | );
70 |
71 | expect(wrapper.toJSON()).toMatchSnapshot();
72 |
73 | });
74 |
75 | it('validation error on blur field', async () => {
76 |
77 | const wrapper = create(
78 |
79 |
80 |
81 | );
82 |
83 | const firstNameInput = wrapper.root.findByProps({id: 'fname-input'});
84 | await act(() =>
85 | firstNameInput.props.onBlur()
86 | );
87 |
88 | expect(wrapper.toJSON()).toMatchSnapshot();
89 |
90 | });
91 |
92 | it('validation error custom handler', async () => {
93 |
94 | const wrapper = create(
95 |
100 |
101 |
102 | );
103 |
104 | const form = wrapper.root.findByType('form');
105 | await act(() =>
106 | form.props.onSubmit({
107 | preventDefault: jest.fn()
108 | })
109 | );
110 |
111 | expect(wrapper.toJSON()).toMatchSnapshot();
112 |
113 | });
114 |
115 | });
116 |
--------------------------------------------------------------------------------
/src/__tests__/map.spec.tsx:
--------------------------------------------------------------------------------
1 | import 'reflect-metadata';
2 | import React from "react";
3 | import {act, create} from "react-test-renderer";
4 | import {ValidatorProvider} from "../index";
5 | import {ContactForm} from "./ContactForm";
6 |
7 | describe('context', () => {
8 |
9 | it('provider should mount correctly', () => {
10 |
11 | const tree = create(
12 |
13 |
14 |
15 | ).toJSON();
16 |
17 | expect(tree).toMatchSnapshot();
18 |
19 | });
20 |
21 | it('validation success on form submit', async () => {
22 |
23 | const wrapper = create(
24 |
25 |
26 |
27 | );
28 |
29 | const firstNameInput = wrapper.root.findByProps({id: 'fname-input'});
30 | await act(() =>
31 | firstNameInput.props.onChange({target: {value: 'Nick'}})
32 | );
33 |
34 | const lastNameInput = wrapper.root.findByProps({id: 'lname-input'});
35 | await act(() =>
36 | lastNameInput.props.onChange({target: {value: 'Fury'}})
37 | );
38 |
39 | const form = wrapper.root.findByType('form');
40 | await act(() =>
41 | form.props.onSubmit({
42 | preventDefault: jest.fn()
43 | })
44 | );
45 |
46 | expect(wrapper.toJSON()).toMatchSnapshot();
47 |
48 | });
49 |
50 | it('validation error on form submit', async () => {
51 |
52 | const wrapper = create(
53 |
54 |
55 |
56 | );
57 |
58 | const firstNameInput = wrapper.root.findByProps({id: 'fname-input'});
59 | await act(() =>
60 | firstNameInput.props.onChange({target: {value: 'Nick'}})
61 | );
62 |
63 | const form = wrapper.root.findByType('form');
64 | await act(() =>
65 | form.props.onSubmit({
66 | preventDefault: jest.fn()
67 | })
68 | );
69 |
70 | expect(wrapper.toJSON()).toMatchSnapshot();
71 |
72 | });
73 |
74 | it('validation error on blur field', async () => {
75 |
76 | const wrapper = create(
77 |
78 |
79 |
80 | );
81 |
82 | const firstNameInput = wrapper.root.findByProps({id: 'fname-input'});
83 | await act(() =>
84 | firstNameInput.props.onBlur()
85 | );
86 |
87 | expect(wrapper.toJSON()).toMatchSnapshot();
88 |
89 | });
90 |
91 | it('validation error custom handler', async () => {
92 |
93 | const wrapper = create(
94 |
100 |
101 |
102 | );
103 |
104 | const form = wrapper.root.findByType('form');
105 | await act(() =>
106 | form.props.onSubmit({
107 | preventDefault: jest.fn()
108 | })
109 | );
110 |
111 | expect(wrapper.toJSON()).toMatchSnapshot();
112 |
113 | });
114 |
115 | });
116 |
--------------------------------------------------------------------------------
/src/context.tsx:
--------------------------------------------------------------------------------
1 | import {ValidationError} from "class-validator";
2 | import React, {createContext, FunctionComponent, PropsWithChildren} from "react";
3 |
4 | export type ValidatorResultType = 'map' | 'boolean';
5 |
6 | export type OnErrorMessageHandler = (error: ValidationError) => string[];
7 | export type ValidatorContextOptions = {
8 | onErrorMessage?: OnErrorMessageHandler;
9 | resultType?: ValidatorResultType;
10 | };
11 |
12 | const defaultOnErrorMessage: OnErrorMessageHandler = (error) =>
13 | Object.keys(error.constraints).map((key) => error.constraints[key]);
14 |
15 | const getDefaultContextOptions = (): ValidatorContextOptions => ({
16 | onErrorMessage: defaultOnErrorMessage,
17 | resultType: 'boolean',
18 | });
19 |
20 | export const ValidatorContext = createContext(null);
21 |
22 | export const ValidatorProvider: FunctionComponent> =
23 | ({options = getDefaultContextOptions(), children}) => (
24 |
28 | {children}
29 |
30 | );
31 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import {validate} from 'class-validator';
2 | import {useContext, useState} from 'react';
3 | import {ValidatorContext, ValidatorContextOptions} from "./context";
4 |
5 | export {ValidatorProvider, ValidatorContextOptions, OnErrorMessageHandler} from './context';
6 |
7 | type Newable = {
8 | new(): T;
9 | } | Function;
10 |
11 | type ValidationOptions = Pick;
12 | type ValidationErrorMap = { [key in K]?: string[] };
13 | type ValidationPayload = { [key in K]?: T[K] };
14 | type ValidationFunction = (payload: ValidationPayload, filter?: K[]) =>
15 | Promise | boolean>;
16 | type UseValidationResult = [ValidationFunction, ValidationErrorMap];
17 |
18 | export const useValidation = (
19 | validationClass: Newable,
20 | opts: ValidationOptions = {}
21 | ): UseValidationResult => {
22 |
23 | const {onErrorMessage, resultType} = useContext(ValidatorContext);
24 | opts = {
25 | ...opts,
26 | resultType: opts.resultType || resultType
27 | }
28 |
29 | const [validationErrors, setErrors] = useState>({});
30 |
31 | const resolveErrors = (errors: ValidationErrorMap) => {
32 | if (errors && Object.keys(errors).length === 0 && errors.constructor === Object) {
33 | return opts.resultType === 'boolean' ? true : errors;
34 | } else {
35 | return opts.resultType === 'boolean' ? false : errors;
36 | }
37 | }
38 |
39 | const validateCallback: ValidationFunction = async (payload, filter: K[] = []) => {
40 |
41 | let errors = await validate(Object.assign(new (validationClass as any)(), payload));
42 | if (errors.length === 0) {
43 |
44 | setErrors({});
45 | return resolveErrors({});
46 |
47 | } else {
48 |
49 | if (filter.length > 0) {
50 | errors = errors.filter((err) => filter.includes(err.property as K));
51 | }
52 |
53 | const validation: ValidationErrorMap = errors.reduce(
54 | (acc, value) => ({
55 | ...acc,
56 | [value.property as K]: onErrorMessage(value)
57 | }),
58 | {} as ValidationErrorMap
59 | );
60 |
61 | if (filter.length > 0) {
62 |
63 | const filteredErrors =
64 | (Object.keys(validationErrors) as K[]).filter((key) =>
65 | !filter.includes(key)
66 | ).reduce((accum, key) => ({
67 | ...accum,
68 | [key]: validationErrors[key]
69 | }), {});
70 |
71 | setErrors({
72 | ...filteredErrors,
73 | ...validation
74 | });
75 |
76 | } else {
77 | setErrors(validation);
78 | }
79 |
80 | return resolveErrors(validation);
81 |
82 | }
83 |
84 | };
85 |
86 | return [validateCallback, validationErrors];
87 |
88 | };
89 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "outDir": "./dist/",
5 | "sourceMap": false,
6 | "noImplicitAny": true,
7 | "module": "commonjs",
8 | "target": "es5",
9 | "jsx": "react",
10 | "allowJs": false,
11 | "moduleResolution": "node",
12 | "esModuleInterop": true,
13 | "experimentalDecorators": true,
14 | "declaration": true,
15 | "declarationDir": "./dist/",
16 | "declarationMap": false,
17 | "inlineSourceMap": true,
18 | "lib": [
19 | "es2015",
20 | "dom"
21 | ]
22 | },
23 | "exclude": [
24 | "node_modules"
25 | ],
26 | "include": [
27 | "./src/index.ts"
28 | ]
29 | }
30 |
31 |
--------------------------------------------------------------------------------