├── .eslintignore ├── .npmignore ├── .babelrc ├── src ├── index.js ├── utils │ ├── isSome.js │ ├── defined.js │ ├── index.js │ └── flattenErrorsObject.js ├── validationFactory.js └── components │ └── validationMixin.js ├── .gitignore ├── karma.conf.js ├── .eslintrc ├── spec ├── utils │ └── isSomeSpec.js ├── components │ ├── MixinSpec.js │ └── Signup.js └── ValidationFactorySpec.js ├── LICENSE ├── README.md └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | examples 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-loose", "react", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Validation from './components/validationMixin'; 2 | 3 | module.exports = Validation; 4 | -------------------------------------------------------------------------------- /src/utils/isSome.js: -------------------------------------------------------------------------------- 1 | export default function isSome(item, ...tests) { 2 | if (Array.isArray(tests)) { 3 | return tests.some(test => item === test); 4 | } 5 | return false; 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/defined.js: -------------------------------------------------------------------------------- 1 | import isSome from './isSome'; 2 | 3 | export default function defined(...items) { 4 | if (Array.isArray(items)) { 5 | return items.every(item => !isSome(item, null, undefined)); 6 | } 7 | return false; 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import defined from './defined'; 2 | import isSome from './isSome'; 3 | import flattenErrorsObject from './flattenErrorsObject'; 4 | 5 | module.exports = { 6 | defined, 7 | isSome, 8 | flattenErrorsObject, 9 | }; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # exact folders 2 | /out/ 3 | /examples/ 4 | /lib 5 | 6 | # masks 7 | .idea 8 | node_modules 9 | *.log 10 | *.sublime-project 11 | *.sublime-workspace 12 | .DS_Store 13 | 14 | # demo 15 | index.html 16 | /spec/demo.js 17 | /lib/bundle.js 18 | -------------------------------------------------------------------------------- /src/utils/flattenErrorsObject.js: -------------------------------------------------------------------------------- 1 | import values from 'lodash.values'; 2 | import flatten from 'lodash.flatten'; 3 | 4 | export default function flattenErrorsObject(obj) { 5 | if (typeof obj === 'string') { 6 | return obj; 7 | } 8 | 9 | return flatten(values(obj).map(value => { 10 | if (typeof obj !== 'string') { 11 | return flattenErrorsObject(value); 12 | } 13 | return value; 14 | })); 15 | } 16 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | basePath: '', 4 | frameworks: ['browserify', 'mocha'], 5 | files: ['spec/**/*Spec.js'], 6 | exclude: [], 7 | preprocessors: { 8 | 'spec/**/*.js': ['browserify'] 9 | }, 10 | browserify: { 11 | transform: ['babelify'], 12 | watch: true 13 | }, 14 | reporters: ['progress'], 15 | port: 9876, 16 | colors: true, 17 | logLevel: config.LOG_DEBUG, 18 | autoWatch: true, 19 | browsers: ['Chrome'], 20 | singleRun: false 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "parserOptions":{ 4 | "ecmaFeatures": { 5 | "experimentalObjectRestSpread": true 6 | } 7 | }, 8 | "env": { 9 | "browser": true, 10 | "mocha": true, 11 | "node": true 12 | }, 13 | "rules": { 14 | "import/no-unresolved": 0, 15 | "max-len": 0, 16 | "no-underscore-dangle": 0, 17 | "prefer-rest-params": 0, 18 | "react/jsx-uses-react": 2, 19 | "react/jsx-uses-vars": 2, 20 | "react/no-multi-comp": 0, 21 | "react/prefer-stateless-function": 0, 22 | "react/react-in-jsx-scope": 2 23 | }, 24 | "plugins": [ 25 | "react" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /spec/utils/isSomeSpec.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {isSome} from '../../src/utils'; 3 | 4 | describe('isSome', function() { 5 | it('ensures strictly equal to single argument', function() { 6 | var item; 7 | const result = isSome(item, undefined); 8 | expect(result).to.equal(true); 9 | }); 10 | it('ensures strictly equal to multiple arguments', function() { 11 | var item; 12 | const result = isSome(item, undefined, null); 13 | expect(result).to.equal(true); 14 | }); 15 | it('ensures returns false when no arguments provided', function() { 16 | var item; 17 | const result = isSome(item); 18 | expect(result).to.equal(false); 19 | }); 20 | it('ensures strictly equal to multiple arguments negative', function() { 21 | var item = 'a'; 22 | const result = isSome(item, null, undefined); 23 | expect(result).to.equal(false); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/validationFactory.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import isEmpty from 'lodash.isempty'; 3 | import get from 'lodash.get'; 4 | import { defined, flattenErrorsObject } from './utils'; 5 | 6 | export default function validationFactory(strategy) { 7 | const _strategy = typeof strategy === 'function' ? strategy() : strategy; 8 | invariant(defined(_strategy), 'Validation strategy not provided. A user provided strategy is expected.'); 9 | invariant(typeof _strategy !== 'function', 'Validation strategy improperly initialized. Refer to documentation of the provided strategy.'); 10 | return { 11 | 12 | getValidationMessages(errors = {}, key) { 13 | if (isEmpty(errors)) { 14 | return []; 15 | } 16 | if (key === undefined) { 17 | return flattenErrorsObject(errors); 18 | } 19 | return get(errors, key); 20 | }, 21 | 22 | isValid(errors, key) { 23 | if (!defined(key)) return isEmpty(errors); 24 | return isEmpty(get(errors, key)); 25 | }, 26 | 27 | ..._strategy, 28 | 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Clint Ayres 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-validation-mixin 2 | _Simple validation library for React._ 3 | 4 | This library simply wraps your React Component, transferring it props containing the boilerplate to validate a React form. 5 | 6 | **react-validation-mixin** aims to provide a low-level toolkit for _React.Component_ validation, relying on existing validation libraries. 7 | 8 | This library currently supports at least two strategies and the community is urged to expand the available strategies. Each strategy is responsible for the data validation and error message responses. A complete list of strategies can be found at [the end of this page](#strategies). 9 | 10 | Users of the library are required to install and include the mixin and a chosen strategy. 11 | 12 | --- 13 | 14 | ### [View the Demo](http://jurassix.github.io/react-validation-mixin/) 15 | 16 | ### [Read the Documentation](http://jurassix.gitbooks.io/docs-react-validation-mixin/content/overview/index.html) 17 | 18 | ### [Steps for migrating from 4.x](http://jurassix.gitbooks.io/docs-react-validation-mixin/content/overview/migration-to-5.html) 19 | 20 | --- 21 | 22 | ### Install 23 | 24 | Install **mixin** via npm OR yarn: 25 | 26 | ```javascript 27 | > npm install --save react-validation-mixin 28 | ``` 29 | ```javascript 30 | > yarn add react-validation-mixin 31 | ``` 32 | Install **validation strategy** via npm OR yarn: 33 | 34 | ```javascript 35 | > npm install --save joi-validation-strategy 36 | ``` 37 | ```javascript 38 | > yarn add joi-validation-strategy 39 | ``` 40 | Make sure you install the peer dependency [Joi](https://github.com/hapijs/joi) if using the _joi-validation-strategy_: 41 | 42 | ```javascript 43 | > npm install --save joi 44 | ``` 45 | ```javascript 46 | > yarn add joi 47 | ``` 48 | ### Strategies 49 | 50 | Current list of validation strategy implementations to choose from: 51 | 52 | * [joi-validation-strategy](https://github.com/jurassix/joi-validation-strategy) - based on [Joi](https://github.com/hapijs/joi) 53 | * [json-schema-validation-strategy](https://github.com/jefersondaniel/json-schema-validation-strategy) - based on [jsen] 54 | (https://github.com/bugventure/jsen) 55 | * [react-validatorjs-strategy](https://github.com/TheChech/react-validatorjs-strategy) - based on [validatorjs](https://github.com/skaterdav85/validatorjs) 56 | 57 | 58 | --- 59 | 60 | ### _Please contribute suggestions, features, issues, and pull requests._ 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-validation-mixin", 3 | "version": "5.4.0", 4 | "description": "Simple Validation Mixin for React.", 5 | "main": "./lib/index.js", 6 | "files": [ 7 | "lib" 8 | ], 9 | "scripts": { 10 | "build": "npm run lint && npm run build:lib && npm run build:spec", 11 | "build:lib": "mkdirp lib && babel src -d lib", 12 | "build:spec": "mkdirp lib/spec && babel spec -d lib/spec", 13 | "test": "karma start karma.conf.js --single-run", 14 | "test-watch": "karma start karma.conf.js", 15 | "clean": "rimraf lib", 16 | "lint": "eslint src", 17 | "prepublish": "npm run clean && npm run build", 18 | "precommit": "npm test && npm build", 19 | "prepush": "npm test && npm build", 20 | "postmerge": "npm install && npm build" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/jurassix/react-validation-mixin.git" 25 | }, 26 | "keywords": [ 27 | "react-component", 28 | "react", 29 | "validation", 30 | "validator", 31 | "mixin", 32 | "joi", 33 | "schema" 34 | ], 35 | "author": "jurassix (clinton.ayres@gmail.com)", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/jurassix/react-validation-mixin/issues" 39 | }, 40 | "homepage": "https://github.com/jurassix/react-validation-mixin", 41 | "devDependencies": { 42 | "babel": "^6.5.2", 43 | "babel-cli": "^6.10.1", 44 | "babel-core": "^6.9.1", 45 | "babel-eslint": "^6.0.4", 46 | "babel-loader": "^6.2.4", 47 | "babel-preset-es2015": "^6.9.0", 48 | "babel-preset-es2015-loose": "^7.0.0", 49 | "babel-preset-react": "^6.5.0", 50 | "babel-preset-stage-0": "^6.5.0", 51 | "babelify": "^7.3.0", 52 | "browserify": "^13.0.1", 53 | "chai": "^3.0.0", 54 | "classnames": "^2.1.3", 55 | "eslint": "^2.12.0", 56 | "eslint-config-airbnb": "9.0.1", 57 | "eslint-plugin-import": "^1.8.1", 58 | "eslint-plugin-jsx-a11y": "^1.4.2", 59 | "eslint-plugin-react": "^5.1.1", 60 | "husky": "^0.11.4", 61 | "joi": "^8.4.2", 62 | "joi-validation-strategy": "0.3.3", 63 | "karma": "^0.13.9", 64 | "karma-browserify": "^5.0.5", 65 | "karma-chrome-launcher": "^1.0.1", 66 | "karma-mocha": "^1.0.1", 67 | "lodash.set": "^4.2.0", 68 | "mkdirp": "^0.5.1", 69 | "mocha": "^2.2.5", 70 | "react": "^15.1.0", 71 | "react-addons-test-utils": "^15.1.0", 72 | "react-dom": "^15.1.0", 73 | "rimraf": "^2.4.3", 74 | "watchify": "^3.7.0", 75 | "webpack": "^1.13.1" 76 | }, 77 | "dependencies": { 78 | "invariant": "^2.1.0", 79 | "lodash.flatten": "^4.2.0", 80 | "lodash.get": "^4.3.0", 81 | "lodash.isempty": "^4.2.1", 82 | "lodash.result": "^4.4.0", 83 | "lodash.values": "^4.1.0", 84 | "react-display-name": "0.2.0" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /spec/components/MixinSpec.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import React from 'react'; 3 | import {findDOMNode} from 'react-dom'; 4 | import SignupComponent from './Signup'; 5 | import TestUtils from 'react-addons-test-utils'; 6 | import strategy from 'joi-validation-strategy'; 7 | import validation from '../../src/components/validationMixin'; 8 | 9 | const Signup = validation(strategy)(SignupComponent); 10 | 11 | describe('Validation Mixin', function() { 12 | it('wraps components displayName correctly', () => { 13 | expect(Signup.displayName).to.equal('Validation(Signup)') 14 | }); 15 | it('validates field on blur', function() { 16 | const signup = TestUtils.renderIntoDocument(); 17 | const email = findDOMNode(signup.refs.component.refs.email); 18 | 19 | TestUtils.Simulate.blur(email); 20 | expect(signup.isValid('email')).to.equal(false); 21 | TestUtils.Simulate.change(email, { 22 | target: { 23 | value: 'invalid.email.com' 24 | } 25 | }); 26 | 27 | TestUtils.Simulate.blur(email); 28 | expect(signup.isValid('email')).to.equal(false); 29 | 30 | TestUtils.Simulate.change(email, { 31 | target: { 32 | value: 'valid@email.com' 33 | } 34 | }); 35 | 36 | TestUtils.Simulate.blur(email); 37 | expect(signup.isValid('email')).to.equal(true); 38 | }); 39 | 40 | it('ensure previous invalid fields remain invalid', function() { 41 | const signup = TestUtils.renderIntoDocument(); 42 | const email = findDOMNode(signup.refs.component.refs.email); 43 | const username = findDOMNode(signup.refs.component.refs.username); 44 | 45 | TestUtils.Simulate.blur(email); 46 | expect(signup.isValid('username')).to.equal(true); 47 | expect(signup.isValid('email')).to.equal(false); 48 | 49 | TestUtils.Simulate.blur(username); 50 | expect(signup.isValid('username')).to.equal(false); 51 | expect(signup.isValid('email')).to.equal(false); 52 | }); 53 | 54 | it('ensure submit on invalid form is invalid', function(done) { 55 | const signup = TestUtils.renderIntoDocument(); 56 | const form = TestUtils.findRenderedDOMComponentWithTag(signup, 'form'); 57 | 58 | //need to mock for submit 59 | signup.refs.component.props.validate(function() { 60 | expect(signup.isValid()).to.equal(false); 61 | done(); 62 | }); 63 | }); 64 | 65 | it('ensure submit on valid form is valid', function(done) { 66 | const signup = TestUtils.renderIntoDocument(); 67 | const form = TestUtils.findRenderedDOMComponentWithTag(signup, 'form'); 68 | 69 | const firstName = findDOMNode(signup.refs.component.refs.firstName); 70 | const lastName = findDOMNode(signup.refs.component.refs.lastName); 71 | const email = findDOMNode(signup.refs.component.refs.email); 72 | const username = findDOMNode(signup.refs.component.refs.username); 73 | const password = findDOMNode(signup.refs.component.refs.password); 74 | const verifyPassword = findDOMNode(signup.refs.component.refs.verifyPassword); 75 | const tv = findDOMNode(signup.refs.component.refs.tv); 76 | 77 | TestUtils.Simulate.change(firstName, { 78 | target: { 79 | value: 'foo' 80 | } 81 | }); 82 | 83 | TestUtils.Simulate.change(lastName, { 84 | target: { 85 | value: 'boo' 86 | } 87 | }); 88 | 89 | TestUtils.Simulate.change(email, { 90 | target: { 91 | value: 'foo@boo.com' 92 | } 93 | }); 94 | 95 | TestUtils.Simulate.change(username, { 96 | target: { 97 | value: 'seaweed' 98 | } 99 | }); 100 | 101 | TestUtils.Simulate.change(password, { 102 | target: { 103 | value: 'Luxury123' 104 | } 105 | }); 106 | 107 | TestUtils.Simulate.change(verifyPassword, { 108 | target: { 109 | value: 'Luxury123' 110 | } 111 | }); 112 | 113 | TestUtils.Simulate.change(tv, { 114 | target: { 115 | value: 'tv' 116 | } 117 | }); 118 | 119 | //need to mock for submit 120 | signup.refs.component.props.validate(function() { 121 | expect(signup.isValid()).to.equal(true); 122 | done(); 123 | }); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /src/components/validationMixin.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import invariant from 'invariant'; 3 | import result from 'lodash.result'; 4 | import factory from '../validationFactory'; 5 | import getDisplayName from 'react-display-name'; 6 | import { defined } from '../utils'; 7 | 8 | export default function validationMixin(strategy) { 9 | const validator = factory(strategy); 10 | return function wrappedComponentFn(WrappedComponent) { 11 | invariant(defined(WrappedComponent), 'Component was not provided to the Validator. Export you Component with "export default validator(strategy)(Component);"'); 12 | class Validation extends React.Component { 13 | 14 | constructor(props, context) { 15 | super(props, context); 16 | this.render = this.render.bind(this); 17 | this.validate = this.validate.bind(this); 18 | this.isValid = this.isValid.bind(this); 19 | this.getValidationMessages = this.getValidationMessages.bind(this); 20 | this.clearValidations = this.clearValidations.bind(this); 21 | this.handleValidation = this.handleValidation.bind(this); 22 | this._invokeCallback = this._invokeCallback.bind(this); 23 | 24 | this.state = { errors: {} }; 25 | } 26 | 27 | /* Get current validation messages for a specified key or entire form. 28 | * 29 | * @param {?String} key to get messages, or entire form if key is undefined. 30 | * @return {Array} 31 | */ 32 | getValidationMessages(key) { 33 | return validator.getValidationMessages(this.state.errors, key) || []; 34 | } 35 | 36 | /* Convenience method to validate a key via an event handler. Useful for 37 | * onBlur, onClick, onChange, etc... 38 | * 39 | * @param {?String} State key to validate 40 | * @return {function} validation event handler 41 | */ 42 | handleValidation(key, callback) { 43 | return () => { 44 | this.validate(key, callback); 45 | }; 46 | } 47 | 48 | /* Method to validate single form key or entire form against the component data. 49 | * 50 | * @param {String|Function} key to validate, or error-first containing the validation errors if any. 51 | * @param {?Function} error-first callback containing the validation errors if any. 52 | */ 53 | validate(/* [key], callback */) { 54 | const fallback = arguments.length <= 1 && typeof arguments[0] === 'function' ? arguments[0] : undefined; 55 | const key = arguments.length <= 1 && typeof arguments[0] === 'function' ? undefined : arguments[0]; 56 | const callback = arguments.length <= 2 && typeof arguments[1] === 'function' ? arguments[1] : fallback; 57 | 58 | const data = result(this.refs.component, 'getValidatorData'); 59 | const schema = result(this.refs.component, 'validatorTypes'); 60 | 61 | invariant(defined(data), 'Data was not provided to the Validator. Implement "getValidatorData" to return data.'); 62 | invariant(defined(schema), 'A schema was not provided to the Validator. Implement "validatorTypes" to return a validation schema.'); 63 | 64 | const options = { 65 | key, 66 | prevErrors: this.state.errors, 67 | }; 68 | validator.validate(data, schema, options, nextErrors => { 69 | this.setState({ errors: { ...nextErrors } }, this._invokeCallback.bind(this, key, callback)); 70 | }); 71 | } 72 | 73 | /* Clear all previous validations 74 | * 75 | * @return {void} 76 | */ 77 | clearValidations(callback) { 78 | return this.setState({ 79 | errors: {}, 80 | }, callback); 81 | } 82 | 83 | /* Check current validity for a specified key or entire form. 84 | * 85 | * @param {?String} key to check validity (entire form if undefined). 86 | * @return {Boolean}. 87 | */ 88 | isValid(key) { 89 | return validator.isValid(this.state.errors, key); 90 | } 91 | 92 | /* Private method that handles executing users callback on validation 93 | * 94 | * @param {Object} errors object keyed on data field names. 95 | * @param {Function} error-first callback containing the validation errors if any. 96 | */ 97 | _invokeCallback(key, callback) { 98 | if (typeof callback !== 'function') { 99 | return; 100 | } 101 | if (this.isValid(key)) { 102 | callback(); 103 | } else { 104 | callback(this.state.errors); 105 | } 106 | } 107 | 108 | render() { 109 | return ( 110 | 120 | ); 121 | } 122 | } 123 | Validation.displayName = `Validation(${getDisplayName(WrappedComponent)})`; 124 | return Validation; 125 | }; 126 | } 127 | -------------------------------------------------------------------------------- /spec/components/Signup.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classnames from 'classnames'; 3 | import Joi from 'joi'; 4 | import set from 'lodash.set'; 5 | 6 | var Signup = React.createClass({ 7 | displayName: 'Signup', 8 | validatorTypes: { 9 | auth: { 10 | firstName: Joi.string().required().label('First Name'), 11 | lastName: Joi.string().allow(null).label('Last Name'), 12 | }, 13 | email: Joi.string().email().label('Email Address'), 14 | username: Joi.string().alphanum().min(3).max(30).required().label('Username'), 15 | password: Joi.string().regex(/[a-zA-Z0-9]{3,30}/).label('Password'), 16 | verifyPassword: Joi.any().valid(Joi.ref('password')).required().label('Password Confirmation'), 17 | referral: Joi.any().valid('tv', 'radio'), 18 | rememberMe: Joi.boolean(), 19 | }, 20 | getValidatorData: function() { 21 | return this.state; 22 | }, 23 | getInitialState: function() { 24 | return { 25 | auth: { 26 | firstName: null, 27 | lastName: null, 28 | }, 29 | email: null, 30 | username: null, 31 | password: null, 32 | verifyPassword: null, 33 | rememberMe: 'off', 34 | referral: null, 35 | feedback: null, 36 | }; 37 | }, 38 | render: function() { 39 | return ( 40 |
41 |

Signup

42 |
43 |
44 |
45 | 46 | 47 | {this.props.getValidationMessages('auth.firstName').map(this.renderHelpText)} 48 |
49 |
50 | 51 | 52 |
53 |
54 | 55 | 56 | {this.props.getValidationMessages('email').map(this.renderHelpText)} 57 |
58 |
59 | 60 | 61 | {this.props.getValidationMessages('username').map(this.renderHelpText)} 62 |
63 |
64 | 65 | 66 | {this.props.getValidationMessages('password').map(this.renderHelpText)} 67 |
68 |
69 | 70 | 71 | {this.props.getValidationMessages('verifyPassword').map(this.renderHelpText)} 72 |
73 |
74 | 75 | 79 | 83 | {this.props.getValidationMessages('referral').map(this.renderHelpText)} 84 |
85 |
86 | 90 | {this.props.getValidationMessages('rememberMe').map(this.renderHelpText)} 91 |
92 |
93 |

{this.state.feedback}

94 |
95 |
96 | 97 | {' '} 98 | 99 |
100 |
101 |
102 |
103 | ) 104 | }, 105 | renderHelpText: function(message) { 106 | return ( 107 | {message} 108 | ); 109 | }, 110 | getClasses: function(field) { 111 | return classnames({ 112 | 'form-group': true, 113 | 'has-error': !this.props.isValid(field) 114 | }); 115 | }, 116 | onChange: function(field) { 117 | return event => { 118 | this.setState(set(this.state, field, event.target.value)); 119 | }; 120 | }, 121 | onCheckboxChange: function(field) { 122 | return event => { 123 | let state = {}; 124 | state[field] = this.state[field] === 'on' ? 'off' : 'on'; 125 | this.setState(state, this.props.handleValidation(field)); 126 | }; 127 | }, 128 | onRadioChange: function(field) { 129 | return event => { 130 | let state = {}; 131 | state[field] = event.target.value; 132 | this.setState(state, this.props.handleValidation(field)); 133 | }; 134 | }, 135 | handleReset: function(event) { 136 | event.preventDefault(); 137 | this.props.clearValidations(); 138 | this.setState(this.getInitialState()); 139 | }, 140 | handleSubmit: function(event) { 141 | event.preventDefault(); 142 | var onValidate = error => { 143 | if (error) { 144 | this.setState({ 145 | feedback: 'Form is invalid do not submit' 146 | }); 147 | } else { 148 | this.setState({ 149 | feedback: 'Form is valid send to action creator' 150 | }); 151 | } 152 | }; 153 | this.props.validate(onValidate); 154 | } 155 | }); 156 | 157 | module.exports = Signup; 158 | -------------------------------------------------------------------------------- /spec/ValidationFactorySpec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import Joi from 'joi'; 3 | import factory from '../src/validationFactory'; 4 | import strategy from 'joi-validation-strategy'; 5 | 6 | const validator = factory(strategy); 7 | 8 | describe('Validation Factory', () => { 9 | describe('validation()', () => { 10 | describe('edge cases', () => { 11 | it('should use `schema` keys on empty schema', done => { 12 | validator.validate({}, undefined, undefined, (result) => { 13 | expect(result).to.eql({}); 14 | done(); 15 | }); 16 | }); 17 | 18 | it('should use `schema` keys when schema provided', done => { 19 | validator.validate({ 20 | username: Joi.string() 21 | }, undefined, undefined, (result) => { 22 | expect(result).to.eql({}); 23 | done(); 24 | }); 25 | }); 26 | 27 | it('should use `data` keys on empty data', done => { 28 | validator.validate(undefined, {}, undefined, (result) => { 29 | expect(result).to.eql({}); 30 | done(); 31 | }); 32 | }); 33 | 34 | it('should use `data` keys when data provided', done => { 35 | validator.validate(undefined, { 36 | username: 'foo' 37 | }, undefined, (result) => { 38 | expect(result).to.eql({}); 39 | done(); 40 | }); 41 | }); 42 | }); 43 | 44 | describe('of entire form', () => { 45 | it('should handle mix of valid, invalid and undefined inputs', done => { 46 | var schema = { 47 | username: Joi.string().required(), 48 | age: Joi.number(), 49 | bonus: Joi.boolean(), // data is undefined 50 | }; 51 | var data = { 52 | username: '', // invalid 53 | password: 'qwerty', // valid required 54 | age: 10, // valid optional 55 | something: 'xyz' // schema is undefined 56 | }; 57 | validator.validate(data, schema, undefined, (result) => { 58 | expect(result).to.deep.eql({ 59 | username: ['"username" is not allowed to be empty'], 60 | }); 61 | done(); 62 | }); 63 | }); 64 | 65 | it('should return multiple errors for multiple failed validations', done => { 66 | var schema = { 67 | password: Joi.string().alphanum().min(6), 68 | }; 69 | var data = { 70 | password: '???', 71 | }; 72 | validator.validate(data, schema, undefined, (result) => { 73 | expect(result['password']).to.deep.eql(['"password" must only contain alpha-numeric characters', '"password" length must be at least 6 characters long']); 74 | done(); 75 | }); 76 | }); 77 | 78 | it('should use labels from Joi Schema', done => { 79 | var schema = { 80 | username: Joi.string().alphanum().min(3).max(30).required().label('Username'), 81 | password: Joi.string().regex(/[a-zA-Z0-9]{3,30}/) 82 | }; 83 | var data = {}; 84 | validator.validate(data, schema, undefined, (result) => { 85 | expect(result['username']).to.deep.eql(['"Username" is required']); 86 | done(); 87 | }); 88 | }); 89 | 90 | it('should return array of items when validating an arry', done => { 91 | var schema = { 92 | list: Joi.array().items(Joi.string().required(), Joi.string().required()) 93 | }; 94 | var data = { 95 | list: [ 96 | 'only one item' 97 | ] 98 | }; 99 | validator.validate(data, schema, undefined, (result) => { 100 | expect(result['list']).to.deep.eql(['"list" does not contain 1 required value(s)']); 101 | done(); 102 | }); 103 | }); 104 | }); 105 | 106 | describe('of specified key', () => { 107 | it('should validate specified key only', done => { 108 | var schema = { 109 | username: Joi.string().required(), 110 | }; 111 | var data = {}; 112 | 113 | validator.validate(data, schema, { 114 | key: 'username' 115 | }, (result) => { 116 | expect(result['username']).to.deep.eql(['"username" is required']); 117 | done(); 118 | }); 119 | }); 120 | 121 | it('should not validate other fields', done => { 122 | // TODO: see https://github.com/hapijs/joi/pull/484 123 | var schema = { 124 | username: Joi.string().required(), 125 | password: Joi.string().required(), 126 | }; 127 | var data = { 128 | password: 'qwerty' 129 | }; 130 | 131 | validator.validate(data, schema, { 132 | key: 'password' 133 | }, (result) => { 134 | expect(result).to.have.keys(['password']); 135 | expect(result['password']).to.be.undefind; 136 | done(); 137 | }); 138 | }); 139 | 140 | it('should handle Joi refs', done => { 141 | // TODO: see https://github.com/hapijs/joi/pull/484 142 | var schema = { 143 | password: Joi.string().required(), 144 | verifyPassword: Joi.any().valid(Joi.ref('password')).options({ 145 | language: { 146 | any: { 147 | allowOnly: 'don\'t match password' 148 | } 149 | } 150 | }).required() 151 | }; 152 | var data = { 153 | password: 'qwerty', 154 | verifyPassword: 'qerty', 155 | }; 156 | 157 | validator.validate(data, schema, { 158 | key: 'verifyPassword' 159 | }, (result) => { 160 | expect(result['verifyPassword']).to.deep.eql(['"verifyPassword" don\'t match password']); 161 | done(); 162 | }); 163 | }); 164 | }); 165 | }); 166 | 167 | describe('getValidationMessages()', () => { 168 | describe('key is defined', () => { 169 | it('should be empty for valid input', done => { 170 | var schema = { 171 | username: Joi.string().required() 172 | }; 173 | var data = { 174 | username: 'bar' 175 | }; 176 | validator.validate(data, schema, undefined, (errors) => { 177 | var result = validator.getValidationMessages(errors, 'username'); 178 | expect(result).to.be.empty; 179 | done(); 180 | }); 181 | }); 182 | 183 | it('should decode for HTML entity encoder', done => { 184 | var label = '使用者名稱'; 185 | var schema = { 186 | username: Joi.string().required().label(label) 187 | }; 188 | var data = { 189 | username: '' 190 | }; 191 | validator.validate(data, schema, undefined, (errors) => { 192 | var result = validator.getValidationMessages(errors, 'username'); 193 | expect(result).to.deep.equal(['"' + label + '" is not allowed to be empty']); 194 | done(); 195 | }); 196 | }); 197 | 198 | it('should be have message for invalid input field', done => { 199 | var schema = { 200 | username: Joi.string().required() 201 | }; 202 | var data = {}; 203 | validator.validate(data, schema, undefined, (errors) => { 204 | var result = validator.getValidationMessages(errors, 'username'); 205 | expect(result).to.deep.equal(['"username" is required']); 206 | done(); 207 | }); 208 | }); 209 | }); 210 | 211 | describe('key is undefined', () => { 212 | it('should be empty for valid input', done => { 213 | var schema = { 214 | username: Joi.string().required() 215 | }; 216 | var data = { 217 | username: 'bar' 218 | }; 219 | validator.validate(data, schema, undefined, (errors) => { 220 | var result = validator.getValidationMessages(errors); 221 | expect(result).to.be.empty; 222 | done(); 223 | }); 224 | }); 225 | 226 | it('should be filled for invalid input', done => { 227 | var label = '使用者名稱'; 228 | var schema = { 229 | username: Joi.string().required().label(label) 230 | }; 231 | var data = {}; 232 | validator.validate(data, schema, undefined, (errors) => { 233 | var result = validator.getValidationMessages(errors); 234 | expect(result.length).to.equal(1); 235 | expect(result).to.deep.equal(['"' + label + '" is required']); 236 | done(); 237 | }); 238 | }); 239 | }); 240 | }); 241 | 242 | describe('isValid()', () => { 243 | describe('key is defined', () => { 244 | it('should be true for valid input', done => { 245 | var schema = { 246 | username: Joi.string().required() 247 | }; 248 | var data = { 249 | username: 'bar' 250 | }; 251 | validator.validate(data, schema, undefined, (errors) => { 252 | var result = validator.isValid(errors, 'username'); 253 | expect(result).to.be.true; 254 | done(); 255 | }); 256 | }); 257 | 258 | it('should be false for invalid input', done => { 259 | var schema = { 260 | username: Joi.string().required() 261 | }; 262 | var data = {}; 263 | validator.validate(data, schema, undefined, (errors) => { 264 | var result = validator.isValid(errors, 'username'); 265 | expect(result).to.be.false; 266 | done(); 267 | }); 268 | }); 269 | }); 270 | 271 | describe('key is undefined', () => { 272 | it('should be true for valid input', done => { 273 | var schema = { 274 | username: Joi.string().required() 275 | }; 276 | var data = { 277 | username: 'bar' 278 | }; 279 | validator.validate(data, schema, {}, (errors) => { 280 | var result = validator.isValid(errors); 281 | expect(result).to.be.true; 282 | done(); 283 | }); 284 | }); 285 | 286 | it('should be false for invalid input', done => { 287 | var schema = { 288 | username: Joi.string().required() 289 | }; 290 | var data = {}; 291 | validator.validate(data, schema, {}, (errors) => { 292 | var result = validator.isValid(errors); 293 | expect(result).to.be.false; 294 | done(); 295 | }); 296 | }); 297 | }); 298 | }); 299 | }); 300 | --------------------------------------------------------------------------------