├── .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 | react-logo 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 | 75 |
76 | 77 | 83 | {errors.first_name && dirty.first_name && ( 84 |

{errors.first_name}

85 | )} 86 |
87 | 88 |
89 | 90 | 96 | {errors.last_name && dirty.last_name && ( 97 |

{errors.last_name}

98 | )} 99 |
100 | 101 |
102 | 103 | 104 | {errors.tags && dirty.tags &&

{errors.tags}

} 105 |
106 | 107 |
108 | 109 | 115 | {errors.password && dirty.password && ( 116 |

{errors.password}

117 | )} 118 |
119 | 120 |
121 | 122 | 128 | {errors.confirm_password && dirty.confirm_password && ( 129 |

{errors.confirm_password}

130 | )} 131 |
132 | 133 | 134 | 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 | 2 | 3 | 4 | 5 | 6 | 7 | 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 | --------------------------------------------------------------------------------