├── .gitignore ├── .babelrc ├── src ├── client │ ├── reducers │ │ └── index.js │ ├── routing │ │ ├── routes.js │ │ ├── hooks │ │ │ └── validateFormTransitionHook.js │ │ └── router.js │ ├── store.js │ ├── index.js │ ├── views │ │ ├── App.js │ │ └── SimpleForm.js │ └── validators │ │ └── forms.js ├── utils │ ├── findComponentMethod.js │ └── renderFullPage.js └── server │ ├── index.js │ └── middleware │ └── routing.js ├── index.js ├── README.md ├── webpack.config.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | npm-debug.log 4 | assets 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "optional": ["es7.classProperties"], 3 | "stage": 0 4 | } 5 | -------------------------------------------------------------------------------- /src/client/reducers/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by mikhail on 15.09.15. 3 | */ 4 | export {reducer as form} from 'redux-form'; 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by mikhail on 15.09.15. 3 | */ 4 | require('babel/register')({ 5 | stage: 0 6 | }); 7 | 8 | global.__SERVER__ = true; 9 | 10 | require('./src/server/index'); 11 | -------------------------------------------------------------------------------- /src/client/routing/routes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by mikhail on 15.09.15. 3 | */ 4 | import React, {Component} from 'react'; 5 | import App from '../views/App.js'; 6 | import {Route} from 'react-router'; 7 | 8 | export default ( 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /src/client/store.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by mikhail on 15.09.15. 3 | */ 4 | import {createStore, combineReducers} from 'redux'; 5 | import * as reducers from './reducers/index.js'; 6 | 7 | const reducer = combineReducers(reducers); 8 | const store = createStore(reducer); 9 | 10 | export default store; 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Universal(isomorphic) example of [redux-form](https://github.com/erikras/redux-form) validation 2 | 3 | ### Problem 4 | The main problem is to validate the same form with enabled and disabled javascript in browser. 5 | 6 | ### How to try 7 | - Clone repository 8 | - Go to cloned project 9 | - ``` npm install ``` 10 | - ``` npm run start ``` 11 | -------------------------------------------------------------------------------- /src/utils/findComponentMethod.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by mikhail on 15.09.15. 3 | */ 4 | export default function (component, method) { 5 | let filteredMethod = component[method]; 6 | let decoratedComponent = (component.DecoratedComponent || component.WrappedComponent); 7 | 8 | if (!filteredMethod && decoratedComponent) { 9 | filteredMethod = decoratedComponent[method]; 10 | 11 | if (!filteredMethod) { 12 | filteredMethod = decoratedComponent.DecoratedComponent && decoratedComponent.DecoratedComponent[method]; 13 | } 14 | } 15 | 16 | return filteredMethod; 17 | } 18 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by mikhail on 15.09.15. 3 | */ 4 | import React from 'react'; 5 | import router from './routing/router.js'; 6 | import createLocation from 'history/lib/createLocation'; 7 | import createBrowserHistory from 'history/lib/createBrowserHistory'; 8 | 9 | const history = createBrowserHistory(); 10 | const location = createLocation(document.location.pathname, document.location.search); 11 | 12 | const dist = document.getElementById('app'); 13 | 14 | router(location, history) 15 | .then((reactEl) => { 16 | React.render(reactEl, dist); 17 | }, (err) => { 18 | React.render(

Initial render error {err}

, dist); 19 | }); 20 | -------------------------------------------------------------------------------- /src/client/routing/hooks/validateFormTransitionHook.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by mikhail on 15.09.15. 3 | */ 4 | import Promise from 'promise'; 5 | import findComponentMethod from '../../../utils/findComponentMethod.js'; 6 | import store from '../../store.js'; 7 | 8 | function getValidatedComponents(component) { 9 | return findComponentMethod(component, 'validateForm'); 10 | } 11 | 12 | export default function validateFormTransitionHook(renderProps, req) { 13 | let result = Promise.resolve(); 14 | 15 | if (req && renderProps) { 16 | result = Promise.all(renderProps.components 17 | .filter(component => getValidatedComponents(component)) 18 | .map(getValidatedComponents) 19 | .map(validateForm => validateForm(store, req))); 20 | } 21 | 22 | return result; 23 | } 24 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by mikhail on 15.09.15. 3 | */ 4 | var webpack = require('webpack'); 5 | var path = require('path'); 6 | 7 | module.exports = { 8 | devtool: 'eval-source-map', 9 | entry: [ 10 | './src/client/index' 11 | ], 12 | output: { 13 | path: path.join(__dirname, 'assets', 'scripts'), 14 | filename: 'app.js' 15 | }, 16 | progress: true, 17 | resolve: { 18 | extensions: ['', '.js'] 19 | }, 20 | module: { 21 | loaders: [ 22 | { 23 | test: /\.js$/, 24 | loaders: ['babel'], 25 | exclude: /node_modules/, 26 | include: [ 27 | path.resolve(__dirname, 'src', 'client'), 28 | path.resolve(__dirname, 'src', 'utils') 29 | ] 30 | } 31 | ] 32 | }, 33 | plugins: [ 34 | new webpack.DefinePlugin({ 35 | __SERVER__: false 36 | }) 37 | ] 38 | }; 39 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by mikhail on 15.09.15. 3 | */ 4 | import express from 'express'; 5 | import routing from './middleware/routing.js'; 6 | import bodyParser from 'body-parser'; 7 | 8 | const app = express(); 9 | 10 | app.use(bodyParser.urlencoded({extended: false})); 11 | app.use(bodyParser.json()); 12 | app.use('/assets', express.static('assets')); 13 | 14 | // just for one case 15 | app.use('/api/simpleForm', (req, res) => { 16 | let data = req.body; 17 | let result = { 18 | errors: {} 19 | }; 20 | 21 | if (data.name === 'SimpleForm') { 22 | result.errors.name = 'Name already exist'; 23 | } 24 | 25 | { 26 | // do some more server validation; 27 | } 28 | 29 | if (!Object.keys(result.errors).length) { 30 | result.errors = null; 31 | } 32 | 33 | res.json(result); 34 | }); 35 | 36 | app.use(routing); 37 | 38 | app.listen(3000, () => { 39 | console.log('Server start at http://localhost:3000'); 40 | }); 41 | -------------------------------------------------------------------------------- /src/client/views/App.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by mikhail on 15.09.15. 3 | */ 4 | import React, {Component} from 'react'; 5 | import SimpleForm from './SimpleForm.js'; 6 | import findComponentMethod from '../../utils/findComponentMethod.js'; 7 | 8 | const validateSimpleForm = findComponentMethod(SimpleForm, 'validateForm'); 9 | 10 | class App extends Component { 11 | static validateForm(...args) { 12 | return validateSimpleForm && validateSimpleForm(...args); 13 | } 14 | render() { 15 | return ( 16 |
17 |

