├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── README.md ├── __tests__ ├── functional-component.test.js └── validation-component.test.js ├── babel.config.js ├── examples ├── FormTest.js └── FunctionalFormTest.js ├── package-lock.json ├── package.json └── src ├── ValidationComponent.js ├── index.js ├── messages └── defaultMessages.js ├── rules └── defaultRules.js └── use-validation.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | pull_request: ~ 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [14.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | registry-url: 'https://registry.npmjs.org' 26 | 27 | - run: npm ci 28 | 29 | - run: npm test 30 | 31 | - name: Conventional Changelog Action 32 | if: ${{ github.ref == 'refs/heads/master' }} 33 | uses: TriPSs/conventional-changelog-action@v3 34 | with: 35 | github-token: ${{ secrets.BUMP_VERSION_TOKEN }} 36 | 37 | - name : Publish library to npm registry 38 | if: ${{ github.ref == 'refs/heads/master' }} 39 | run: npm publish 40 | env: 41 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # Android/IJ 26 | # 27 | .idea 28 | .gradle 29 | local.properties 30 | 31 | # node.js 32 | # 33 | node_modules/ 34 | npm-debug.log 35 | 36 | # BUCK 37 | buck-out/ 38 | \.buckd/ 39 | android/app/libs 40 | android/keystores/debug.keystore 41 | 42 | /coverage/ 43 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "semi": true, 7 | "bracketSpacing": true, 8 | "trailingComma": "none" 9 | } 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [0.5.0](https://github.com/perscrew/react-native-form-validator/compare/v0.3.2...v0.5.0) (2021-09-08) 2 | 3 | 4 | ### Features 5 | 6 | * **deps:** update dependencies ([51aa614](https://github.com/perscrew/react-native-form-validator/commit/51aa614642f2b4d1e4d619dbe51b5c9176ca288d)) 7 | 8 | 9 | 10 | ## [0.3.2](https://github.com/perscrew/react-native-form-validator/compare/v0.3.1...v0.3.2) (2019-10-25) 11 | 12 | 13 | ### Features 14 | 15 | * **release:** v0.3.2 ([a904b9f](https://github.com/perscrew/react-native-form-validator/commit/a904b9fa8d94600a1218eb21e26693a12a3f5a80)) 16 | 17 | 18 | 19 | ## [0.3.1](https://github.com/perscrew/react-native-form-validator/compare/v0.2.0...v0.3.1) (2019-10-17) 20 | 21 | 22 | ### Features 23 | 24 | * **release:** v0.3.1 ([4cd370d](https://github.com/perscrew/react-native-form-validator/commit/4cd370d8dccc3f5441a6ac9856a0fd55f15bbc86)) 25 | 26 | 27 | 28 | # [0.2.0](https://github.com/perscrew/react-native-form-validator/compare/v0.1.3...v0.2.0) (2017-12-11) 29 | 30 | 31 | 32 | ## [0.1.3](https://github.com/perscrew/react-native-form-validator/compare/v0.1.2...v0.1.3) (2017-03-10) 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ** ⚠️ THIS LIBRARY HAS BEEN REWRITTEN WITH TYPESCRIPT AND HAS MOVED TO [https://github.com/perscrew/react-simple-form-validator](https://github.com/perscrew/react-simple-form-validator) ⚠️ ** 2 | 3 | # React native form validator 4 | [![Node.js CI](https://github.com/perscrew/react-native-form-validator/actions/workflows/ci.yml/badge.svg)](https://github.com/perscrew/react-native-form-validator/actions/workflows/ci.yml) 5 | [![Npm package version](https://badgen.net/npm/v/react-native-form-validator)](https://www.npmjs.com/package/react-native-form-validator) 6 | [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://GitHub.com/Naereen/StrapDown.js/graphs/commit-activity) 7 | [![made-with-javascript](https://img.shields.io/badge/Made%20with-JavaScript-1f425f.svg)](https://www.javascript.com) 8 | 9 | React native form validator is a simple library to validate your form fields with React Native. 10 | The library is easy to use. You just have to extend the "ValidationComponent" class on your desired React native form component. 11 | 12 | ## 1. Installation 13 | * Run npm install 'react-native-form-validator' to fetch the library : 14 | ```sh 15 | npm install 'react-native-form-validator' --save 16 | ``` 17 | 18 | ## 2. Use it in your app 19 | ### For class component: 20 | 21 | 22 | Extend "ValidationComponent" class on a form component : 23 | ```js 24 | import React from 'react'; 25 | import ValidationComponent from 'react-native-form-validator'; 26 | 27 | export default class MyForm extends ValidationComponent { 28 | ... 29 | } 30 | ``` 31 | The ValidationComponent extends the React.Component class. 32 | 33 | To ensure form validation you have to call the "this.validate" method in a custom function. 34 | ```js 35 | constructor(props) { 36 | super(props); 37 | this.state = {name : "My name", email: "titi@gmail.com", number:"56", date: "2017-03-01"}; 38 | } 39 | 40 | _onSubmit() { 41 | // Call ValidationComponent validate method 42 | this.validate({ 43 | name: {minlength:3, maxlength:7, required: true}, 44 | email: {email: true}, 45 | number: {numbers: true}, 46 | date: {date: 'YYYY-MM-DD'} 47 | }); 48 | } 49 | ``` 50 | The method arguments should be a representation of the React component state. The first graph level matches with the React state variables. 51 | The second level matches with the existing validation rules. 52 | 53 | You will find bellow the default rules available in the library [defaultRules.js](./defaultRules.js) : 54 | 55 | |Rule|Benefits| 56 | |-------|--------| 57 | |numbers|Check if a state variable is a number.| 58 | |email|Check if a state variable is an email.| 59 | |required|Check if a state variable is not empty.| 60 | |date|Check if a state variable respects the date pattern. Ex: date: 'YYYY-MM-DD'| 61 | |minlength|Check if a state variable is greater than minlength.| 62 | |maxlength|Check if a state variable is lower than maxlength.| 63 | |equalPassword|Check if a state variable is equal to another value (useful for password confirm).| 64 | |hasNumber|Check if a state variable contains a number.| 65 | |hasUpperCase|Check if a state variable contains a upper case letter.| 66 | |hasLowerCase|Check if a state variable contains a lower case letter.| 67 | |hasSpecialCharacter|Check if a state variable contains a special character.| 68 | 69 | You can also override this file via the component React props : 70 | ```js 71 | const rules = {any: /^(.*)$/}; 72 | 73 | 74 | ``` 75 | 76 | 77 | Once you have extended the class a set of useful methods become avaiblable : 78 | 79 | |Method|Output|Benefits| 80 | |-------|--------|--------| 81 | |this.validate(state_rules)|Boolean|This method ensures form validation within the object passed in argument.The object should be a representation of the React component state. The first graph level matches with the React state variables.The second level matches with the existing validation rules.| 82 | |this.isFormValid()|Boolean|This method indicates if the form is valid and if there are no errors.| 83 | |this.isFieldInError(fieldName)|Boolean|This method indicates if a specific field has an error. The field name will match with your React state| 84 | |this.getErrorMessages(separator)|String|This method returns the different error messages bound to your React state. The argument is optional, by default the separator is a \n. Under the hood a join method is used.| 85 | |this.getErrorsInField(fieldName)|Array|This method returns the error messages bound to the specified field. The field name will match with your React state. It returns an empty array if no error was bound to the field.| 86 | 87 | The library also contains a [defaultMessages.js](./defaultMessages.js) file which includes the errors label for a language locale. 88 | You can override this file via the component React props : 89 | ```js 90 | const messages = { 91 | en: {numbers: "error on numbers !"}, 92 | fr: {numbers: "erreur sur les nombres !"} 93 | }; 94 | 95 | 96 | ``` 97 | You can add custom labels to the state variables, which will be useful if you want to change it's label in the error messages or translate it 98 | to the local language : 99 | ```js 100 | const labels = { 101 | name: 'Name', 102 | email: 'E-mail', 103 | number: 'Phone number' 104 | }; 105 | 106 | 107 | ``` 108 | 109 | You can also specify the default custom local language in the props : 110 | 111 | ```js 112 | 113 | ``` 114 | ### Dynamic validation onChangeText 115 | You can also use dynamic validation by calling validate function on onChangeText event : 116 | 117 | ```js 118 | { 120 | this.setState({ lastName }, () => { 121 | this.validate({ 122 | lastName: { required: true }, 123 | }) 124 | }) 125 | }} value={this.state.lastName} style={[styles.input]} /> 126 | {this.isFieldInError('lastName') && this.getErrorsInField('lastName').map(errorMessage => {errorMessage})} 127 | ``` 128 | 129 | ### For functional component: 130 | 131 | You will use useValidation hook inside your component like this : 132 | 133 | ```js 134 | import { useValidation } from 'react-native-form-validator'; 135 | import customValidationMessages from './customValidationMessages'; 136 | 137 | const MyFunction = () => { 138 | const [email, setEmail] = useState(''); 139 | const [name, setName] = useState(''); 140 | 141 | const { validate, getErrorsInField } = useValidation({ 142 | state: { email, name }, 143 | messages: customValidationMessages, 144 | }); 145 | 146 | const _validateForm = () => { 147 | validate({ 148 | email: { email: true }, 149 | name: { required: true } 150 | }) 151 | } 152 | } 153 | ``` 154 | You need to pass the state manually to the useValidation hook in state object like above. 155 | You can also pass custom messages, labels, rules, deviceLocale and it returns object with all the methods that available in the class component. 156 | 157 | 158 | ## 3. Complete example 159 | 160 | ### Class component: 161 | 162 | You can find a complete example in the [formTest.js](./test/formTest.js) file : 163 | 164 | ```js 165 | 'use strict'; 166 | 167 | import React, {Component} from 'react'; 168 | import {View, Text, TextInput, TouchableHighlight} from 'react-native'; 169 | import ValidationComponent from '../index'; 170 | 171 | export default class FormTest extends ValidationComponent { 172 | 173 | constructor(props) { 174 | super(props); 175 | this.state = {name : "My name", email: "tibtib@gmail.com", number:"56", date: "2017-03-01", newPassword : "", confirmPassword : ""}; 176 | } 177 | 178 | _onPressButton() { 179 | // Call ValidationComponent validate method 180 | this.validate({ 181 | name: {minlength:3, maxlength:7, required: true}, 182 | email: {email: true}, 183 | number: {numbers: true}, 184 | date: {date: 'YYYY-MM-DD'}, 185 | confirmPassword : {equalPassword : this.state.newPassword} 186 | }); 187 | } 188 | 189 | render() { 190 | return ( 191 | 192 | this.setState({name})} value={this.state.name} /> 193 | this.setState({email})} value={this.state.email} /> 194 | this.setState({number})} value={this.state.number} /> 195 | this.setState({date})} value={this.state.date} /> 196 | {this.isFieldInError('date') && this.getErrorsInField('date').map(errorMessage => {errorMessage}) } 197 | 198 | this.setState({newPassword})} value={this.state.newPassword} secureTextEntry={true}/> 199 | this.setState({confirmPassword})} value={this.state.confirmPassword} secureTextEntry={true} /> 200 | {this.isFieldInError('confirmPassword') && this.getErrorsInField('confirmPassword').map(errorMessage => {errorMessage}) } 201 | 202 | 203 | Submit 204 | 205 | 206 | 207 | {this.getErrorMessages()} 208 | 209 | 210 | ); 211 | } 212 | 213 | } 214 | ``` 215 | 216 | ### Function Component: 217 | 218 | ```js 219 | 'use strict'; 220 | 221 | import React, { useState } from 'react'; 222 | import { View, Text, TextInput, TouchableHighlight } from 'react-native'; 223 | import { useValidation } from 'react-native-form-validator'; 224 | 225 | const FormTest = () => { 226 | const [name, setName] = useState('My name'); 227 | const [email, setEmail] = useState('tibtib@gmail.com'); 228 | const [number, setNumber] = useState('56'); 229 | const [date, setDate] = useState('2017-03-01'); 230 | const [newPassword, setNewPassword] = useState(''); 231 | const [confirmPassword, setConfirmPassword] = useState(''); 232 | 233 | const { validate, isFieldInError, getErrorsInField, getErrorMessages } = 234 | useValidation({ 235 | state: { name, email, number, date, newPassword, confirmPassword }, 236 | }); 237 | 238 | const _onPressButton = () => { 239 | validate({ 240 | name: { minlength: 3, maxlength: 7, required: true }, 241 | email: { email: true }, 242 | number: { numbers: true }, 243 | date: { date: 'YYYY-MM-DD' }, 244 | confirmPassword: { equalPassword: newPassword }, 245 | }); 246 | }; 247 | 248 | return ( 249 | 250 | 251 | 252 | 253 | 254 | {isFieldInError('date') && 255 | getErrorsInField('date').map(errorMessage => ( 256 | {errorMessage} 257 | ))} 258 | 259 | 264 | 269 | {isFieldInError('confirmPassword') && 270 | getErrorsInField('confirmPassword').map(errorMessage => ( 271 | {errorMessage} 272 | ))} 273 | 274 | 275 | Submit 276 | 277 | 278 | {getErrorMessages()} 279 | 280 | ); 281 | }; 282 | 283 | export default FormTest; 284 | ``` 285 | -------------------------------------------------------------------------------- /__tests__/functional-component.test.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { shallow, configure } from 'enzyme'; 3 | import FunctionalFormTest from '../examples/FunctionalFormTest'; 4 | import { TextInput, Text, TouchableHighlight } from 'react-native'; 5 | 6 | import Adapter from 'enzyme-adapter-react-16'; 7 | configure({ adapter: new Adapter() }); 8 | 9 | describe('FunctionalComponent', () => { 10 | test('should display 7 text inputs', () => { 11 | const wrapper = shallow(); 12 | const textInputs = wrapper.find(TextInput); 13 | expect(textInputs.length).toBe(7); 14 | }); 15 | 16 | test('should display name error if name is lower than 3', () => { 17 | const wrapper = shallow(); 18 | const textInput = wrapper.find(TextInput).at(0); // name input 19 | textInput.simulate('changeText', 'em'); 20 | 21 | wrapper.find(TouchableHighlight).simulate('press'); 22 | wrapper.update(); 23 | 24 | const errorMsg = wrapper.find(Text).at(0); 25 | expect(errorMsg.contains(The field "name" length must be greater than 2.)).toBe(true); 26 | }); 27 | 28 | test('should display name error if name is greater than 8', () => { 29 | const wrapper = shallow(); 30 | const textInput = wrapper.find(TextInput).at(0); // name input 31 | textInput.simulate('changeText', 'mybeautifultest'); 32 | 33 | wrapper.find(TouchableHighlight).simulate('press'); 34 | wrapper.update(); 35 | 36 | const errorMsg = wrapper.find(Text).at(0); 37 | expect(errorMsg.contains(The field "name" length must be lower than 7.)).toBe(true); 38 | }); 39 | 40 | test('should display pseudo error if pseudo is not inquired', () => { 41 | const wrapper = shallow(); 42 | 43 | wrapper.find(TouchableHighlight).simulate('press'); 44 | wrapper.update(); 45 | 46 | const errorMsg = wrapper.find(Text).at(1); 47 | expect(errorMsg.contains(The field "name" is mandatory.)).toBe(true); 48 | }); 49 | 50 | test('should display email error if email is not a valid email', () => { 51 | const wrapper = shallow(); 52 | const textInput = wrapper.find(TextInput).at(2); // email input 53 | textInput.simulate('changeText', 'wrongemai'); 54 | 55 | wrapper.find(TouchableHighlight).simulate('press'); 56 | wrapper.update(); 57 | 58 | const errorMsg = wrapper.find(Text).at(3); 59 | expect(errorMsg.contains(The field "email" must be a valid email address.)).toBe(true); 60 | }); 61 | 62 | test('should display email error if email is not a valid email', () => { 63 | const wrapper = shallow(); 64 | const textInput = wrapper.find(TextInput).at(2); // email input 65 | textInput.simulate('changeText', 'wrongemai'); 66 | 67 | wrapper.find(TouchableHighlight).simulate('press'); 68 | wrapper.update(); 69 | 70 | const errorMsg = wrapper.find(Text).at(3); 71 | expect(errorMsg.contains(The field "email" must be a valid email address.)).toBe(true); 72 | }); 73 | 74 | test('should display number error if number is not a valid number', () => { 75 | const wrapper = shallow(); 76 | const textInput = wrapper.find(TextInput).at(3); // number input 77 | textInput.simulate('changeText', 'abc'); 78 | 79 | wrapper.find(TouchableHighlight).simulate('press'); 80 | wrapper.update(); 81 | 82 | const errorMsg = wrapper.find(Text).at(3); 83 | expect(errorMsg.contains(The field "number" must be a valid number.)).toBe(true); 84 | }); 85 | 86 | test('should display date error if date is not a valid date pattern', () => { 87 | const wrapper = shallow(); 88 | const textInput = wrapper.find(TextInput).at(4); // date input 89 | textInput.simulate('changeText', 'fdsfdf'); 90 | 91 | wrapper.find(TouchableHighlight).simulate('press'); 92 | wrapper.update(); 93 | 94 | const errorMsg = wrapper.find(Text).at(3); 95 | expect(errorMsg.contains(The field "date" must be a valid date (YYYY-MM-DD).)).toBe(true); 96 | }); 97 | 98 | test('should display confirm password error if passwords are not identical', () => { 99 | const wrapper = shallow(); 100 | const password = wrapper.find(TextInput).at(5); // password input 101 | const confirmPassword = wrapper.find(TextInput).at(6); // confirm password input 102 | password.simulate('changeText', 'test'); 103 | confirmPassword.simulate('changeText', 'test2'); 104 | 105 | wrapper.find(TouchableHighlight).simulate('press'); 106 | wrapper.update(); 107 | 108 | const errorMsg = wrapper.find(Text); 109 | expect(errorMsg.contains(Passwords are different.)).toBe(true); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /__tests__/validation-component.test.js: -------------------------------------------------------------------------------- 1 | 2 | import * as React from 'react'; 3 | import { shallow, configure } from 'enzyme'; 4 | import renderer from 'react-test-renderer'; 5 | import FormTest from '../examples/FormTest'; 6 | import { TextInput } from 'react-native'; 7 | 8 | import Adapter from 'enzyme-adapter-react-16'; 9 | configure({ adapter: new Adapter() }); 10 | 11 | describe('ValidationComponent:', () => { 12 | 13 | test('initialize', () => { 14 | const component = renderer.create(); 15 | expect(component.getInstance().errors).toEqual([]); 16 | }); 17 | 18 | it('default fields validation should be ok', () => { 19 | const component = renderer.create(); 20 | const formTest = component.getInstance(); 21 | 22 | formTest._onPressButton(); 23 | 24 | expect(formTest.errors).toEqual([]); 25 | expect(formTest.isFormValid()).toBe(true); 26 | expect(formTest.getErrorsInField('name')).toEqual([]); 27 | expect(formTest.getErrorsInField('number')).toEqual([]); 28 | expect(formTest.getErrorMessages()).toBe(""); 29 | }); 30 | 31 | it('method validate should return false', () => { 32 | const wrapper = shallow(); 33 | const textInput = wrapper.find(TextInput).first(); // name input 34 | //Seize a value lower than 2 chars 35 | textInput.simulate('changeText', "na"); // minlength = 3 36 | 37 | const formTest = wrapper.instance(); 38 | expect(formTest.validate({ name: { minlength: 3 } })).toBe(false); 39 | }); 40 | 41 | it('empty field with required rule should not be ok', () => { 42 | const wrapper = shallow(); 43 | const textInput = wrapper.find(TextInput).first(); // name input 44 | //Seize a value lower than 2 chars 45 | textInput.simulate('changeText', ""); // minlength = 3 46 | 47 | const formTest = wrapper.instance(); 48 | expect(formTest.validate({ name: { required: true } })).toBe(false); 49 | }); 50 | 51 | it('fields validation name should not be ok', () => { 52 | const wrapper = shallow(); 53 | const textInput = wrapper.find(TextInput).first(); // name input 54 | 55 | //Seize a value lower than 2 chars 56 | textInput.simulate('changeText', "na"); // minlength = 3 57 | 58 | const formTest = wrapper.instance(); 59 | formTest._onPressButton(); 60 | 61 | expect(formTest.isFormValid()).toBe(false); 62 | expect(formTest.getErrorMessages()).toBe('The field "name" length must be greater than 2.'); 63 | expect(formTest.isFieldInError('name')).toBe(true); 64 | 65 | // Seize an empty value 66 | textInput.simulate('changeText', ""); // minlength = 3 67 | formTest._onPressButton(); 68 | expect(formTest.isFormValid()).toBe(false); 69 | expect(formTest.getErrorMessages()).toBe('The field "name" length must be greater than 2.\nThe field "name" is mandatory.'); 70 | expect(formTest.getErrorsInField('name')).toEqual([ 71 | 'The field "name" length must be greater than 2.', 72 | 'The field "name" is mandatory.' 73 | ]) 74 | expect(formTest.isFieldInError('name')).toBe(true); 75 | 76 | // Seize a value greater than maxlength (7) 77 | textInput.simulate('changeText', "12345678"); // maxlength = 7 78 | formTest._onPressButton(); 79 | expect(formTest.isFormValid()).toBe(false); 80 | expect(formTest.getErrorMessages()).toBe('The field "name" length must be lower than 7.'); 81 | expect(formTest.isFieldInError('name')).toBe(true); 82 | }); 83 | 84 | it('fields validation email should not be ok', () => { 85 | const wrapper = shallow(); 86 | const textInput = wrapper.find(TextInput).at(1); // email input 87 | textInput.simulate('changeText', "em"); 88 | 89 | const formTest = wrapper.instance(); 90 | formTest._onPressButton(); 91 | 92 | expect(formTest.isFormValid()).toBe(false); 93 | expect(formTest.getErrorMessages()).toBe('The field "email" must be a valid email address.'); 94 | expect(formTest.isFieldInError('email')).toBe(true); 95 | }); 96 | 97 | 98 | 99 | it('fields validation date should not be ok', () => { 100 | const wrapper = shallow(); 101 | const textInput = wrapper.find(TextInput).at(3); // date input 102 | textInput.simulate('changeText', "fdsfds"); 103 | 104 | const formTest = wrapper.instance(); 105 | formTest._onPressButton(); 106 | 107 | expect(formTest.isFormValid()).toBe(false); 108 | expect(formTest.getErrorMessages()).toBe('The field "date" must be a valid date (YYYY-MM-DD).'); 109 | expect(formTest.isFieldInError('date')).toBe(true); 110 | }); 111 | 112 | 113 | it('fields validation number should not be ok', () => { 114 | const wrapper = shallow(); 115 | const textInput = wrapper.find(TextInput).at(2); // number input 116 | textInput.simulate('changeText', "not_number"); 117 | 118 | const formTest = wrapper.instance(); 119 | formTest._onPressButton(); 120 | 121 | expect(formTest.isFormValid()).toBe(false); 122 | expect(formTest.getErrorMessages()).toBe('The field "number" must be a valid number.'); 123 | expect(formTest.isFieldInError('number')).toBe(true); 124 | }); 125 | 126 | it('fields validation number should not be very OK', () => { 127 | const wrapper = shallow(); 128 | const textInput = wrapper.find(TextInput).at(2); // number input 129 | textInput.simulate('changeText', "not_number"); 130 | 131 | const formTest = wrapper.instance(); 132 | formTest._onPressButton(); 133 | 134 | expect(formTest.isFormValid()).toBe(false); 135 | expect(formTest.getErrorMessages()).toBe('The field "number" must be a valid number.'); 136 | expect(formTest.getErrorsInField('number')).toEqual(['The field "number" must be a valid number.']) 137 | expect(formTest.isFieldInError('number')).toBe(true); 138 | }); 139 | 140 | 141 | it('deviceLocale props should be fr', () => { 142 | const wrapper = shallow(); 143 | const formTest = wrapper.instance(); 144 | expect(formTest.deviceLocale).toBe("fr"); 145 | }); 146 | 147 | it('error messages should be in fr locale', () => { 148 | const wrapper = shallow(); 149 | const textInput = wrapper.find(TextInput).at(1); // email input 150 | textInput.simulate('changeText', "em"); 151 | 152 | const formTest = wrapper.instance(); 153 | formTest._onPressButton(); 154 | 155 | expect(formTest.isFormValid()).toBe(false); 156 | expect(formTest.getErrorMessages()).toBe('Le champ "email" doit être une adresse email valide.'); 157 | expect(formTest.isFieldInError('email')).toBe(true); 158 | expect(formTest.deviceLocale).toBe("fr"); 159 | }); 160 | 161 | it('rules props should be updated', () => { 162 | const rules = { any: /^(.*)$/ }; 163 | const wrapper = shallow(); 164 | const formTest = wrapper.instance(); 165 | expect(formTest.rules).toEqual(rules); 166 | }); 167 | 168 | it('messages props should be updated', () => { 169 | const messages = { 170 | en: { numbers: "error on numbers !" }, 171 | fr: { numbers: "erreur sur les nombres !" } 172 | }; 173 | const wrapper = shallow(); 174 | const formTest = wrapper.instance(); 175 | expect(formTest.messages).toEqual(messages); 176 | }); 177 | 178 | it("fields password doesn't have number - not ok", () => { 179 | const wrapper = shallow(); 180 | const textInput = wrapper.find(TextInput).at(4); // number input 181 | textInput.simulate('changeText', "Azerty*"); 182 | const textInput2 = wrapper.find(TextInput).at(5); // number input 183 | textInput2.simulate('changeText', "Azerty*"); 184 | 185 | const formTest = wrapper.instance(); 186 | formTest._onPressButton(); 187 | 188 | expect(formTest.isFormValid()).toBe(false); 189 | expect(formTest.getErrorMessages()).toBe('The field "password" must contain a number.'); 190 | expect(formTest.isFieldInError('password')).toBe(true); 191 | }); 192 | 193 | it("field password doesn't have special character - not ok", () => { 194 | const wrapper = shallow(); 195 | const textInput = wrapper.find(TextInput).at(4); // number input 196 | textInput.simulate('changeText', "Azerty1"); 197 | const textInput2 = wrapper.find(TextInput).at(5); // number input 198 | textInput2.simulate('changeText', "Azerty1"); 199 | 200 | const formTest = wrapper.instance(); 201 | formTest._onPressButton(); 202 | 203 | expect(formTest.isFormValid()).toBe(false); 204 | expect(formTest.getErrorMessages()).toBe('The field "password" must contain a special character.'); 205 | expect(formTest.isFieldInError('password')).toBe(true); 206 | }); 207 | 208 | it("field password doesn't have lower case - not ok", () => { 209 | const wrapper = shallow(); 210 | const textInput = wrapper.find(TextInput).at(4); // number input 211 | textInput.simulate('changeText', "A1*"); 212 | const textInput2 = wrapper.find(TextInput).at(5); // number input 213 | textInput2.simulate('changeText', "A1*"); 214 | 215 | const formTest = wrapper.instance(); 216 | formTest._onPressButton(); 217 | 218 | expect(formTest.isFormValid()).toBe(false); 219 | expect(formTest.getErrorMessages()).toBe('The field "password" must contain a lower case.'); 220 | expect(formTest.isFieldInError('password')).toBe(true); 221 | }); 222 | 223 | it("field password doesn't have upper case - not ok", () => { 224 | const wrapper = shallow(); 225 | const textInput = wrapper.find(TextInput).at(4); // number input 226 | textInput.simulate('changeText', "a1*"); 227 | const textInput2 = wrapper.find(TextInput).at(5); // number input 228 | textInput2.simulate('changeText', "a1*"); 229 | 230 | const formTest = wrapper.instance(); 231 | formTest._onPressButton(); 232 | 233 | expect(formTest.isFormValid()).toBe(false); 234 | expect(formTest.getErrorMessages()).toBe('The field "password" must contain a upper case.'); 235 | expect(formTest.isFieldInError('password')).toBe(true); 236 | }); 237 | 238 | it("field password is not equal to confirm password - not ok", () => { 239 | const wrapper = shallow(); 240 | const textInput = wrapper.find(TextInput).at(4); // number input 241 | textInput.simulate('changeText', "Aa1*"); 242 | const textInput2 = wrapper.find(TextInput).at(5); // number input 243 | textInput2.simulate('changeText', "a1*"); 244 | 245 | const formTest = wrapper.instance(); 246 | formTest._onPressButton(); 247 | 248 | expect(formTest.isFormValid()).toBe(false); 249 | expect(formTest.getErrorMessages()).toBe('Passwords are different.'); 250 | expect(formTest.isFieldInError('confirmPassword')).toBe(true); 251 | }); 252 | 253 | it("password fields are ok", () => { 254 | const wrapper = shallow(); 255 | const textInput = wrapper.find(TextInput).at(4); // number input 256 | textInput.simulate('changeText', "Aa1*"); 257 | const textInput2 = wrapper.find(TextInput).at(5); // number input 258 | textInput2.simulate('changeText', "Aa1*"); 259 | 260 | const formTest = wrapper.instance(); 261 | formTest._onPressButton(); 262 | 263 | expect(formTest.isFormValid()).toBe(true); 264 | expect(formTest.getErrorMessages()).toBe(""); 265 | expect(formTest.isFieldInError('password')).toBe(false); 266 | expect(formTest.isFieldInError('confirmPassword')).toBe(false); 267 | }); 268 | 269 | }); 270 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; -------------------------------------------------------------------------------- /examples/FormTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, TextInput, TouchableHighlight, View } from 'react-native'; 3 | import ValidationComponent from '../src/index'; 4 | 5 | export default class FormTest extends ValidationComponent { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = { name: "My name", email: "tibtib@gmail.com", number: "56", date: "2017-03-01", password: "", confirmPassword: "", error: false }; 10 | } 11 | 12 | _onPressButton() { 13 | // Call ValidationComponent validate method 14 | this.validate({ 15 | name: { minlength: 3, maxlength: 7, required: true }, 16 | email: { email: true }, 17 | number: { numbers: true }, 18 | date: { date: 'YYYY-MM-DD' }, 19 | password: { hasNumber: true, hasLowerCase: true, hasUpperCase: true, hasSpecialCharacter: true }, 20 | confirmPassword: { equalPassword: this.state.password }, 21 | }); 22 | } 23 | 24 | render() { 25 | return ( 26 | 27 | this.setState({ name })} value={this.state.name} /> 28 | this.setState({ email })} value={this.state.email} /> 29 | this.setState({ number })} value={this.state.number} /> 30 | this.setState({ date })} value={this.state.date} /> 31 | this.setState({ password })} value={this.state.password} /> 32 | this.setState({ confirmPassword })} value={this.state.confirmPassword} /> 33 | 34 | 35 | Submit 36 | 37 | 38 | 39 | {this.getErrorMessages()} 40 | 41 | 42 | ); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /examples/FunctionalFormTest.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { View, Text, TextInput, TouchableHighlight } from 'react-native'; 3 | import { useValidation } from '../src/index'; 4 | 5 | const FunctionalFormTest = () => { 6 | const [name, setName] = useState(''); 7 | const [pseudo, setPseudo] = useState(''); 8 | const [email, setEmail] = useState(''); 9 | const [number, setNumber] = useState(''); 10 | const [date, setDate] = useState(''); 11 | const [newPassword, setNewPassword] = useState(''); 12 | const [confirmPassword, setConfirmPassword] = useState(''); 13 | 14 | const { validate, isFieldInError, getErrorsInField, getErrorMessages } = useValidation({ 15 | state: { name, email, pseudo, number, date, newPassword, confirmPassword } 16 | }); 17 | 18 | const _onPressButton = () => { 19 | validate({ 20 | name: { minlength: 3, maxlength: 7, required: true }, 21 | pseudo: { required: true }, 22 | email: { email: true }, 23 | number: { numbers: true }, 24 | date: { date: 'YYYY-MM-DD' }, 25 | confirmPassword: { equalPassword: newPassword } 26 | }); 27 | }; 28 | 29 | return ( 30 | 31 | 32 | {isFieldInError('name') && 33 | getErrorsInField('name').map((errorMessage) => {errorMessage})} 34 | 35 | 36 | {isFieldInError('pseudo') && 37 | getErrorsInField('pseudo').map((errorMessage) => {errorMessage})} 38 | 39 | 40 | {isFieldInError('email') && 41 | getErrorsInField('email').map((errorMessage) => {errorMessage})} 42 | 43 | 44 | {isFieldInError('number') && getErrorsInField('number').map((errorMessage) => {errorMessage})} 45 | 46 | 47 | {isFieldInError('date') && getErrorsInField('date').map((errorMessage) => {errorMessage})} 48 | 49 | 50 | 51 | {isFieldInError('confirmPassword') && 52 | getErrorsInField('confirmPassword').map((errorMessage) => {errorMessage})} 53 | 54 | 55 | Submit 56 | 57 | 58 | {getErrorMessages()} 59 | 60 | ); 61 | }; 62 | 63 | export default FunctionalFormTest; 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-form-validator", 3 | "version": "0.5.0", 4 | "description": "React native library to validate form fields", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "start": "node node_modules/react-native/local-cli/cli.js start", 8 | "test": "jest", 9 | "test:watch": "jest --watch", 10 | "commit": "git-cz" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/perscrew/react-native-form-validator.git" 15 | }, 16 | "keywords": [ 17 | "react-native", 18 | "react", 19 | "javascript" 20 | ], 21 | "author": "Thibaud Dervily", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/perscrew/react-native-form-validator/issues" 25 | }, 26 | "homepage": "https://github.com/perscrew/react-native-form-validator#readme", 27 | "dependencies": { 28 | "moment": "^2.11.2", 29 | "prop-types": "15.6.0" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.15.0", 33 | "@babel/plugin-syntax-optional-chaining": "^7.8.3", 34 | "@babel/preset-env": "^7.15.0", 35 | "@babel/preset-react": "^7.14.5", 36 | "@commitlint/cli": "^13.1.0", 37 | "@commitlint/config-conventional": "^13.1.0", 38 | "@types/jest": "^27.0.1", 39 | "babel-jest": "^27.0.6", 40 | "commitizen": "^4.2.4", 41 | "cz-conventional-changelog": "^1.1.6", 42 | "enzyme": "^3.11.0", 43 | "enzyme-adapter-react-16": "^1.15.6", 44 | "jest": "^27.0.6", 45 | "jsdom": "^9.4.1", 46 | "metro-react-native-babel-preset": "^0.66.2", 47 | "react": "^16.13.1", 48 | "react-dom": "^16.13.1", 49 | "react-native": "^0.65.1", 50 | "react-test-renderer": "^17.0.2" 51 | }, 52 | "jest": { 53 | "preset": "react-native" 54 | }, 55 | "commitlint": { 56 | "extends": [ 57 | "@commitlint/config-conventional" 58 | ] 59 | }, 60 | "config": { 61 | "commitizen": { 62 | "path": "./node_modules/cz-conventional-changelog" 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/ValidationComponent.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { Component } from 'react'; 3 | import defaultMessages from './messages/defaultMessages'; 4 | import defaultRules from './rules/defaultRules'; 5 | 6 | export default class ValidationComponent extends Component { 7 | constructor(props) { 8 | super(props); 9 | // array to store error on each fields 10 | // ex: 11 | // [{ fieldName: "name", messages: ["The field name is required."] }] 12 | this.errors = []; 13 | // Retrieve props 14 | this.deviceLocale = props.deviceLocale || 'en'; // ex: en, fr 15 | this.rules = props.rules || defaultRules; // rules for Validation 16 | this.messages = props.messages || defaultMessages; 17 | this.labels = props.labels || {}; 18 | this.state = { error: false }; 19 | } 20 | 21 | /* 22 | * Method validate to verify if each children respect the validator rules 23 | * Fields example (Array) : 24 | * fields = { 25 | * input1: { 26 | * required:true, 27 | * numbers:true, 28 | * maxLength:5 29 | * } 30 | *} 31 | */ 32 | validate(fields) { 33 | // Reset errors 34 | this._resetErrors(); 35 | // Iterate over inner state 36 | for (const key of Object.keys(this.state)) { 37 | // Check if child name is equals to fields array set up in parameters 38 | const rules = fields[key]; 39 | if (rules) { 40 | // Check rule for current field 41 | this._checkRules(key, rules, this.state[key]); 42 | } 43 | } 44 | return this.isFormValid(); 45 | } 46 | 47 | // Method to check rules on a spefific field 48 | _checkRules(fieldName, rules, value) { 49 | if (!value && !rules.required) { 50 | return; // if value is empty AND its not required by the rules, no need to check any other rules 51 | } 52 | for (const key of Object.keys(rules)) { 53 | const isRuleFn = typeof this.rules[key] == 'function'; 54 | const isRegExp = this.rules[key] instanceof RegExp; 55 | if ((isRuleFn && !this.rules[key](rules[key], value)) || (isRegExp && !this.rules[key].test(value))) { 56 | this._addError(fieldName, key, rules[key], isRuleFn); 57 | } 58 | } 59 | } 60 | 61 | // Add error 62 | // ex: 63 | // [{ fieldName: "name", messages: ["The field name is required."] }] 64 | _addError(fieldName, rule, value, isFn) { 65 | const name = this.labels[fieldName]; 66 | value = rule == 'minlength' ? value - 1 : value; 67 | const errMsg = this.messages[this.deviceLocale][rule].replace('{0}', name || fieldName).replace('{1}', value); 68 | let [error] = this.errors.filter((err) => err.fieldName === fieldName); 69 | // error already exists 70 | if (error) { 71 | // Update existing element 72 | const index = this.errors.indexOf(error); 73 | error.messages.push(errMsg); 74 | error.failedRules.push(rule); 75 | this.errors[index] = error; 76 | } else { 77 | // Add new item 78 | this.errors.push({ 79 | fieldName, 80 | failedRules: [rule], 81 | messages: [errMsg] 82 | }); 83 | } 84 | // this.setState({ error: true }); 85 | } 86 | 87 | // Reset error fields 88 | _resetErrors() { 89 | this.errors = []; 90 | this.setState({ error: false }); 91 | } 92 | 93 | // Method to check if the field is in error 94 | isFieldInError(fieldName) { 95 | return this.errors.filter((err) => err.fieldName === fieldName).length > 0; 96 | } 97 | 98 | isFormValid() { 99 | return this.errors.length == 0; 100 | } 101 | 102 | // Return an object where the keys are the field names and the value is an array with the rules that failed validation 103 | getFailedRules() { 104 | let failedRulesPerField = {}; 105 | for (let index = 0; index < this.errors.length; index++) { 106 | let error = this.errors[index]; 107 | failedRulesPerField[error.fieldName] = error.failedRules; 108 | } 109 | return failedRulesPerField; 110 | } 111 | 112 | // Return the rules that failed validation for the given field 113 | getFailedRulesInField(fieldName) { 114 | const foundError = this.errors.find((err) => err.fieldName === fieldName); 115 | if (!foundError) { 116 | return []; 117 | } 118 | return foundError.failedRules; 119 | } 120 | 121 | // Concatenate each error messages 122 | getErrorMessages(separator = '\n') { 123 | return this.errors.map((err) => err.messages.join(separator)).join(separator); 124 | } 125 | 126 | // Method to return errors on a specific field 127 | getErrorsInField(fieldName) { 128 | const foundError = this.errors.find((err) => err.fieldName === fieldName); 129 | if (!foundError) { 130 | return []; 131 | } 132 | return foundError.messages; 133 | } 134 | } 135 | 136 | // PropTypes for component 137 | ValidationComponent.propTypes = { 138 | deviceLocale: PropTypes.string, // Used for language locale 139 | rules: PropTypes.object, // rules for validations 140 | messages: PropTypes.object, // messages for validation errors 141 | labels: PropTypes.object // labels for validation messages 142 | }; 143 | // DefaultProps for component 144 | ValidationComponent.defaultProps = { 145 | deviceLocale: 'en', 146 | rules: defaultRules, 147 | messages: defaultMessages, 148 | labels: {} 149 | }; 150 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import useValidation from "./use-validation"; 2 | import ValidationComponent from "./ValidationComponent"; 3 | 4 | export { useValidation }; 5 | export default ValidationComponent; 6 | -------------------------------------------------------------------------------- /src/messages/defaultMessages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const defaultMessages = { 4 | // English language - Used by default 5 | en: { 6 | numbers: 'The field "{0}" must be a valid number.', 7 | email: 'The field "{0}" must be a valid email address.', 8 | required: 'The field "{0}" is mandatory.', 9 | date: 'The field "{0}" must be a valid date ({1}).', 10 | minlength: 'The field "{0}" length must be greater than {1}.', 11 | maxlength: 'The field "{0}" length must be lower than {1}.', 12 | equalPassword: 'Passwords are different.', 13 | hasUpperCase: 'The field "{0}" must contain a upper case.', 14 | hasLowerCase: 'The field "{0}" must contain a lower case.', 15 | hasNumber: 'The field "{0}" must contain a number.', 16 | hasSpecialCharacter: 'The field "{0}" must contain a special character.', 17 | }, 18 | //Arabic language 19 | ar: { 20 | numbers: 'الحقل "{0}" يجب أن يكون رقم صحيح', 21 | email: 'الحقل "{0}" يجب أن يكون بريد إلكتروني صحيح', 22 | required: 'الحقل "{0}" مطلوب', 23 | date: 'الحقل "{0}" يجب أن يكون تاريخ صحيح ({1}).', 24 | minlength: 'الحقل "{0}" يجب أن يكون أكثر من {1}.', 25 | maxlength: 'الحقل "{0}" يجب أن يكون أقل من {1}.', 26 | equalPassword: 'كلمات المرور مختلفة.', 27 | hasNumber: 'يجب أن يحتوي الحقل "{0}" على رقم.', 28 | hasUpperCase: 'يجب أن يحتوي الحقل "{0}" على حرف كبير', 29 | hasLowerCase: 'يجب أن يحتوي الحقل "{0}" على أحرف صغيرة', 30 | hasSpecialCharacter: 'يجب أن يحتوي الحقل "{0}" على رمز خاص', 31 | }, 32 | // French language 33 | fr: { 34 | numbers: 'Le champ "{0}" doit être un nombre valide.', 35 | email: 'Le champ "{0}" doit être une adresse email valide.', 36 | required: 'Le champ "{0}" est obligatoire.', 37 | date: 'Le champ "{0}" doit correspondre à une date valide ({1}).', 38 | minlength: 'Le nombre de caractère du champ "{0}" doit être supérieur à {1}.', 39 | maxlength: 'Le nombre de caractère du champ "{0}" doit être inférieur à {1}.', 40 | equalPassword: 'Les mots de passes sont differents', 41 | hasNumber: 'Le champ "{0}" doit contenir un chiffre.', 42 | hasUpperCase: 'Le champ "{0}" doit contenir une majuscule', 43 | hasLowerCase: 'Le champ "{0}" doit contenir une minuscule', 44 | hasSpecialCharacter: 'Le champ "{0}" doit contenir un caractère spécial', 45 | }, 46 | // Persian (Farsi) language 47 | fa: { 48 | numbers: 'فیلد "{0}" باید یک عدد باشد.', 49 | email: 'فیلد "{0}" باید یک آدرس ایمیل باشد.', 50 | required: 'فیلد "{0}" نباید خالی باشد.', 51 | date: 'فیلد "{0}" باید یک تاریخ ({1}) باشد.', 52 | minlength: 'طول فیلد "{0}" باید بیشتر از "{1}" باشد.', 53 | maxlength: 'طول فیلد "{0}" باید کمتر از "{1}" باشد.', 54 | equalPassword: 'رمزهای عبور متفاوت هستند', 55 | hasNumber: 'قسمت "{0}" باید حاوی یک عدد باشد.', 56 | hasUpperCase: 'قسمت "{0}" باید شامل یک حرف بزرگ باشد', 57 | hasLowerCase: 'قسمت "{0}" باید دارای حروف کوچک باشد', 58 | hasSpecialCharacter: 'قسمت "{0}" باید دارای یک نویسه خاص باشد', 59 | }, 60 | // Indonesian language 61 | id: { 62 | numbers: '"{0}" harus berupa angka', 63 | email: 'Format email pada "{0}" harus valid', 64 | required: '"{0}" harus diisi', 65 | date: 'Format tanggal pada "{0}" harus ({1})', 66 | minlength: '"{0}" harus lebih dari {1} karakter', 67 | maxlength: '"{0}" harus kurang dari {1} karakter', 68 | equalPassword: 'Kata sandi berbeda', 69 | hasNumber: '"{0}" harus berisi angka.', 70 | hasUpperCase: '"{0}" harus mengandung huruf kapital', 71 | hasLowerCase: '"{0}" harus berisi huruf kecil', 72 | hasSpecialCharacter: '"{0}" harus berisi karakter khusus', 73 | }, 74 | // Portuguese language of Brazil 75 | ptBR: { 76 | numbers: 'O campo "{0}" precisar conter um número válido.', 77 | email: 'O campo "{0}" precisa conter um email válido.', 78 | required: 'O campo "{0}" é obrigatório.', 79 | date: 'O campo "{0}" precisa conter uma data válida ({1}).', 80 | minlength: 'O campo "{0}" precisa ser maior que {1} caracteres.', 81 | maxlength: 'O campo "{0}" precisa ser menor que {1} caracteres.', 82 | equalPassword: 'As senhas são diferentes', 83 | hasNumber: 'O campo "{0}" deve conter um número.', 84 | hasUpperCase: 'O campo "{0}" deve conter uma letra maiúscula', 85 | hasLowerCase: 'O campo "{0}" deve conter letras minúsculas', 86 | hasSpecialCharacter: 'O campo "{0}" deve conter um caractere especial', 87 | }, 88 | // Spanish language 89 | es: { 90 | numbers: 'El campo "{0}" debe ser un número válido.', 91 | email: 'El campo "{0}" debe ser un email válido.', 92 | required: 'El campo "{0}" es requerido.', 93 | date: 'El campo "{0}" debe contener una fecha válida ({1}).', 94 | minlength: 'La longitud del campo "{0}" debe ser mayor que {1} caracteres', 95 | maxlength: 'La longitud del campo "{0}" debe ser menor que {1} caracteres.', 96 | equalPassword: 'Las contraseñas son diferentes', 97 | hasNumber: 'El campo "{0}" debe contener un número.', 98 | hasUpperCase: 'El campo "{0}" debe contener una letra mayúscula', 99 | hasLowerCase: 'El campo "{0}" debe contener minúsculas', 100 | hasSpecialCharacter: 'El campo "{0}" debe contener un carácter especial', 101 | }, 102 | // Turkish language - Used by default 103 | tr: { 104 | numbers: '"{0}" alanı geçerli bir sayı olmalıdır.', 105 | email: '"{0}" alanı geçerli bir email adresi olmalıdır.', 106 | required: '"{0}" alanı gereklidir.', 107 | date: '"{0}" alanı geçerli bir tarih olmalıdır. ({1}).', 108 | minlength: '"{0}" alanı {1} karakterden daha uzun olmalıdır.', 109 | maxlength: '"{0}" alanı {1} karakterden daha kısa olmalıdır.', 110 | equalPassword: 'Şifreler farklı', 111 | hasNumber: '"{0}" alanı bir sayı içermelidir.', 112 | hasUpperCase: '"{0}" alanı büyük harf içermelidir', 113 | hasLowerCase: '"{0}" alanı küçük harf içermelidir', 114 | hasSpecialCharacter: '"{0}" alanı özel bir karakter içermelidir', 115 | }, 116 | it: { 117 | numbers: 'Il campo "{0}" deve contenere solo numeri.', 118 | email: 'Il campo "{0}" deve contenere un indirizzo email valido.', 119 | required: 'Il campo "{0}" é obbligatorio.', 120 | date: 'Il campo "{0}" deve contenere una data valida ({1}).', 121 | minlength: 'Il campo "{0}" deve contenere almeno {1} caratteri.', 122 | maxlength: 'Il campo "{0}" deve contenere meno di {1} caratteri.', 123 | equalPassword: 'Le passwords inserite sono differenti', 124 | hasUpperCase: 'Il campo "{0}" deve contenere una maiuscola', 125 | hasLowerCase: 'Il campo "{0}" deve contenere una minuscola', 126 | hasNumber: 'Il campo "{0}" deve contenere un numero', 127 | hasSpecialCharacter: 'Il campo "{0}" deve contenere un carattere speciale', 128 | }, 129 | // TODO Add other languages here... 130 | }; 131 | 132 | export default defaultMessages; 133 | -------------------------------------------------------------------------------- /src/rules/defaultRules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import moment from 'moment'; 4 | 5 | // Custom default rules to validate form fields 6 | const defaultRules = { 7 | numbers: /^(([0-9]*)|(([0-9]*)\.([0-9]*)))$/, 8 | email: 9 | /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, 10 | required: /\S+/, 11 | hasNumber: /\d/, 12 | hasUpperCase: /(?=.*[A-Z])/, 13 | hasLowerCase: /(?=.*[a-z])/, 14 | hasSpecialCharacter: /(\W)/, 15 | date(format = 'YYYY-MM-DD', value) { 16 | const d = moment(value, format); 17 | if (d == null || !d.isValid()) return false; 18 | return true; 19 | }, 20 | minlength(length, value) { 21 | if (length === void 0) { 22 | throw 'ERROR: It is not a valid length, checkout your minlength settings.'; 23 | } else if (value.length >= length) { 24 | return true; 25 | } 26 | return false; 27 | }, 28 | maxlength(length, value) { 29 | if (length === void 0) { 30 | throw 'ERROR: It is not a valid length, checkout your maxlength settings.'; 31 | } else if (value.length > length) { 32 | return false; 33 | } 34 | return true; 35 | }, 36 | equalPassword(dataToCompare, value) { 37 | return dataToCompare === value; 38 | } 39 | }; 40 | 41 | export default defaultRules; 42 | -------------------------------------------------------------------------------- /src/use-validation.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { useState, useRef } from 'react'; 3 | import defaultMessages from './messages/defaultMessages'; 4 | import defaultRules from './rules/defaultRules'; 5 | 6 | const useValidation = (props) => { 7 | const { state = {} } = props; 8 | // array to store error on each fields 9 | // ex: 10 | // [{ fieldName: "name", messages: ["The field name is required."] }] 11 | // Retrieve props 12 | const deviceLocale = props.deviceLocale || 'en'; // ex: en, fr 13 | const baseRules = props.rules || defaultRules; // rules for Validation 14 | const messages = props.messages || defaultMessages; 15 | const labels = props.labels || {}; 16 | const errors = useRef([]); 17 | const [internalErrors, setInternalErrors] = useState([]); 18 | 19 | /* 20 | * Method validate to verify if each children respect the validator rules 21 | * Fields example (Array) : 22 | * fields = { 23 | * input1: { 24 | * required:true, 25 | * numbers:true, 26 | * maxLength:5 27 | * } 28 | *} 29 | */ 30 | const validate = (fields) => { 31 | // Reset errors 32 | _resetErrors(); 33 | // Iterate over inner state 34 | for (const key of Object.keys(state)) { 35 | // Check if child name is equals to fields array set up in parameters 36 | const rules = fields[key]; 37 | if (rules) { 38 | // Check rule for current field 39 | _checkRules(key, rules, state[key]); 40 | } 41 | } 42 | return isFormValid(); 43 | }; 44 | 45 | // Method to check rules on a spefific field 46 | const _checkRules = (fieldName, rules, value) => { 47 | if (!value && !rules.required) { 48 | return; // if value is empty AND its not required by the rules, no need to check any other rules 49 | } 50 | for (const key of Object.keys(rules)) { 51 | const isRuleFn = typeof baseRules[key] == 'function'; 52 | const isRegExp = baseRules[key] instanceof RegExp; 53 | if ((isRuleFn && !baseRules[key](rules[key], value)) || (isRegExp && !baseRules[key].test(value))) { 54 | _addError(fieldName, key, rules[key], isRuleFn); 55 | } 56 | } 57 | }; 58 | 59 | // Add error 60 | // ex: 61 | // [{ fieldName: "name", messages: ["The field name is required."] }] 62 | const _addError = (fieldName, rule, value, isFn) => { 63 | const name = labels[fieldName]; 64 | value = rule == 'minlength' ? value - 1 : value; 65 | const errMsg = messages[deviceLocale][rule].replace('{0}', name || fieldName).replace('{1}', value); 66 | let [error] = errors.current.filter((err) => err.fieldName === fieldName); 67 | // error already exists 68 | if (error) { 69 | // Update existing element 70 | const index = errors.current.indexOf(error); 71 | error.messages.push(errMsg); 72 | error.failedRules.push(rule); 73 | errors.current[index] = error; 74 | } else { 75 | // Add new item 76 | errors.current.push({ 77 | fieldName, 78 | failedRules: [rule], 79 | messages: [errMsg] 80 | }); 81 | setInternalErrors(errors.current); 82 | } 83 | }; 84 | 85 | // Reset error fields 86 | const _resetErrors = () => { 87 | errors.current = []; 88 | setInternalErrors([]); 89 | }; 90 | 91 | // Method to check if the field is in error 92 | const isFieldInError = (fieldName) => { 93 | return internalErrors.filter((err) => err.fieldName === fieldName).length > 0; 94 | }; 95 | 96 | const isFormValid = () => { 97 | return errors.current?.length == 0; 98 | }; 99 | 100 | // Return an object where the keys are the field names and the value is an array with the rules that failed validation 101 | const getFailedRules = () => { 102 | let failedRulesPerField = {}; 103 | for (let index = 0; index < internalErrors.length; index++) { 104 | let error = internalErrors[index]; 105 | failedRulesPerField[error.fieldName] = error.failedRules; 106 | } 107 | return failedRulesPerField; 108 | }; 109 | 110 | // Return the rules that failed validation for the given field 111 | const getFailedRulesInField = (fieldName) => { 112 | const foundError = internalErrors.find((err) => err.fieldName === fieldName); 113 | if (!foundError) { 114 | return []; 115 | } 116 | return foundError.failedRules; 117 | }; 118 | 119 | // Concatenate each error messages 120 | const getErrorMessages = (separator = '\n') => { 121 | return internalErrors.map((err) => err.messages.join(separator)).join(separator); 122 | }; 123 | 124 | // Method to return errors on a specific field 125 | const getErrorsInField = (fieldName) => { 126 | const foundError = internalErrors.find((err) => err.fieldName === fieldName); 127 | if (!foundError) { 128 | return []; 129 | } 130 | return foundError.messages; 131 | }; 132 | 133 | return { 134 | validate, 135 | isFormValid, 136 | isFieldInError, 137 | getFailedRules, 138 | getFailedRulesInField, 139 | getErrorMessages, 140 | getErrorsInField 141 | }; 142 | }; 143 | 144 | useValidation.propTypes = { 145 | deviceLocale: PropTypes.string, // Used for language locale 146 | rules: PropTypes.object, // rules for validations 147 | messages: PropTypes.object, // messages for validation errors 148 | labels: PropTypes.object // labels for validation messages 149 | }; 150 | 151 | useValidation.defaultProps = { 152 | deviceLocale: 'en', 153 | rules: defaultRules, 154 | messages: defaultMessages, 155 | labels: {} 156 | }; 157 | 158 | export default useValidation; 159 | --------------------------------------------------------------------------------