├── .babelrc ├── .bowerrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .stylelintignore ├── .stylelintrc ├── README.md ├── bower.json ├── dist └── react-validation-decorator.js ├── example ├── app │ ├── app.js │ ├── assets │ │ ├── images │ │ │ └── logo.svg │ │ └── styles │ │ │ ├── _common.scss │ │ │ ├── _mixin.scss │ │ │ ├── _page.scss │ │ │ ├── _region.scss │ │ │ ├── _variable.scss │ │ │ └── app.scss │ ├── components │ │ ├── App.js │ │ ├── Footer.js │ │ ├── Header.js │ │ ├── common │ │ │ └── Document.js │ │ └── pages │ │ │ ├── Example1 │ │ │ └── index.js │ │ │ ├── Example2 │ │ │ └── index.js │ │ │ └── Home │ │ │ ├── Component.js │ │ │ └── index.js │ ├── index.html │ └── routes │ │ ├── Example1 │ │ └── index.js │ │ ├── Example1Route.js │ │ ├── Example2 │ │ └── index.js │ │ └── Example2Route.js ├── webpack.config.js └── webpack.server.js ├── gulpfile.babel.js ├── lib ├── Validation.js └── index.js ├── package.json ├── src ├── Validation.js └── index.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "optional": [], 4 | "env": { 5 | "development": { 6 | "plugins": ["react-transform"], 7 | "extra": { 8 | "react-transform": { 9 | "transforms": [ 10 | { 11 | "transform": "react-transform-hmr", 12 | "imports": ["react"], 13 | "locals": ["module"] 14 | } 15 | ] 16 | } 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | bower_components/* 3 | dist/* 4 | example/dist/* 5 | lib/* 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "ecmaFeatures": { 8 | "arrowFunctions": true, 9 | "blockBindings": true, 10 | "classes": true, 11 | "defaultParams": true, 12 | "destructuring": true, 13 | "forOf": true, 14 | "modules": true, 15 | "objectLiteralComputedProperties": true, 16 | "objectLiteralShorthandMethods": true, 17 | "objectLiteralShorthandProperties": true, 18 | "spread": true, 19 | "superInFunctions": true, 20 | "templateStrings": true, 21 | "unicodeCodePointEscapes": true, 22 | "jsx": true 23 | }, 24 | "rules": { 25 | "strict": 0, 26 | "curly": 0, 27 | "quotes": [2, "single", "avoid-escape"], 28 | "semi": 2, 29 | "no-underscore-dangle": 0, 30 | "no-unused-vars": 2, 31 | "camelcase": [2, {"properties": "never"}], 32 | "new-cap": 0, 33 | "accessor-pairs": 0, 34 | "brace-style": [2, "1tbs"], 35 | "consistent-return": 2, 36 | "dot-location": [2, "property"], 37 | "dot-notation": 2, 38 | "eol-last": 2, 39 | "indent": [2, 2, {"SwitchCase": 1}], 40 | "no-bitwise": 0, 41 | "no-multi-spaces": 2, 42 | "no-shadow": 2, 43 | "no-unused-expressions": 2, 44 | "space-after-keywords": 2, 45 | "space-before-blocks": 2, 46 | "jsx-quotes": [1, "prefer-double"], 47 | "react/display-name": 0, 48 | "react/jsx-boolean-value": [2, "always"], 49 | "react/jsx-no-undef": 2, 50 | "react/jsx-sort-props": 0, 51 | "react/jsx-sort-prop-types": 0, 52 | "react/jsx-uses-react": 2, 53 | "react/jsx-uses-vars": 2, 54 | "react/no-did-mount-set-state": 2, 55 | "react/no-did-update-set-state": 2, 56 | "react/no-multi-comp": [2, {"ignoreStateless": true}], 57 | "react/no-unknown-property": 2, 58 | "react/prop-types": 1, 59 | "react/react-in-jsx-scope": 2, 60 | "react/self-closing-comp": 2, 61 | "react/sort-comp": 0, 62 | "react/wrap-multilines": [2, {"declaration": false, "assignment": false}] 63 | }, 64 | "globals": { 65 | "inject": false, 66 | "module": false, 67 | "describe": false, 68 | "it": false, 69 | "before": false, 70 | "beforeEach": false, 71 | "after": false, 72 | "afterEach": false, 73 | "expect": false, 74 | "window": false, 75 | "document": false 76 | }, 77 | "plugins": [ 78 | "react" 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | node_modules/* 3 | bower_components/* 4 | example/dist/* 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | example -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | bower_components/* 3 | dist/* 4 | example/dist/* 5 | lib/* 6 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": { 4 | "at-rule-empty-line-before": [ 5 | "always", { 6 | "except": ["blockless-group", "all-nested"], 7 | "ignore": ["after-comment"] 8 | } 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Validation Decorator 2 | 3 | Validation decorator for ReactJS base on [joi](https://github.com/hapijs/joi). 4 | 5 | [Demo](http://minhtranite.github.io/react-validation-decorator) 6 | 7 | ## Installation 8 | 9 | ### NPM 10 | 11 | ```bash 12 | npm install --save react-validation-decorator 13 | ``` 14 | 15 | ### Bower 16 | 17 | ```bash 18 | bower install --save react-validation-decorator 19 | ``` 20 | 21 | ## Usage 22 | 23 | ### With decorator: 24 | 25 | ```js 26 | import React from 'react'; 27 | import {Validation, Joi} from 'react-validation-decorator'; 28 | 29 | @Validation 30 | class Component extends React.Component { 31 | validationSchema = Joi.object().keys({ 32 | name: Joi.string().required().label('Name'), 33 | email: Joi.string().email().required().label('Email').label('Email'), 34 | password: Joi.string().min(3).max(30).label('Password'), 35 | verifyPassword: Joi.string().valid(Joi.ref('password')).options({ 36 | language: { 37 | any: { 38 | allowOnly: 'don\'t match' 39 | } 40 | } 41 | }).required().label('Verify Password') 42 | }); 43 | 44 | state = {}; 45 | 46 | handleNameChange = (e) => { 47 | this.setState({ 48 | name: e.target.value 49 | }, () => { 50 | this.validate('name'); 51 | }); 52 | }; 53 | 54 | handleEmailChange = (e) => { 55 | this.setState({ 56 | email: e.target.value 57 | }, () => { 58 | this.validate('email'); 59 | }); 60 | }; 61 | 62 | handlePasswordChange = (e) => { 63 | this.setState({ 64 | password: e.target.value 65 | }, () => { 66 | this.validate('password'); 67 | }); 68 | }; 69 | 70 | handleVerifyPasswordChange = (e) => { 71 | this.setState({ 72 | verifyPassword: e.target.value 73 | }, () => { 74 | this.validate('verifyPassword'); 75 | }); 76 | }; 77 | 78 | handleSubmit = (e) => { 79 | e.preventDefault(); 80 | }; 81 | 82 | render() { 83 | return ( 84 |
85 |
86 | 87 | 90 | {this.renderValidationMessages('name')} 91 |
92 |
93 | 94 | 97 | {this.renderValidationMessages('email')} 98 |
99 |
100 | 101 | 104 | {this.renderValidationMessages('password')} 105 |
106 |
107 | 108 | 111 | {this.renderValidationMessages('verifyPassword')} 112 |
113 | 116 |
117 |

State:

118 |
{JSON.stringify(this.state, undefined, 4)}
119 |
120 | ); 121 | } 122 | } 123 | 124 | export default Component; 125 | ``` 126 | 127 | ### Without decorator: 128 | 129 | ```js 130 | //... 131 | var Component = React.createClass({ 132 | //... 133 | validationSchema: Joi.object().keys({ 134 | name: Joi.string().required().label('Name'), 135 | email: Joi.string().email().required().label('Email').label('Email'), 136 | password: Joi.string().min(3).max(30).label('Password'), 137 | verifyPassword: Joi.string().valid(Joi.ref('password')).options({ 138 | language: { 139 | any: { 140 | allowOnly: 'don\'t match' 141 | } 142 | } 143 | }).required().label('Verify Password') 144 | }), 145 | getInitialState: function () { 146 | return {}; 147 | }, 148 | //... 149 | }); 150 | 151 | module.exports = Validation(Component); 152 | ``` 153 | 154 | ### UMD 155 | 156 | ```html 157 | 158 | ``` 159 | 160 | ```js 161 | //ES2015 162 | const {Validation, Joi} = window.ReactValidationDecorator; 163 | // Or 164 | var Validation = window.ReactValidationDecorator.Validation; 165 | var Joi = window.ReactValidationDecorator.Joi; 166 | ``` 167 | 168 | Example [here](http://codepen.io/vn38minhtran/pen/gPbJNx) 169 | 170 | ## API 171 | 172 | ### `validationSchema` 173 | 174 | - is [Joi](https://github.com/hapijs/joi) schema. 175 | - can be defined as `joi object` or `function`. 176 | 177 | ```js 178 | // Defined as joi object 179 | validationSchema = Joi.object().keys({ 180 | name: Joi.string().required().label('Name'), 181 | email: Joi.string().email().required().label('Email').label('Email'), 182 | password: Joi.string().min(3).max(30).label('Password'), 183 | verifyPassword: Joi.string().valid(Joi.ref('password')).options({ 184 | language: { 185 | any: { 186 | allowOnly: 'don\'t match' 187 | } 188 | } 189 | }).required().label('Verify Password') 190 | }); 191 | 192 | // Defined as function 193 | validationSchema = () => { 194 | return Joi.object().keys({ 195 | name: Joi.string().required().label('Name'), 196 | email: Joi.string().email().required().label('Email').label('Email'), 197 | password: Joi.string().min(3).max(30).label('Password'), 198 | verifyPassword: Joi.string().valid(Joi.ref('password')).options({ 199 | language: { 200 | any: { 201 | allowOnly: 'don\'t match' 202 | } 203 | } 204 | }).required().label('Verify Password') 205 | }); 206 | }; 207 | ``` 208 | 209 | ### `validationValue` 210 | - is validation value. 211 | - it is optional, default `validationValue` use `state` as value. 212 | 213 | ```js 214 | // Defined as object 215 | validationValue = () => { 216 | return Merge(this.state, this.props); // Sample 217 | }; 218 | ``` 219 | 220 | ### `validationOptions` 221 | - is [Joi](https://github.com/hapijs/joi#validatevalue-schema-options-callback) options. 222 | - can be defined as `object` or `function`. 223 | 224 | ```js 225 | // Defined as object 226 | validationOptions = { 227 | convert: false 228 | }; 229 | 230 | // Defined as function 231 | validationOptions = () => { 232 | return { 233 | convert: false 234 | }; 235 | } 236 | ``` 237 | 238 | ### `validate(path, [callback])` 239 | - Validates `validationValue` using the given `validationSchema`. 240 | - After it called `isDirty(path)` will return `true`. 241 | 242 | ```js 243 | handleNameChange = (e) => { 244 | this.setState({ 245 | name: e.target.value 246 | }, () => { 247 | this.validate('name'); 248 | }); 249 | }; 250 | ``` 251 | 252 | ### `handleValidation(path)` 253 | 254 | ### `isValid([path])` 255 | 256 | ```js 257 | this.isValid('name'); 258 | // return true if field name valid other while return false. 259 | 260 | this.isValid(); 261 | // return true if all fields in schema valid other while return false. 262 | ``` 263 | 264 | ### `isDirty([path])` 265 | 266 | ```js 267 | this.isDirty('name'); 268 | // return true if field name dirty other while return false. 269 | 270 | this.isDirty(); 271 | // return true if any field in schema valid other while return false. 272 | ``` 273 | 274 | ### `getValidationMessages([path])` 275 | 276 | If `path` is defined return error details of `path` other while return all error details. 277 | 278 | ### `getValidationValue()` 279 | 280 | Return validated value. 281 | 282 | ### `resetValidation([callback])` 283 | 284 | Reset validation. 285 | 286 | ### `getValidationClassName(path, [successClass, errorClass, defaultClass])` 287 | 288 | ```js 289 | this.getValidationClassName('name') 290 | // default return: `form-group` 291 | // when name dirty and valid return : `form-group has-success`. 292 | // when name dirty and invalid return: `form-group has-error` 293 | 294 | this.getValidationClassName('name', 'valid', 'invalid', 'field') 295 | // default return: `field` 296 | // when name dirty and valid return : `field valid`. 297 | // when name dirty and invalid return: `field invalid` 298 | ``` 299 | 300 | ### `renderValidationMessages(path, [className='help-block', onlyFirst=true])` 301 | 302 | Render validation messages, if `onlyFirst == false` it will render all messages of `path`. 303 | 304 | ### `updateState(newState, [callback])` 305 | 306 | ```js 307 | //... 308 | state = { 309 | user: { 310 | name: 'John', 311 | age: 30 312 | } 313 | }; 314 | //... 315 | 316 | this.updateState({ 317 | 'user.name': 'John smith' 318 | }) 319 | ``` 320 | 321 | See [object-path](https://github.com/mariocasciaro/object-path). 322 | 323 | ## Troubleshooting 324 | 325 | #### Cannot resolve module 'net' or 'dns': 326 | 327 | ```js 328 | // webpack.config.js 329 | //... 330 | module.exports = { 331 | //... 332 | node: { 333 | net: 'mock', 334 | dns: 'mock' 335 | } 336 | //... 337 | }; 338 | //... 339 | ``` 340 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-validation-decorator", 3 | "version": "0.4.0", 4 | "description": "Validation decorator for ReactJS base on joi.", 5 | "main": "lib/index.js", 6 | "keywords": [ 7 | "react-component", 8 | "react", 9 | "component", 10 | "validation", 11 | "form validate" 12 | ], 13 | "license": "MIT", 14 | "ignore": [ 15 | "**/.*", 16 | "node_modules", 17 | "bower_components", 18 | "test", 19 | "tests" 20 | ], 21 | "dependencies": {}, 22 | "devDependencies": {} 23 | } 24 | -------------------------------------------------------------------------------- /example/app/app.js: -------------------------------------------------------------------------------- 1 | import 'babel-core/polyfill'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import {createHistory} from 'history'; 6 | import {Router, useRouterHistory} from 'react-router'; 7 | import App from 'components/App.js'; 8 | import {name} from '../../package.json'; 9 | 10 | import 'bootstrap/dist/css/bootstrap.css'; 11 | import 'assets/styles/app.scss'; 12 | 13 | const routes = { 14 | path: '/', 15 | component: App, 16 | indexRoute: { 17 | component: require('components/pages/Home') 18 | }, 19 | childRoutes: [ 20 | require('routes/Example1'), 21 | require('routes/Example2') 22 | ] 23 | }; 24 | 25 | const DEV = process && process.env && process.env.NODE_ENV === 'development'; 26 | const history = useRouterHistory(createHistory)({ 27 | basename: '/' + (DEV ? '' : name) 28 | }); 29 | 30 | const run = () => { 31 | ReactDOM.render( 32 | , 33 | document.getElementById('app') 34 | ); 35 | }; 36 | 37 | if (window.addEventListener) { 38 | window.addEventListener('DOMContentLoaded', run); 39 | } else { 40 | window.attachEvent('onload', run); 41 | } 42 | -------------------------------------------------------------------------------- /example/app/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 11 | 16 | 21 | 22 | -------------------------------------------------------------------------------- /example/app/assets/styles/_common.scss: -------------------------------------------------------------------------------- 1 | /* Pre Render 2 | ========================================================================== */ 3 | @keyframes spinner { 4 | 0% { 5 | transform: rotate(0deg); 6 | } 7 | 100% { 8 | transform: rotate(360deg); 9 | } 10 | } 11 | 12 | .pre-render { 13 | background: rgba(255, 255, 255, 0.7); 14 | position: fixed; 15 | top: 0; 16 | left: 0; 17 | width: 100%; 18 | height: 100%; 19 | z-index: 99999; 20 | .spinner { 21 | width: 48px; 22 | height: 48px; 23 | border: 1px solid lighten($primary, 40%); 24 | border-left-color: darken($primary, 10%); 25 | border-radius: 50%; 26 | animation: spinner 700ms infinite linear; 27 | position: absolute; 28 | top: 50%; 29 | left: 50%; 30 | margin-left: -24px; 31 | margin-top: -24px; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example/app/assets/styles/_mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix() { 2 | &::before, 3 | &::after { 4 | content: " "; 5 | display: table; 6 | } 7 | &::after { 8 | clear: both; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/app/assets/styles/_page.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minhtranite/react-validation-decorator/5c26006a60e2f233a2719bcd29a48155cf4bd01b/example/app/assets/styles/_page.scss -------------------------------------------------------------------------------- /example/app/assets/styles/_region.scss: -------------------------------------------------------------------------------- 1 | /* Header 2 | ========================================================================== */ 3 | .navbar { 4 | border-radius: 0; 5 | } 6 | 7 | /* Footer 8 | ========================================================================== */ 9 | html, 10 | body, 11 | #app { 12 | height: 100%; 13 | } 14 | 15 | .layout-page { 16 | position: relative; 17 | min-height: 100%; 18 | padding-bottom: 60px; 19 | } 20 | 21 | .layout-main { 22 | margin-bottom: 30px; 23 | } 24 | 25 | .layout-footer { 26 | position: absolute; 27 | bottom: 0; 28 | width: 100%; 29 | height: 60px; 30 | background-color: #f8f8f8; 31 | padding: 20px 0; 32 | } 33 | -------------------------------------------------------------------------------- /example/app/assets/styles/_variable.scss: -------------------------------------------------------------------------------- 1 | $black: #000; 2 | $white: #fff; 3 | $primary: $black; 4 | -------------------------------------------------------------------------------- /example/app/assets/styles/app.scss: -------------------------------------------------------------------------------- 1 | /* ========================================================================== 2 | React component starter 3 | ========================================================================== */ 4 | @import "variable"; 5 | @import "mixin"; 6 | @import "common"; 7 | @import "region"; 8 | @import "page"; 9 | -------------------------------------------------------------------------------- /example/app/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from './Header'; 3 | import Footer from './Footer'; 4 | 5 | class App extends React.Component { 6 | static propTypes = { 7 | children: React.PropTypes.node 8 | }; 9 | 10 | render() { 11 | return ( 12 |
13 |
14 |
15 |
16 | {this.props.children} 17 |
18 |
19 |
20 |
21 | ); 22 | } 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /example/app/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Footer extends React.Component { 4 | render() { 5 | return ( 6 | 11 | ); 12 | } 13 | } 14 | 15 | export default Footer; 16 | 17 | -------------------------------------------------------------------------------- /example/app/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | 4 | import logo from 'assets/images/logo.svg'; 5 | 6 | class Header extends React.Component { 7 | render() { 8 | return ( 9 |
10 | 25 |
26 | ); 27 | } 28 | } 29 | 30 | export default Header; 31 | 32 | -------------------------------------------------------------------------------- /example/app/components/common/Document.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Document extends React.Component { 4 | static propTypes = { 5 | title: React.PropTypes.string, 6 | className: React.PropTypes.string, 7 | children: React.PropTypes.any.isRequired 8 | }; 9 | 10 | state = { 11 | oldTitle: document.title, 12 | oldClassName: document.body.className 13 | }; 14 | 15 | componentWillMount = () => { 16 | if (this.props.title) { 17 | document.title = this.props.title; 18 | } 19 | if (this.props.className) { 20 | let className = this.state.oldClassName + ' ' + this.props.className; 21 | document.body.className = className.trim().replace(' ', ' '); 22 | } 23 | }; 24 | 25 | componentWillUnmount = () => { 26 | document.title = this.state.oldTitle; 27 | document.body.className = this.state.oldClassName; 28 | }; 29 | 30 | render() { 31 | if (this.props.children) { 32 | return React.Children.only(this.props.children); 33 | } 34 | return null; 35 | } 36 | } 37 | 38 | export default Document; 39 | -------------------------------------------------------------------------------- /example/app/components/pages/Example1/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Document from 'components/common/Document'; 3 | 4 | class Example1Page extends React.Component { 5 | render() { 6 | return ( 7 | 9 |

Example 1

10 |
11 | ); 12 | } 13 | } 14 | 15 | export default Example1Page; 16 | 17 | -------------------------------------------------------------------------------- /example/app/components/pages/Example2/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Document from 'components/common/Document'; 3 | 4 | class Example2Page extends React.Component { 5 | render() { 6 | return ( 7 | 9 |

Example 2

10 |
11 | ); 12 | } 13 | } 14 | 15 | export default Example2Page; 16 | 17 | -------------------------------------------------------------------------------- /example/app/components/pages/Home/Component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Validation, Joi} from 'react-validation-decorator'; 3 | 4 | @Validation 5 | class Component extends React.Component { 6 | static propTypes = { 7 | name: React.PropTypes.string 8 | }; 9 | 10 | validationSchema = Joi.object().keys({ 11 | name: Joi.string().required().label('Name'), 12 | email: Joi.string().email().required().label('Email').label('Email'), 13 | age: Joi.number().min(18).required().label('Age'), 14 | password: Joi.string().min(3).max(30).label('Password'), 15 | verifyPassword: Joi.string().valid(Joi.ref('password')).options({ 16 | language: { 17 | any: { 18 | allowOnly: 'don\'t match' 19 | } 20 | } 21 | }).required().label('Verify Password') 22 | }); 23 | 24 | state = {}; 25 | 26 | handleNameChange = (e) => { 27 | this.setState({ 28 | name: e.target.value 29 | }, () => { 30 | this.validate('name'); 31 | }); 32 | }; 33 | 34 | handleEmailChange = (e) => { 35 | this.setState({ 36 | email: e.target.value 37 | }, () => { 38 | this.validate('email'); 39 | }); 40 | }; 41 | 42 | handleAgeChange = (e) => { 43 | this.setState({ 44 | age: e.target.value 45 | }, () => { 46 | this.validate('age'); 47 | }); 48 | }; 49 | 50 | handlePasswordChange = (e) => { 51 | this.setState({ 52 | password: e.target.value 53 | }, () => { 54 | this.validate('password'); 55 | }); 56 | }; 57 | 58 | handleVerifyPasswordChange = (e) => { 59 | this.setState({ 60 | verifyPassword: e.target.value 61 | }, () => { 62 | this.validate('verifyPassword'); 63 | }); 64 | }; 65 | 66 | handleSubmit = (e) => { 67 | e.preventDefault(); 68 | }; 69 | 70 | render() { 71 | return ( 72 |
73 |
74 | 75 | 78 | {this.renderValidationMessages('name')} 79 |
80 |
81 | 82 | 85 | {this.renderValidationMessages('email')} 86 |
87 |
88 | 89 | 92 | {this.renderValidationMessages('age')} 93 |
94 |
95 | 96 | 99 | {this.renderValidationMessages('password')} 100 |
101 |
102 | 103 | 106 | {this.renderValidationMessages('verifyPassword')} 107 |
108 | 111 |
112 |

State:

113 |
{JSON.stringify(this.state, null, 4)}
114 |
115 | ); 116 | } 117 | } 118 | 119 | export default Component; 120 | 121 | -------------------------------------------------------------------------------- /example/app/components/pages/Home/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Document from 'components/common/Document'; 3 | import Component from './Component'; 4 | 5 | class HomePage extends React.Component { 6 | render() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | } 14 | 15 | export default HomePage; 16 | -------------------------------------------------------------------------------- /example/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React validation example 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /example/app/routes/Example1/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | path: 'ex-1', 3 | getComponent(location, callback) { 4 | require.ensure([], require => { 5 | callback(null, require('components/pages/Example1')); 6 | }, 'page-ex-1'); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /example/app/routes/Example1Route.js: -------------------------------------------------------------------------------- 1 | export default { 2 | path: 'ex-1', 3 | getComponent(location, callback) { 4 | require.ensure([], require => { 5 | callback(null, require('../components/pages/PageExample1')); 6 | }, 'page-ex-1'); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /example/app/routes/Example2/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | path: 'ex-2', 3 | getComponent(location, callback) { 4 | require.ensure([], require => { 5 | callback(null, require('components/pages/Example2')); 6 | }, 'page-ex-2'); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /example/app/routes/Example2Route.js: -------------------------------------------------------------------------------- 1 | export default { 2 | path: 'ex-2', 3 | getComponent(location, callback) { 4 | require.ensure([], require => { 5 | callback(null, require('../components/pages/PageExample2')); 6 | }, 'page-ex-2'); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import path from 'path'; 3 | import autoprefixer from 'autoprefixer'; 4 | import cssnano from 'cssnano'; 5 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 6 | import StylelintWebpackPlugin from 'stylelint-webpack-plugin'; 7 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 8 | import pkg from '../package.json'; 9 | 10 | const ENV = process.env.NODE_ENV || 'development'; 11 | const DEV = ENV === 'development'; 12 | const PROD = ENV === 'production'; 13 | 14 | const webpackConfig = { 15 | entry: { 16 | app: PROD 17 | ? path.join(__dirname, 'app/app.js') 18 | : ['webpack-hot-middleware/client?reload=true&quiet=true', path.join(__dirname, 'app/app.js')] 19 | }, 20 | output: { 21 | path: path.join(__dirname, 'dist'), 22 | filename: PROD ? '[hash].js' : '[name].js', 23 | chunkFilename: PROD ? '[chunkhash].js' : '[name].chunk.js', 24 | hashDigestLength: 32, 25 | publicPath: PROD ? `/${pkg.name}/` : '/' 26 | }, 27 | resolve: { 28 | root: path.join(__dirname, 'app'), 29 | modulesDirectories: ['node_modules', 'bower_components'], 30 | extensions: ['', '.jsx', '.js'], 31 | alias: { 32 | [`${pkg.name}$`]: path.join(__dirname, '../src/index.js'), 33 | [`${pkg.name}/src`]: path.join(__dirname, '../src') 34 | } 35 | }, 36 | module: { 37 | preLoaders: [ 38 | { 39 | test: /\.(js|jsx)$/, 40 | exclude: /(node_modules|bower_components)/, 41 | loader: 'eslint-loader' 42 | } 43 | ], 44 | loaders: [ 45 | { 46 | test: /\.(js|jsx)$/, 47 | exclude: /(node_modules|bower_components)/, 48 | loader: 'babel-loader' 49 | }, 50 | { 51 | test: /\.json$/, 52 | loader: 'json-loader' 53 | }, 54 | { 55 | test: /\.css$/, 56 | loader: PROD 57 | ? ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader') 58 | : 'style-loader!css-loader!postcss-loader' 59 | }, 60 | { 61 | test: /\.scss$/, 62 | loader: PROD 63 | ? ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader!sass-loader') 64 | : 'style-loader!css-loader!postcss-loader!sass-loader' 65 | }, 66 | { 67 | test: /\.(png|jpg|gif|swf)$/, 68 | loader: PROD 69 | ? 'file-loader?name=[hash].[ext]' 70 | : 'file-loader?name=[name].[ext]' 71 | }, 72 | { 73 | test: /\.(ttf|eot|svg|woff(2)?)(\S+)?$/, 74 | loader: PROD 75 | ? 'file-loader?name=[hash].[ext]' 76 | : 'file-loader?name=[name].[ext]' 77 | }, 78 | { 79 | test: /\.html$/, 80 | loader: 'html-loader?interpolate' 81 | } 82 | ] 83 | }, 84 | plugins: [ 85 | new webpack.DefinePlugin({ 86 | 'process.env': { 87 | NODE_ENV: JSON.stringify(ENV) 88 | } 89 | }), 90 | new StylelintWebpackPlugin({ 91 | files: '**/*.?(s)@(a|c)ss', 92 | configFile: path.join(__dirname, '../.stylelintrc'), 93 | failOnError: PROD 94 | }), 95 | new HtmlWebpackPlugin({ 96 | template: path.join(__dirname, 'app/index.html') 97 | }) 98 | ], 99 | eslint: { 100 | configFile: path.join(__dirname, '../.eslintrc'), 101 | failOnError: PROD, 102 | emitError: PROD 103 | }, 104 | postcss: () => { 105 | let processors = [ 106 | autoprefixer({ 107 | browsers: [ 108 | 'ie >= 10', 109 | 'ie_mob >= 10', 110 | 'ff >= 30', 111 | 'chrome >= 34', 112 | 'safari >= 7', 113 | 'opera >= 23', 114 | 'ios >= 7', 115 | 'android >= 4.4', 116 | 'bb >= 10' 117 | ] 118 | }) 119 | ]; 120 | if (PROD) { 121 | processors.push(cssnano({ 122 | safe: true, 123 | discardComments: { 124 | removeAll: true 125 | } 126 | })); 127 | } 128 | return processors; 129 | }, 130 | sassLoader: { 131 | includePaths: [ 132 | path.join(__dirname, '../bower_components'), 133 | path.join(__dirname, '../node_modules') 134 | ], 135 | outputStyle: PROD ? 'compressed' : 'expanded' 136 | }, 137 | node: { 138 | net: 'mock', 139 | dns: 'mock' 140 | }, 141 | debug: DEV, 142 | devtool: DEV ? '#eval' : false, 143 | stats: { 144 | children: false 145 | }, 146 | progress: PROD, 147 | profile: PROD, 148 | bail: PROD 149 | }; 150 | 151 | if (DEV) { 152 | webpackConfig.plugins = webpackConfig.plugins.concat([ 153 | new webpack.HotModuleReplacementPlugin(), 154 | new webpack.NoErrorsPlugin() 155 | ]); 156 | } 157 | 158 | if (PROD) { 159 | webpackConfig.plugins = webpackConfig.plugins.concat([ 160 | new ExtractTextPlugin('[contenthash].css'), 161 | new webpack.optimize.UglifyJsPlugin({ 162 | sourceMap: false, 163 | compress: { 164 | warnings: false 165 | }, 166 | output: { 167 | comments: false 168 | } 169 | }), 170 | new webpack.optimize.DedupePlugin() 171 | ]); 172 | } 173 | 174 | export default webpackConfig; 175 | -------------------------------------------------------------------------------- /example/webpack.server.js: -------------------------------------------------------------------------------- 1 | import url from 'url'; 2 | import express from 'express'; 3 | import webpack from 'webpack'; 4 | import webpackConfig from './webpack.config'; 5 | import webpackDevMiddleware from 'webpack-dev-middleware'; 6 | import webpackHotMiddleware from 'webpack-hot-middleware'; 7 | import fs from 'fs'; 8 | import path from 'path'; 9 | import http from 'http'; 10 | import https from 'https'; 11 | import opn from 'opn'; 12 | import httpProxy from 'http-proxy'; 13 | 14 | const devURL = 'http://localhost:3000'; 15 | const urlParts = url.parse(devURL); 16 | const proxyOptions = []; 17 | 18 | const proxy = httpProxy.createProxyServer({ 19 | changeOrigin: true, 20 | ws: true 21 | }); 22 | 23 | const compiler = webpack(webpackConfig); 24 | 25 | const app = express(); 26 | 27 | app.use(webpackDevMiddleware(compiler, { 28 | noInfo: true, 29 | publicPath: webpackConfig.output.publicPath, 30 | stats: { 31 | colors: true, 32 | hash: false, 33 | timings: false, 34 | chunks: false, 35 | chunkModules: false, 36 | modules: false, 37 | children: false, 38 | version: false, 39 | cached: false, 40 | cachedAssets: false, 41 | reasons: false, 42 | source: false, 43 | errorDetails: false 44 | } 45 | })); 46 | 47 | app.use(webpackHotMiddleware(compiler)); 48 | 49 | app.use('/assets', express.static(path.join(__dirname, 'app/assets'))); 50 | 51 | proxyOptions.forEach(option => { 52 | app.all(option.path, (req, res) => { 53 | proxy.web(req, res, option, err => { 54 | console.log(err.message); 55 | res.statusCode = 502; 56 | res.end(); 57 | }); 58 | }); 59 | }); 60 | 61 | app.get('*', (req, res, next) => { 62 | let filename = path.join(compiler.outputPath, 'index.html'); 63 | compiler.outputFileSystem.readFile(filename, (error, result) => { 64 | if (error) { 65 | return next(error); 66 | } 67 | res.set('content-type', 'text/html'); 68 | res.send(result); 69 | res.end(); 70 | }); 71 | }); 72 | 73 | let server = http.createServer(app); 74 | if (urlParts.protocol === 'https:') { 75 | server = https.createServer({ 76 | key: fs.readFileSync(path.join(__dirname, 'key.pem')), 77 | cert: fs.readFileSync(path.join(__dirname, 'cert.pem')) 78 | }, app); 79 | } 80 | 81 | server.listen(urlParts.port, () => { 82 | console.log('Listening at ' + devURL); 83 | opn(devURL); 84 | }); 85 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import {spawnSync} from 'child_process'; 3 | import del from 'del'; 4 | import stylelint from 'gulp-stylelint'; 5 | import eslint from 'gulp-eslint'; 6 | import babel from 'gulp-babel'; 7 | import webpackStream from 'webpack-stream'; 8 | import webpackConfig from './webpack.config'; 9 | import exampleWebpackConfig from './example/webpack.config'; 10 | import webpack from 'webpack'; 11 | import sass from 'gulp-sass'; 12 | import filter from 'gulp-filter'; 13 | import postcss from 'gulp-postcss'; 14 | import autoprefixer from 'autoprefixer'; 15 | import cssnano from 'cssnano'; 16 | import concat from 'gulp-concat'; 17 | import pkg from './package.json'; 18 | import runSequence from 'run-sequence'; 19 | 20 | gulp.task('start', (callback) => { 21 | let start = spawnSync('babel-node', ['example/webpack.server.js'], {stdio: 'inherit'}); 22 | if (start.stderr) { 23 | callback(start.stderr); 24 | } 25 | }); 26 | 27 | gulp.task('build:lib:clean', () => { 28 | del.sync(['lib', 'dist']); 29 | }); 30 | 31 | gulp.task('build:lib:stylelint', () => { 32 | return gulp 33 | .src(['src/**/*.{css,scss,sass}']) 34 | .pipe(stylelint({ 35 | failAfterError: true, 36 | reporters: [ 37 | {formatter: 'string', console: true} 38 | ] 39 | })); 40 | }); 41 | 42 | gulp.task('build:lib:eslint', () => { 43 | return gulp 44 | .src(['src/**/*.js']) 45 | .pipe(eslint()) 46 | .pipe(eslint.format()) 47 | .pipe(eslint.failOnError()); 48 | }); 49 | 50 | gulp.task('build:lib:babel', () => { 51 | return gulp 52 | .src(['src/**/*.js']) 53 | .pipe(babel()) 54 | .pipe(gulp.dest('lib')); 55 | }); 56 | 57 | gulp.task('build:lib:umd', () => { 58 | return gulp 59 | .src(['src/index.js']) 60 | .pipe(webpackStream(webpackConfig, webpack)) 61 | .pipe(gulp.dest('dist')); 62 | }); 63 | 64 | gulp.task('build:lib:sass', () => { 65 | let cssFilter = filter('**/*.css'); 66 | return gulp 67 | .src(['src/**/*.scss', '!src/**/_*.scss']) 68 | .pipe(sass({outputStyle: 'expanded'}).on('error', sass.logError)) 69 | .pipe(cssFilter) 70 | .pipe(gulp.dest('lib')) 71 | .pipe(concat(pkg.name + '.css')) 72 | .pipe(postcss([ 73 | autoprefixer({ 74 | browsers: [ 75 | 'ie >= 10', 76 | 'ie_mob >= 10', 77 | 'ff >= 30', 78 | 'chrome >= 34', 79 | 'safari >= 7', 80 | 'opera >= 23', 81 | 'ios >= 7', 82 | 'android >= 4.4', 83 | 'bb >= 10' 84 | ] 85 | }), 86 | cssnano({ 87 | safe: true, 88 | discardComments: {removeAll: true} 89 | }) 90 | ])) 91 | .pipe(gulp.dest('dist')); 92 | }); 93 | 94 | gulp.task('build:lib:copy', () => { 95 | return gulp 96 | .src(['src/**/*', '!src/**/*.{scss,js}']) 97 | .pipe(gulp.dest('lib')) 98 | .pipe(gulp.dest('dist')); 99 | }); 100 | 101 | gulp.task('build:lib', (callback) => { 102 | runSequence( 103 | 'build:lib:clean', 104 | 'build:lib:stylelint', 105 | 'build:lib:eslint', 106 | 'build:lib:babel', 107 | 'build:lib:umd', 108 | 'build:lib:sass', 109 | 'build:lib:copy', 110 | callback 111 | ); 112 | }); 113 | 114 | gulp.task('build:example:clean', () => { 115 | del.sync(['example/dist']); 116 | }); 117 | 118 | gulp.task('build:example:webpack', () => { 119 | return gulp 120 | .src(['example/app/app.js']) 121 | .pipe(webpackStream(exampleWebpackConfig, webpack)) 122 | .pipe(gulp.dest('example/dist')); 123 | }); 124 | 125 | gulp.task('build:example:copy', () => { 126 | return gulp 127 | .src(['example/app/*', '!example/app/*.{html,js}'], {nodir: true}) 128 | .pipe(gulp.dest('example/dist')); 129 | }); 130 | 131 | gulp.task('build:example', (callback) => { 132 | runSequence( 133 | 'build:example:clean', 134 | 'build:example:webpack', 135 | 'build:example:copy', 136 | callback 137 | ); 138 | }); 139 | 140 | gulp.task('build', (callback) => { 141 | runSequence('build:lib', 'build:example', callback); 142 | }); 143 | 144 | gulp.task('default', ['build']); 145 | -------------------------------------------------------------------------------- /lib/Validation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true 5 | }); 6 | 7 | var _get = function get(_x7, _x8, _x9) { var _again = true; _function: while (_again) { var object = _x7, property = _x8, receiver = _x9; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x7 = parent; _x8 = property; _x9 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; 8 | 9 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 10 | 11 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 12 | 13 | function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 14 | 15 | var _react = require('react'); 16 | 17 | var _react2 = _interopRequireDefault(_react); 18 | 19 | var _joi = require('joi'); 20 | 21 | var _joi2 = _interopRequireDefault(_joi); 22 | 23 | var _lodashFilter = require('lodash.filter'); 24 | 25 | var _lodashFilter2 = _interopRequireDefault(_lodashFilter); 26 | 27 | var _lodashResult = require('lodash.result'); 28 | 29 | var _lodashResult2 = _interopRequireDefault(_lodashResult); 30 | 31 | var _objectPath = require('object-path'); 32 | 33 | var _objectPath2 = _interopRequireDefault(_objectPath); 34 | 35 | var _lodashMerge = require('lodash.merge'); 36 | 37 | var _lodashMerge2 = _interopRequireDefault(_lodashMerge); 38 | 39 | var _lodashClonedeep = require('lodash.clonedeep'); 40 | 41 | var _lodashClonedeep2 = _interopRequireDefault(_lodashClonedeep); 42 | 43 | var _lodashStartswith = require('lodash.startswith'); 44 | 45 | var _lodashStartswith2 = _interopRequireDefault(_lodashStartswith); 46 | 47 | var Validation = function Validation(ComposedComponent) { 48 | return (function (_ComposedComponent) { 49 | _inherits(ValidationComponent, _ComposedComponent); 50 | 51 | function ValidationComponent(props, context) { 52 | var _this = this; 53 | 54 | _classCallCheck(this, ValidationComponent); 55 | 56 | _get(Object.getPrototypeOf(ValidationComponent.prototype), 'constructor', this).call(this, props, context); 57 | 58 | this.validate = function (path, callback) { 59 | var validationValue = (0, _lodashClonedeep2['default'])((0, _lodashResult2['default'])(_this, 'validationValue', _this.state)); 60 | if (typeof validationValue === 'object' && validationValue.hasOwnProperty('validation')) { 61 | delete validationValue.validation; 62 | } 63 | var validationSchema = (0, _lodashResult2['default'])(_this, 'validationSchema'); 64 | var validationOptions = (0, _lodashMerge2['default'])({ 65 | abortEarly: false, 66 | allowUnknown: true 67 | }, (0, _lodashResult2['default'])(_this, 'validationOptions', {})); 68 | _joi2['default'].validate(validationValue, validationSchema, validationOptions, function (error, value) { 69 | var validation = _objectPath2['default'].get(_this.state, 'validation', {}); 70 | validation.errors = error && error.details ? error.details : []; 71 | validation.value = value; 72 | var pushDirty = function pushDirty(p) { 73 | var dirtyArr = arguments.length <= 1 || arguments[1] === undefined ? [] : arguments[1]; 74 | 75 | if (p && dirtyArr.indexOf(p) === -1) { 76 | dirtyArr.push(p); 77 | } 78 | var pArr = p.split('.'); 79 | if (pArr.length > 1) { 80 | pArr.splice(-1, 1); 81 | pushDirty(pArr.join('.'), dirtyArr); 82 | } 83 | }; 84 | pushDirty(path, validation.dirty); 85 | if (callback) { 86 | _this.setState({ 87 | validation: validation 88 | }, callback); 89 | } else { 90 | _this.setState({ 91 | validation: validation 92 | }); 93 | } 94 | }); 95 | }; 96 | 97 | this.handleValidation = function (path) { 98 | return function (e) { 99 | e.preventDefault(); 100 | _this.validate(path); 101 | }; 102 | }; 103 | 104 | this.isValid = function (path) { 105 | var errors = _objectPath2['default'].get(_this.state, 'validation.errors', []); 106 | if (path) { 107 | errors = (0, _lodashFilter2['default'])(errors, function (error) { 108 | return error.path === path || (0, _lodashStartswith2['default'])(error.path, path + '.'); 109 | }); 110 | } 111 | return errors.length === 0; 112 | }; 113 | 114 | this.isDirty = function (path) { 115 | var dirty = _objectPath2['default'].get(_this.state, 'validation.dirty', []); 116 | if (path) { 117 | dirty = (0, _lodashFilter2['default'])(dirty, function (d) { 118 | return d === path; 119 | }); 120 | } 121 | return dirty.length !== 0; 122 | }; 123 | 124 | this.getValidationMessages = function (path) { 125 | var errors = _objectPath2['default'].get(_this.state, 'validation.errors', []); 126 | if (path) { 127 | errors = (0, _lodashFilter2['default'])(errors, function (error) { 128 | return error.path === path || (0, _lodashStartswith2['default'])(error.path, path + '.'); 129 | }); 130 | } 131 | return errors; 132 | }; 133 | 134 | this.getValidationValue = function () { 135 | return (0, _lodashClonedeep2['default'])(_objectPath2['default'].get(_this.state, 'validation.value')); 136 | }; 137 | 138 | this.resetValidation = function (callback) { 139 | if (callback) { 140 | _this.setState({ 141 | validation: { 142 | dirty: [], 143 | errors: [], 144 | value: null 145 | } 146 | }, callback); 147 | } else { 148 | _this.setState({ 149 | validation: { 150 | dirty: [], 151 | errors: [], 152 | value: null 153 | } 154 | }); 155 | } 156 | }; 157 | 158 | this.getValidationClassName = function (path) { 159 | var successClass = arguments.length <= 1 || arguments[1] === undefined ? 'has-success' : arguments[1]; 160 | var errorClass = arguments.length <= 2 || arguments[2] === undefined ? 'has-error' : arguments[2]; 161 | var defaultClass = arguments.length <= 3 || arguments[3] === undefined ? 'form-group' : arguments[3]; 162 | 163 | var className = [defaultClass]; 164 | if (_this.isValid(path) && _this.isDirty(path)) { 165 | className.push(successClass); 166 | } 167 | if (!_this.isValid(path) && _this.isDirty(path)) { 168 | className.push(errorClass); 169 | } 170 | return className.join(' '); 171 | }; 172 | 173 | this.renderValidationMessages = function (path) { 174 | var className = arguments.length <= 1 || arguments[1] === undefined ? 'help-block' : arguments[1]; 175 | var onlyFirst = arguments.length <= 2 || arguments[2] === undefined ? true : arguments[2]; 176 | 177 | var errors = _this.getValidationMessages(path); 178 | if (errors.length !== 0 && _this.isDirty(path)) { 179 | errors = onlyFirst ? [errors[0]] : errors; 180 | var html = errors.map(function (error, index) { 181 | return _react2['default'].createElement( 182 | 'div', 183 | { key: error.path + index }, 184 | error.message 185 | ); 186 | }); 187 | return _react2['default'].createElement( 188 | 'div', 189 | { className: className }, 190 | html 191 | ); 192 | } 193 | return null; 194 | }; 195 | 196 | this.updateState = function (newState, callback) { 197 | var state = _this.state; 198 | var stateModel = (0, _objectPath2['default'])(state); 199 | for (var property in newState) { 200 | if (newState.hasOwnProperty(property)) { 201 | stateModel.set(property, newState[property]); 202 | } 203 | } 204 | if (callback) { 205 | _this.setState(state, callback); 206 | } else { 207 | _this.setState(state); 208 | } 209 | }; 210 | 211 | this.state.validation = { 212 | dirty: [], 213 | errors: [], 214 | value: null 215 | }; 216 | } 217 | 218 | return ValidationComponent; 219 | })(ComposedComponent); 220 | }; 221 | 222 | exports['default'] = Validation; 223 | module.exports = exports['default']; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true 5 | }); 6 | 7 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 8 | 9 | var _ValidationJs = require('./Validation.js'); 10 | 11 | var _ValidationJs2 = _interopRequireDefault(_ValidationJs); 12 | 13 | var _joi = require('joi'); 14 | 15 | var _joi2 = _interopRequireDefault(_joi); 16 | 17 | exports.Validation = _ValidationJs2['default']; 18 | exports.Joi = _joi2['default']; 19 | exports['default'] = _ValidationJs2['default']; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-validation-decorator", 3 | "version": "0.4.0", 4 | "description": "Validation decorator for ReactJS base on joi.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "start": "NODE_ENV=development gulp start", 8 | "eslint": "NODE_ENV=development eslint .", 9 | "stylelint": "NODE_ENV=development stylelint '**/*.?(s)@(a|c)ss'", 10 | "lint": "npm run eslint && npm run stylelint", 11 | "build": "NODE_ENV=production gulp build" 12 | }, 13 | "keywords": [ 14 | "react-component", 15 | "react", 16 | "component", 17 | "validation", 18 | "form validate" 19 | ], 20 | "peerDependencies": { 21 | "react": "^ 0.13 || ^0.14 || ^15.0.0" 22 | }, 23 | "dependencies": { 24 | "joi": "^6.10.1", 25 | "lodash.clonedeep": "^3.0.1", 26 | "lodash.filter": "^3.1.1", 27 | "lodash.merge": "^3.3.2", 28 | "lodash.result": "^3.1.2", 29 | "lodash.startswith": "^3.0.1", 30 | "object-path": "^0.9.2" 31 | }, 32 | "devDependencies": { 33 | "autoprefixer": "^6.3.1", 34 | "babel": "^5.8.34", 35 | "babel-core": "^5.8.29", 36 | "babel-eslint": "^4.1.3", 37 | "babel-loader": "^5.3.2", 38 | "babel-plugin-react-transform": "^1.1.1", 39 | "bootstrap": "^3.3.6", 40 | "camelcase": "^2.0.1", 41 | "css-loader": "^0.23.0", 42 | "cssnano": "^3.3.2", 43 | "del": "^2.1.0", 44 | "eslint": "^1.10.1", 45 | "eslint-loader": "^1.2.0", 46 | "eslint-plugin-react": "^3.15.0", 47 | "express": "^4.13.3", 48 | "extract-text-webpack-plugin": "^1.0.1", 49 | "file-loader": "^0.8.5", 50 | "gulp": "^3.9.0", 51 | "gulp-babel": "^5.2.1", 52 | "gulp-concat": "^2.6.0", 53 | "gulp-eslint": "^1.1.0", 54 | "gulp-filter": "^3.0.1", 55 | "gulp-postcss": "^6.1.0", 56 | "gulp-sass": "^2.2.0", 57 | "gulp-stylelint": "^2.0.2", 58 | "history": "^2.0.1", 59 | "html-loader": "^0.4.0", 60 | "html-webpack-plugin": "^2.7.1", 61 | "http-proxy": "^1.12.0", 62 | "json-loader": "^0.5.4", 63 | "node-sass": "^3.4.2", 64 | "opn": "^4.0.0", 65 | "postcss-loader": "^0.8.0", 66 | "react-dom": "^0.14 || ^15.0.0", 67 | "react-router": "^2.0.0", 68 | "react-transform-hmr": "^1.0.1", 69 | "run-sequence": "^1.1.5", 70 | "sass-loader": "^3.1.2", 71 | "style-loader": "^0.13.0", 72 | "stylelint": "^6.5.0", 73 | "stylelint-config-standard": "^8.0.0", 74 | "stylelint-webpack-plugin": "^0.2.0", 75 | "webpack": "^1.12.11", 76 | "webpack-dev-middleware": "^1.2.0", 77 | "webpack-hot-middleware": "^2.5.0", 78 | "webpack-stream": "^3.0.1" 79 | }, 80 | "repository": { 81 | "type": "git", 82 | "url": "https://github.com/vn38minhtran/react-validation-decorator.git" 83 | }, 84 | "author": "Minh Tran", 85 | "license": "MIT", 86 | "bugs": { 87 | "url": "https://github.com/vn38minhtran/react-validation-decorator/issues" 88 | }, 89 | "homepage": "https://github.com/vn38minhtran/react-validation-decorator" 90 | } 91 | -------------------------------------------------------------------------------- /src/Validation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Joi from 'joi'; 3 | import Filter from 'lodash.filter'; 4 | import Result from 'lodash.result'; 5 | import ObjectPath from 'object-path'; 6 | import Merge from 'lodash.merge'; 7 | import CloneDeep from 'lodash.clonedeep'; 8 | import StartsWith from 'lodash.startswith'; 9 | 10 | const Validation = (ComposedComponent) => { 11 | return class ValidationComponent extends ComposedComponent { 12 | constructor(props, context) { 13 | super(props, context); 14 | this.state.validation = { 15 | dirty: [], 16 | errors: [], 17 | value: null 18 | }; 19 | } 20 | 21 | validate = (path, callback) => { 22 | let validationValue = CloneDeep(Result(this, 'validationValue', this.state)); 23 | if (typeof validationValue === 'object' && validationValue.hasOwnProperty('validation')) { 24 | delete validationValue.validation; 25 | } 26 | let validationSchema = Result(this, 'validationSchema'); 27 | let validationOptions = Merge({ 28 | abortEarly: false, 29 | allowUnknown: true 30 | }, Result(this, 'validationOptions', {})); 31 | Joi.validate(validationValue, validationSchema, validationOptions, (error, value) => { 32 | let validation = ObjectPath.get(this.state, 'validation', {}); 33 | validation.errors = (error && error.details) ? error.details : []; 34 | validation.value = value; 35 | let pushDirty = (p, dirtyArr = []) => { 36 | if (p && dirtyArr.indexOf(p) === -1) { 37 | dirtyArr.push(p); 38 | } 39 | let pArr = p.split('.'); 40 | if (pArr.length > 1) { 41 | pArr.splice(-1, 1); 42 | pushDirty(pArr.join('.'), dirtyArr); 43 | } 44 | }; 45 | pushDirty(path, validation.dirty); 46 | if (callback) { 47 | this.setState({ 48 | validation: validation 49 | }, callback); 50 | } else { 51 | this.setState({ 52 | validation: validation 53 | }); 54 | } 55 | }); 56 | }; 57 | 58 | handleValidation = (path) => { 59 | return (e) => { 60 | e.preventDefault(); 61 | this.validate(path); 62 | }; 63 | }; 64 | 65 | isValid = (path) => { 66 | let errors = ObjectPath.get(this.state, 'validation.errors', []); 67 | if (path) { 68 | errors = Filter(errors, (error) => (error.path === path || StartsWith(error.path, path + '.'))); 69 | } 70 | return errors.length === 0; 71 | }; 72 | 73 | isDirty = (path) => { 74 | let dirty = ObjectPath.get(this.state, 'validation.dirty', []); 75 | if (path) { 76 | dirty = Filter(dirty, (d) => d === path); 77 | } 78 | return dirty.length !== 0; 79 | }; 80 | 81 | getValidationMessages = (path) => { 82 | let errors = ObjectPath.get(this.state, 'validation.errors', []); 83 | if (path) { 84 | errors = Filter(errors, (error) => (error.path === path || StartsWith(error.path, path + '.'))); 85 | } 86 | return errors; 87 | }; 88 | 89 | getValidationValue = () => { 90 | return CloneDeep(ObjectPath.get(this.state, 'validation.value')); 91 | }; 92 | 93 | resetValidation = (callback) => { 94 | if (callback) { 95 | this.setState({ 96 | validation: { 97 | dirty: [], 98 | errors: [], 99 | value: null 100 | } 101 | }, callback); 102 | } else { 103 | this.setState({ 104 | validation: { 105 | dirty: [], 106 | errors: [], 107 | value: null 108 | } 109 | }); 110 | } 111 | }; 112 | 113 | getValidationClassName = (path, successClass = 'has-success', errorClass = 'has-error', defaultClass = 'form-group') => { 114 | let className = [defaultClass]; 115 | if (this.isValid(path) && this.isDirty(path)) { 116 | className.push(successClass); 117 | } 118 | if (!this.isValid(path) && this.isDirty(path)) { 119 | className.push(errorClass); 120 | } 121 | return className.join(' '); 122 | }; 123 | 124 | renderValidationMessages = (path, className = 'help-block', onlyFirst = true) => { 125 | let errors = this.getValidationMessages(path); 126 | if (errors.length !== 0 && this.isDirty(path)) { 127 | errors = onlyFirst ? [errors[0]] : errors; 128 | let html = errors.map(function (error, index) { 129 | return (
{error.message}
); 130 | }); 131 | return (
{html}
); 132 | } 133 | return null; 134 | }; 135 | 136 | updateState = (newState, callback) => { 137 | let state = this.state; 138 | let stateModel = ObjectPath(state); 139 | for (let property in newState) { 140 | if (newState.hasOwnProperty(property)) { 141 | stateModel.set(property, newState[property]); 142 | } 143 | } 144 | if (callback) { 145 | this.setState(state, callback); 146 | } else { 147 | this.setState(state); 148 | } 149 | }; 150 | }; 151 | }; 152 | 153 | export default Validation; 154 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Validation from './Validation.js'; 2 | import Joi from 'joi'; 3 | 4 | export {Validation, Joi}; 5 | export default Validation; 6 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import pkg from './package.json'; 3 | import camelCase from 'camelcase'; 4 | 5 | const capitalizeFirstLetter = (string) => { 6 | return string.charAt(0).toUpperCase() + string.slice(1); 7 | }; 8 | 9 | const webpackConfig = { 10 | output: { 11 | filename: pkg.name + '.js', 12 | library: capitalizeFirstLetter(camelCase(pkg.name)), 13 | libraryTarget: 'umd' 14 | }, 15 | externals: { 16 | react: { 17 | root: 'React', 18 | commonjs: 'react', 19 | commonjs2: 'react', 20 | amd: 'react' 21 | } 22 | }, 23 | module: { 24 | loaders: [ 25 | { 26 | test: /\.(js|jsx)$/, 27 | exclude: /(node_modules)/, 28 | loader: 'babel-loader' 29 | } 30 | ] 31 | }, 32 | resolve: { 33 | modulesDirectories: ['node_modules', 'bower_components'], 34 | extensions: ['', '.jsx', '.js'] 35 | }, 36 | plugins: [ 37 | new webpack.DefinePlugin({ 38 | 'process.env': { 39 | NODE_ENV: JSON.stringify(process.env.NODE_ENV) 40 | } 41 | }), 42 | new webpack.optimize.UglifyJsPlugin({ 43 | sourceMap: false, 44 | compress: { 45 | warnings: false 46 | }, 47 | output: { 48 | comments: false 49 | } 50 | }), 51 | new webpack.optimize.DedupePlugin() 52 | ], 53 | node: { 54 | net: 'mock', 55 | dns: 'mock' 56 | } 57 | }; 58 | 59 | export default webpackConfig; 60 | --------------------------------------------------------------------------------