├── .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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------