Simple form example

18 | 19 |

This example shows the simple form validation with enabled and disabled javascript.

20 |

You can try to fill and submit the form. Result you can se in console

21 |

*SimpleForm is the value which would not be valid, some kind of server validation

22 |
23 | ); 24 | } 25 | } 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /src/utils/renderFullPage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by mikhail on 15.09.15. 3 | */ 4 | export default function (reactEl) { 5 | return ` 6 | 7 | 8 | 9 | Redux-form universal example 10 | 29 | 30 | 31 |
${reactEl}
32 | 33 | 34 | 35 | `; 36 | } 37 | -------------------------------------------------------------------------------- /src/server/middleware/routing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by mikhail on 16.07.15. 3 | */ 4 | import React from 'react'; 5 | import Router from 'react-router'; 6 | import Promise from 'promise'; 7 | import routes from '../../client/routing/routes.js'; 8 | import renderFullPage from '../../utils/renderFullPage.js'; 9 | import router from '../../client/routing/router.js'; 10 | import createLocation from 'history/lib/createLocation'; 11 | import createMemoryHistory from 'history/lib/createMemoryHistory'; 12 | 13 | const history = createMemoryHistory(); 14 | 15 | export default function routing(req, res) { 16 | let location = createLocation(req.path); 17 | 18 | router(location, history, req, res) 19 | .then((reactEl) => { 20 | try { 21 | let reactStr = React.renderToString(reactEl); 22 | res.send(renderFullPage(reactStr)); 23 | } catch (err) { 24 | res.status(500).send({error: err.toString()}); 25 | } 26 | }, ({message, code}) => { 27 | console.error(message, code); 28 | res.sendStatus(code || 500); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /src/client/validators/forms.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by mikhail on 15.09.15. 3 | */ 4 | import Promise from 'promise'; 5 | import superagent from 'superagent'; 6 | 7 | export function validateSignUpFormAsync(data) { 8 | let url = '/api/simpleForm'; 9 | 10 | if (__SERVER__) { 11 | url = `http://localhost:3000${url}`; 12 | } 13 | 14 | return new Promise((fullfill, reject) => { 15 | superagent.post(url) 16 | .send(data) 17 | .end((err, response) => { 18 | let defaultValidResult = {}; 19 | 20 | if (err) { 21 | reject(err); 22 | } 23 | 24 | if (__SERVER__) { 25 | defaultValidResult = {valid: true}; 26 | } 27 | 28 | 29 | let result = response.body.errors || defaultValidResult; 30 | 31 | fullfill(result); 32 | }); 33 | }); 34 | } 35 | 36 | export function validateSignUpFormSync(data) { 37 | const errors = {}; 38 | 39 | if (!data.name) { 40 | errors.name = 'Required'; 41 | } 42 | 43 | if (!data.email) { 44 | errors.email = 'Required'; 45 | } 46 | 47 | return errors; 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-form-universal-example", 3 | "version": "0.0.2", 4 | "description": "Universal(isomorphic) example of redux-form validation", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npm run build-client && npm run serve", 8 | "serve": "node --harmony ./index.js", 9 | "build-client": "webpack", 10 | "test": "test" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/mikhail-riabokon/redux-form-universal-example.git" 15 | }, 16 | "keywords": [ 17 | "redux-form", 18 | "isomorphic", 19 | "form-validation" 20 | ], 21 | "author": "Mikhail Riabokon", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/mikhail-riabokon/redux-form-universal-example/issues" 25 | }, 26 | "homepage": "https://github.com/mikhail-riabokon/redux-form-universal-example#readme", 27 | "dependencies": { 28 | "babel": "^5.8.23", 29 | "babel-core": "^5.8.24", 30 | "babel-loader": "^5.3.2", 31 | "body-parser": "^1.13.3", 32 | "express": "^4.13.3", 33 | "history": "^1.9.1", 34 | "promise": "^7.0.4", 35 | "react": "^0.13.3", 36 | "react-redux": "^2.1.2", 37 | "react-router": "^1.0.0-rc1", 38 | "redux": "^3.0.0", 39 | "redux-form": "^1.5.3", 40 | "superagent": "^1.4.0", 41 | "webpack": "^1.12.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/client/routing/router.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by mikhail on 15.09.15. 3 | */ 4 | import React from 'react'; 5 | import Promise from 'promise'; 6 | import {Provider} from 'react-redux'; 7 | import Router, {RoutingContext} from 'react-router'; 8 | import routes from './routes.js'; 9 | import {match} from 'react-router'; 10 | import validateFormTransitionHook from './hooks/validateFormTransitionHook.js'; 11 | import store from '../../client/store.js'; 12 | 13 | function getInitialComponent(renderedProps) { 14 | let component = null; 15 | 16 | if (__SERVER__) { 17 | component = (); 18 | } else { 19 | component = React.createElement(Router, renderedProps); 20 | } 21 | 22 | return component; 23 | } 24 | 25 | export default function (location, history, req, res) { 26 | return new Promise((fullfill, reject) => { 27 | match({location, history, routes}, (err, redirectInfo, renderedProps) => { 28 | if (!err) { 29 | if (renderedProps) { 30 | renderedProps.history = history; 31 | 32 | Promise.all([ 33 | validateFormTransitionHook(renderedProps, req) 34 | ]) 35 | .then(() => { 36 | fullfill( 37 | 38 | {() => getInitialComponent(renderedProps)} 39 | 40 | ); 41 | }, (...args) => { 42 | reject(...args) 43 | }) 44 | } else { 45 | reject({ 46 | message: 'Not found', 47 | code: 404 48 | }); 49 | } 50 | } else { 51 | reject(err); 52 | } 53 | }); 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /src/client/views/SimpleForm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by mikhail on 15.09.15. 3 | */ 4 | import React, {PropTypes, Component} from 'react'; 5 | import {connectReduxForm, initialize, stopSubmit} from 'redux-form'; 6 | import {validateSignUpFormSync, validateSignUpFormAsync} from '../validators/forms.js'; 7 | 8 | @connectReduxForm({ 9 | form: 'simpleForm', 10 | fields: ['name', 'email'], 11 | validate: validateSignUpFormSync, 12 | asyncValidate: validateSignUpFormAsync 13 | }) 14 | class SimpleForm extends Component { 15 | static propTypes = { 16 | dispatch: PropTypes.func, 17 | fields: PropTypes.object, 18 | handleSubmit: PropTypes.func, 19 | valid: PropTypes.bool 20 | }; 21 | static validateForm(store, req) { 22 | let data = {}; 23 | let result = Promise.resolve(true); 24 | 25 | if (req.method === 'POST') { 26 | data = req.body || data; 27 | 28 | // validate form on the server 29 | result = validateSignUpFormAsync(data).then((response) => { 30 | if (!response.valid) { 31 | store.dispatch(stopSubmit('simpleForm', response)); 32 | } else { 33 | console.log('Form is valid, do something else'); 34 | } 35 | }); 36 | } 37 | //init form 38 | store.dispatch(initialize('simpleForm', data)); 39 | 40 | return result; 41 | } 42 | submitForm(validFormData) { 43 | // you can save data here 44 | console.log('Valid form data -->', JSON.stringify(validFormData, null, 4)); 45 | } 46 | showError(field) { 47 | return (__SERVER__) ? !!field.error : field.touched && field.error; 48 | } 49 | render() { 50 | const { 51 | fields: { 52 | name, 53 | email 54 | }, 55 | handleSubmit 56 | } = this.props; 57 | 58 | return ( 59 |
60 |
61 | 65 | {this.showError(name) ?
{name.error}
: null} 66 |
67 |
68 | 72 | {this.showError(email) ?
{email.error}
: null} 73 |
74 | 75 |
76 | ); 77 | } 78 | } 79 | 80 | export default SimpleForm; 81 | --------------------------------------------------------------------------------