├── .babelrc
├── .env
├── .gitignore
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── App.css
├── App.js
├── App.test.js
├── example
│ └── Form
│ │ ├── index.css
│ │ └── index.js
├── index.css
├── index.js
├── lib
│ ├── index.d.ts
│ ├── index.js
│ └── utils.js
└── logo.svg
└── tsconfig.json
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [["react-app", { "absoluteRuntime": false }]]
3 | }
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | SKIP_PREFLIGHT_CHECK=true
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 | /dist
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Vince Llauderes
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 useForm Hooks
2 |
3 | A simple and declarative way to validate your forms in React.
4 |
5 | # Example usage
6 |
7 | Create your stateSchema
8 |
9 | Your **stateSchema** is an object key value-pair contains the value of an input and the current error if any.
10 |
11 | ```
12 | const stateSchema = {
13 | first_name: { value: '', error: '', },
14 | last_name: { value: '', error: '', },
15 | tags: { value: '', error: '', },
16 | };
17 | ```
18 |
19 | Create validation in your stateSchema
20 |
21 | Property should be the same in your stateSchema in-order validation works. **validator** prop accepts a **func** that you can be use to validate your state via regEx.
22 |
23 | ```
24 | const stateValidatorSchema = {
25 | first_name: {
26 | required: true,
27 | validator: {
28 | func: value => /^[a-zA-Z]+$/.test(value),
29 | error: 'Invalid first name format.',
30 | },
31 | },
32 | last_name: {
33 | required: true,
34 | validator: {
35 | func: value => /^[a-zA-Z]+$/.test(value),
36 | error: 'Invalid last name format.',
37 | },
38 | },
39 | tags: {
40 | required: true,
41 | validator: {
42 | func: value => /^(,?\w{3,})+$/.test(value),
43 | error: 'Invalid tag format.',
44 | },
45 | },
46 | };
47 | ```
48 |
49 | Passed your stateSchema and stateValidatorSchema in useForm hooks. 3rd parameter is (optional) callback function to be called when you submit your form if all fields is valid.
50 |
51 | ```
52 | const {
53 | values,
54 | errors,
55 | handleOnChange,
56 | handleOnSubmit,
57 | disable
58 | } = useForm(stateSchema, stateValidatorSchema, onSubmitForm);
59 | ```
60 |
61 | # Demo
62 |
63 | Working demo [here](https://codesandbox.io/s/react-form-validation-v7k5z).
64 |
65 | # Contributing
66 |
67 | Feel free to contribute in this project.
68 |
69 | ## License
70 |
71 | This project is under [MIT License](https://github.com/llauderesv/react-form-validation/blob/master/LICENSE).
72 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-useform-validation",
3 | "version": "1.0.0",
4 | "private": false,
5 | "main": "dist/index.js",
6 | "module": "dist/index.js",
7 | "files": [
8 | "dist",
9 | "README.md"
10 | ],
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/llauderesv/react-form-validation"
14 | },
15 | "dependencies": {},
16 | "peerDependencies": {
17 | "react": "16.10.2",
18 | "react-dom": "16.10.2"
19 | },
20 | "scripts": {
21 | "start": "react-scripts start",
22 | "build": "rm -rf dist && NODE_ENV=production npx babel src/lib --out-dir dist --copy-files --ignore __tests__,spec.js,test.js,__snapshots__",
23 | "test": "react-scripts test",
24 | "eject": "react-scripts eject"
25 | },
26 | "eslintConfig": {
27 | "extends": "react-app"
28 | },
29 | "browserslist": {
30 | "production": [
31 | ">0.2%",
32 | "not dead",
33 | "not op_mini all"
34 | ],
35 | "development": [
36 | "last 1 chrome version",
37 | "last 1 firefox version",
38 | "last 1 safari version"
39 | ]
40 | },
41 | "devDependencies": {
42 | "@babel/cli": "^7.12.8",
43 | "@babel/core": "^7.12.9",
44 | "@types/react": "^17.0.0",
45 | "babel-preset-react": "^6.24.1",
46 | "lodash": "^4.17.20",
47 | "lodash.template": "4.5.0",
48 | "react-scripts": "3.3.0"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llauderesv/react-form-validation/da6add374a88640a59a53c2cb9e86155a2d4dfac/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | React App
23 |
24 |
25 |
26 |
27 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | div {
2 | margin-bottom: 10px;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 40vmin;
8 | pointer-events: none;
9 | height: 80px;
10 | }
11 |
12 | .App-header {
13 | background-color: #282c34;
14 | min-height: 250px;
15 | display: flex;
16 | flex-direction: column;
17 | align-items: center;
18 | justify-content: center;
19 | font-size: calc(10px + 2vmin);
20 | color: white;
21 | }
22 |
23 | .App-link {
24 | color: #61dafb;
25 | }
26 |
27 | @keyframes App-logo-spin {
28 | from {
29 | transform: rotate(0deg);
30 | }
31 | to {
32 | transform: rotate(360deg);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Form from './example/Form';
3 |
4 | import logo from './logo.svg';
5 | import './App.css';
6 |
7 | function App() {
8 | return (
9 |
10 |
11 |

12 |
React Form Validation
13 |
14 | A simple and easiest way to validate your forms in React using Hooks.
15 |
16 |
17 |
18 |
19 | );
20 | }
21 |
22 | export default App;
23 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, unmountComponentAtNode } from 'react-dom';
3 | import App from './App';
4 |
5 | it('Should render w/out crashing', () => {
6 | const div = document.createElement('div');
7 |
8 | render(, div);
9 | unmountComponentAtNode(div);
10 | });
11 |
--------------------------------------------------------------------------------
/src/example/Form/index.css:
--------------------------------------------------------------------------------
1 | .my-form {
2 | position: absolute;
3 | left: 50%;
4 | transform: translateX(-50%);
5 | }
6 |
7 | .my-form p {
8 | margin: 0;
9 | }
10 |
11 | .my-form .error {
12 | color: rgb(247, 70, 70);
13 | font-size: 13px;
14 | }
15 |
--------------------------------------------------------------------------------
/src/example/Form/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import useForm from 'lib';
3 | import './index.css';
4 |
5 | function Form() {
6 | // Define your state schema
7 | const stateSchema = {
8 | first_name: { value: 'Vincent', error: '' },
9 | last_name: { value: '', error: '' },
10 | tags: { value: '', error: '' },
11 | password: { value: '', error: '' },
12 | confirm_password: { value: '', error: '' },
13 | };
14 |
15 | const onSubmitForm = (state) => {
16 | alert(JSON.stringify(state, null, 2));
17 | };
18 |
19 | const {
20 | values,
21 | errors,
22 | dirty,
23 | handleOnChange,
24 | handleOnSubmit,
25 | disable,
26 | } = useForm(
27 | stateSchema,
28 | {
29 | first_name: {
30 | required: true,
31 | validator: {
32 | func: (value) => /^[a-zA-Z]+$/.test(value),
33 | error: 'Invalid first name format.',
34 | },
35 | },
36 | last_name: {
37 | required: true,
38 | validator: {
39 | func: (value) => /^[a-zA-Z]+$/.test(value),
40 | error: 'Invalid last name format.',
41 | },
42 | },
43 | tags: {
44 | validator: {
45 | func: (value) => /^(,?\w{3,})+$/.test(value),
46 | error: 'Invalid tag format.',
47 | },
48 | },
49 | confirm_password: {
50 | required: true,
51 | validator: {
52 | func: (value, values) => value === values.password,
53 | error: 'Confirm Password does not match to Password',
54 | },
55 | },
56 | password: {
57 | required: true,
58 | compare: {
59 | to: 'confirm_password',
60 | error: 'Password does not match to confirm password',
61 | },
62 | validator: {
63 | func: (value) => /^[a-zA-Z]+$/.test(value),
64 | error: 'Password does not meet the requirement',
65 | },
66 | },
67 | },
68 | onSubmitForm
69 | );
70 |
71 | const { first_name, last_name, tags, password, confirm_password } = values;
72 |
73 | return (
74 |
135 | );
136 | }
137 |
138 | export default Form;
139 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | import './index.css';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 |
--------------------------------------------------------------------------------
/src/lib/index.d.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface StateSchemaProp {
4 | value: string | number;
5 | error: string;
6 | }
7 |
8 | interface Validator {
9 | func(param: any): boolean;
10 | error: string;
11 | }
12 |
13 | interface ValidatorSchema {
14 | required?: boolean;
15 | validator?: Validator;
16 | compare?: { to: keyof T; error: string };
17 | }
18 |
19 | type ReturnValue = {
20 | values: { [P in keyof T]: any };
21 | errors: { [P in keyof T]: any };
22 | dirty: { [P in keyof T]: any };
23 | handleOnChange(event: React.ChangeEvent): void;
24 | handleOnSubmit(event: React.FormEvent): void;
25 | disable: boolean;
26 | };
27 |
28 | export type StateSchema = {
29 | [P in keyof T]: StateSchemaProp;
30 | };
31 |
32 | export type StateValidatorSchema = {
33 | [P in keyof T]: ValidatorSchema;
34 | };
35 |
36 | export default function useForm(
37 | stateSchema: StateSchema,
38 | stateValidatorSchema: StateValidatorSchema,
39 | onSubmitForm?: () => void
40 | ): ReturnValue;
41 |
--------------------------------------------------------------------------------
/src/lib/index.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useCallback } from 'react';
2 | import { get_prop_values, is_object, is_required, VALUE, ERROR } from './utils';
3 |
4 | /**
5 | * useForm hooks to handle your validation in your forms
6 | *
7 | * @param {object} stateSchema stateSchema.
8 | * @param {object} stateValidatorSchema stateSchemaValidation to validate your forms in react.
9 | * @param {function} submitFormCallback function to be execute during form submission.
10 | */
11 | function useForm(
12 | stateSchema = {},
13 | stateValidatorSchema = {},
14 | submitFormCallback
15 | ) {
16 | const [state, setStateSchema] = useState(stateSchema);
17 | const [validatorSchema, setValidatorSchema] = useState(stateValidatorSchema);
18 |
19 | const [values, setValues] = useState(get_prop_values(state, VALUE));
20 | const [errors, setErrors] = useState(get_prop_values(state, ERROR));
21 | const [dirty, setDirty] = useState(get_prop_values(state, false));
22 |
23 | const [disable, setDisable] = useState(true);
24 | const [isDirty, setIsDirty] = useState(false);
25 |
26 | // Get a local copy of stateSchema
27 | useEffect(() => {
28 | setStateSchema(stateSchema);
29 |
30 | setInitialErrorState();
31 | }, []); // eslint-disable-line
32 |
33 | // Set a brand new field values and errors
34 | // If stateSchema changes
35 | useEffect(() => {
36 | const values = get_prop_values(state, VALUE);
37 | const errors = Object.keys(values).reduce((accu, curr) => {
38 | accu[curr] = validateField(curr, values[curr]);
39 | return accu;
40 | }, {});
41 |
42 | // Marked form as dirty if state was changed.
43 | setIsDirty(true);
44 |
45 | setValues(values);
46 | setErrors(errors);
47 | }, [state]); // eslint-disable-line
48 |
49 | // Run validation if validatorSchema was already set or has change...
50 | useEffect(() => {
51 | const errors = Object.keys(values).reduce((accu, curr) => {
52 | accu[curr] = validateField(curr, values[curr]);
53 | return accu;
54 | }, {});
55 |
56 | setErrors(errors);
57 | }, [validatorSchema]); // eslint-disable-line
58 |
59 | // For every changed in our state this will be fired
60 | // To be able to disable the button
61 | useEffect(() => {
62 | if (isDirty) {
63 | setDisable(validateErrorState());
64 | }
65 | }, [errors, isDirty]); // eslint-disable-line
66 |
67 | // Set field value to specific field.
68 | const setFieldValue = ({ name, value }) => {
69 | setValues((prevState) => ({ ...prevState, [name]: value }));
70 | setDirty((prevState) => ({ ...prevState, [name]: true }));
71 | };
72 |
73 | // Set to specific field.
74 | const setFieldError = ({ name, error }) => {
75 | setErrors((prevState) => ({ ...prevState, [name]: error }));
76 | };
77 |
78 | // Function used to validate form fields
79 | const validateField = useCallback(
80 | (name, value) => {
81 | const fieldValidator = validatorSchema[name];
82 | // Making sure that stateValidatorSchema name is same in
83 | // stateSchema
84 | if (!fieldValidator) {
85 | return;
86 | }
87 |
88 | let error = '';
89 | error = is_required(value, fieldValidator['required']);
90 | if (error) {
91 | return error;
92 | }
93 |
94 | // Bail out if field is not required and no value set.
95 | // To prevent proceeding to validator function
96 | if (!fieldValidator['required'] && !value) {
97 | return error;
98 | }
99 |
100 | // Run custom validator function
101 | if (!error && is_object(fieldValidator['validator'])) {
102 | // Test the function callback if the value is meet the criteria
103 | if (!fieldValidator['validator']['func'](value, values)) {
104 | error = fieldValidator['validator']['error'];
105 | }
106 | }
107 |
108 | if (!error && is_object(fieldValidator['compare'])) {
109 | const { to, error: errorMessage } = fieldValidator.compare;
110 | if (to && errorMessage && values[to] !== '') {
111 | if (value !== values[to]) {
112 | error = errorMessage;
113 | } else {
114 | setFieldError({ name: to, error: '' });
115 | }
116 | }
117 | }
118 |
119 | return error;
120 | },
121 | [validatorSchema, values]
122 | );
123 |
124 | // Set Initial Error State
125 | // When hooks was first rendered...
126 | const setInitialErrorState = useCallback(() => {
127 | Object.keys(errors).map((name) =>
128 | setFieldError({ name, error: validateField(name, values[name]) })
129 | );
130 | }, [errors, values, validateField]);
131 |
132 | // Used to disable submit button if there's a value in errors
133 | // or the required field in state has no value.
134 | // Wrapped in useCallback to cached the function to avoid intensive memory leaked
135 | // in every re-render in component
136 | const validateErrorState = useCallback(
137 | () => Object.values(errors).some((error) => error),
138 | [errors]
139 | );
140 |
141 | // Use this callback function to safely submit the form
142 | // without any errors in state...
143 | const handleOnSubmit = useCallback(
144 | (event) => {
145 | event.preventDefault();
146 |
147 | // Making sure that there's no error in the state
148 | // before calling the submit callback function
149 | if (!validateErrorState()) {
150 | submitFormCallback(values);
151 | }
152 | },
153 | [validateErrorState, submitFormCallback, values]
154 | );
155 |
156 | // Event handler for handling changes in input.
157 | const handleOnChange = useCallback(
158 | (event) => {
159 | const name = event.target.name;
160 | const value = event.target.value;
161 |
162 | const error = validateField(name, value);
163 |
164 | setFieldValue({ name, value });
165 | setFieldError({ name, error });
166 | },
167 | [validateField]
168 | );
169 |
170 | return {
171 | dirty,
172 | values,
173 | errors,
174 | disable,
175 | setStateSchema,
176 | setValidatorSchema,
177 | setFieldValue,
178 | setFieldError,
179 | handleOnChange,
180 | handleOnSubmit,
181 | validateErrorState,
182 | };
183 | }
184 |
185 | export default useForm;
186 |
--------------------------------------------------------------------------------
/src/lib/utils.js:
--------------------------------------------------------------------------------
1 | export const VALUE = 'value';
2 | export const ERROR = 'error';
3 | export const REQUIRED_FIELD_ERROR = 'This is required field';
4 |
5 | function is_bool(value) {
6 | return typeof value === 'boolean';
7 | }
8 |
9 | /**
10 | * Determines a value if it's an object
11 | *
12 | * @param {object} value
13 | */
14 | export function is_object(value) {
15 | return typeof value === 'object' && value !== null;
16 | }
17 |
18 | /**
19 | *
20 | * @param {string} value
21 | * @param {boolean} isRequired
22 | */
23 | export function is_required(value, isRequired) {
24 | if (!value && isRequired) return REQUIRED_FIELD_ERROR;
25 | return '';
26 | }
27 |
28 | export function get_prop_values(stateSchema, prop) {
29 | return Object.keys(stateSchema).reduce((field, key) => {
30 | field[key] = is_bool(prop) ? prop : stateSchema[key][prop];
31 |
32 | return field;
33 | }, {});
34 | }
35 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./src",
4 | "alwaysStrict": true,
5 | "esModuleInterop": true,
6 | "module": "ESNext",
7 | "target": "ES5",
8 | "outDir": "./dist",
9 | "lib": ["ESNext", "ES6", "DOM"],
10 | "strict": true,
11 | "allowJs": true,
12 | "skipLibCheck": true,
13 | "allowSyntheticDefaultImports": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "moduleResolution": "Node",
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | },
19 | "formatCodeOptions": {
20 | "indentSize": 2,
21 | "tabSize": 2
22 | },
23 | "include": ["src"]
24 | }
25 |
--------------------------------------------------------------------------------