├── .gitignore ├── build.config.js ├── demo.js ├── dist └── demo.html ├── package.json └── src ├── actions.js ├── components ├── Form.js ├── SubmitButton.js └── Text.js ├── constants.js ├── index.js ├── store.js └── validators.js /.gitignore: -------------------------------------------------------------------------------- 1 | dist/*.js 2 | node_modules 3 | .idea 4 | .DS_Store 5 | npm-debug.log -------------------------------------------------------------------------------- /build.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | context: __dirname, //current folder as the reference to the other paths 6 | entry: { 7 | demo: './demo.js' //entry point for building scripts 8 | }, 9 | output: { 10 | path: path.resolve('./dist'), //save result in 'dist' folder 11 | filename: 'demo.js' 12 | }, 13 | module: { 14 | loaders: [ 15 | { //transpile ES2015 with JSX into ES5 16 | test: /\.js?$/, 17 | exclude: /node_modules/, 18 | loader: 'babel', 19 | query: { 20 | presets: ['react', 'es2015'] 21 | } 22 | } 23 | ] 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /demo.js: -------------------------------------------------------------------------------- 1 | // demo.js 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import Form, {Text, SubmitButton} from './src/index'; 5 | 6 | ReactDOM.render(( 7 |
console.log(data)}> 8 | 13 | 18 | 23 | 24 | 25 | 26 | ), document.getElementById('container')); 27 | -------------------------------------------------------------------------------- /dist/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demo 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/demo.js", 6 | "scripts": { 7 | "watch": "webpack --progress --colors --watch --config ./build.config.js", 8 | "build": "webpack --config ./build.config.js" 9 | }, 10 | "author": "Kasper Warguła", 11 | "license": "MIT", 12 | "dependencies": { 13 | "lodash.assign": "4.0.1", 14 | "material-ui": "0.14.3", 15 | "react": "0.14.7", 16 | "react-dom": "0.14.7", 17 | "react-redux": "4.1.1", 18 | "react-tap-event-plugin": "0.2.2", 19 | "redux": "3.1.2", 20 | "redux-logger": "2.4.0", 21 | "redux-thunk": "1.0.3" 22 | }, 23 | "devDependencies": { 24 | "babel-loader": "6.2.1", 25 | "babel-preset-es2015": "6.3.13", 26 | "babel-preset-react": "6.3.13", 27 | "path": "0.12.7", 28 | "webpack": "1.12.12" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | // src/actions.js 2 | import * as c from './constants'; 3 | 4 | export function update(name, value) { 5 | return dispatch => dispatch({ 6 | type: c.FORM_UPDATE_VALUE, 7 | name, value 8 | }); 9 | } 10 | 11 | export function reset() { 12 | return dispatch => dispatch({ 13 | type: c.FORM_RESET 14 | }); 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/components/Form.js: -------------------------------------------------------------------------------- 1 | // src/components/Form.js 2 | import React, {PropTypes} from 'react'; 3 | import without from 'lodash.without'; 4 | import assign from 'lodash.assign'; 5 | 6 | const noop = () => undefined; 7 | 8 | export default React.createClass({ 9 | displayName: 'Form', 10 | 11 | propTypes: { 12 | children: PropTypes.node, 13 | values: PropTypes.object, 14 | update: PropTypes.func, 15 | reset: PropTypes.func, 16 | onSubmit: PropTypes.func 17 | }, 18 | 19 | childContextTypes: { 20 | update: PropTypes.func, 21 | reset: PropTypes.func, 22 | submit: PropTypes.func, 23 | values: PropTypes.object, 24 | registerValidation: PropTypes.func, 25 | isFormValid: PropTypes.func, 26 | }, 27 | 28 | getDefaultProps() { 29 | return { 30 | onSubmit: noop 31 | }; 32 | }, 33 | 34 | validations: [], 35 | 36 | registerValidation(isValidFunc) { 37 | this.validations = [...this.validations, isValidFunc]; 38 | return this.removeValidation.bind(null, isValidFunc); 39 | }, 40 | 41 | removeValidation(ref) { 42 | this.validations = without(this.validations, ref); 43 | }, 44 | 45 | isFormValid(showErrors) { 46 | return this.validations.reduce((memo, isValidFunc) => 47 | isValidFunc(showErrors) && memo, true); 48 | }, 49 | 50 | submit(){ 51 | if (this.isFormValid(true)) { 52 | this.props.onSubmit(assign({}, this.props.values)); 53 | this.props.reset(); 54 | } 55 | }, 56 | 57 | getChildContext() { 58 | return { 59 | update: this.props.update, 60 | reset: this.props.reset, 61 | submit: this.submit, 62 | values: this.props.values, 63 | registerValidation: this.registerValidation, 64 | isFormValid: this.isFormValid 65 | }; 66 | }, 67 | 68 | render() { 69 | return ( 70 |
71 | {this.props.children} 72 |
73 | ); 74 | } 75 | }); 76 | -------------------------------------------------------------------------------- /src/components/SubmitButton.js: -------------------------------------------------------------------------------- 1 | // src/components/SubmitButton.js 2 | import React, { PropTypes } from 'react'; 3 | import RaisedButton from 'material-ui/lib/raised-button'; 4 | 5 | export default React.createClass({ 6 | 7 | displayName: 'SubmitButton', 8 | 9 | propTypes: { 10 | label: PropTypes.string 11 | }, 12 | 13 | contextTypes: { 14 | isFormValid: PropTypes.func.isRequired, 15 | submit: PropTypes.func.isRequired 16 | }, 17 | 18 | getDefaultProps() { 19 | return { 20 | label: 'Submit' 21 | }; 22 | }, 23 | 24 | render() { 25 | return ( 26 | 31 | ); 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /src/components/Text.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import TextField from 'material-ui/lib/text-field'; 3 | import * as validators from '../validators'; 4 | 5 | export default React.createClass({ 6 | 7 | displayName: 'Text', 8 | 9 | propTypes: { 10 | name: PropTypes.string.isRequired, 11 | placeholder: PropTypes.string, 12 | label: PropTypes.string, 13 | validate: PropTypes.arrayOf(PropTypes.string) 14 | }, 15 | 16 | contextTypes: { 17 | update: PropTypes.func.isRequired, 18 | values: PropTypes.object.isRequired, 19 | registerValidation: PropTypes.func.isRequired 20 | }, 21 | 22 | componentWillMount() { 23 | this.removeValidationFromContext = this.context.registerValidation(show => 24 | this.isValid(show)); 25 | }, 26 | 27 | componentWillUnmount() { 28 | this.removeValidationFromContext(); 29 | }, 30 | 31 | getDefaultProps() { 32 | return { 33 | validate: [] 34 | } 35 | }, 36 | 37 | getInitialState() { 38 | return { 39 | errors: [] 40 | }; 41 | }, 42 | 43 | updateValue(value) { 44 | this.context.update(this.props.name, value); 45 | 46 | if (this.state.errors.length) { 47 | setTimeout(() => this.isValid(true), 0); 48 | } 49 | }, 50 | 51 | onChange(event) { 52 | this.updateValue(event.target.value) 53 | }, 54 | 55 | isValid(showErrors) { 56 | const errors = this.props.validate 57 | .reduce((memo, currentName) => 58 | memo.concat(validators[currentName]( 59 | this.context.values[this.props.name] 60 | )), []); 61 | 62 | if (showErrors) { 63 | this.setState({ 64 | errors 65 | }); 66 | } 67 | return !errors.length; 68 | }, 69 | 70 | onBlur() { 71 | this.isValid(true); 72 | }, 73 | 74 | render() { 75 | return ( 76 |
77 | 85 | {this.state.errors.map((error, i) =>
{error}
)} 86 |
87 | ) : null}/> 88 | 89 | ); 90 | } 91 | }); -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | // src/constants.js 2 | export const FORM_UPDATE_VALUE = 'FORM_UPDATE_VALUE'; 3 | export const FORM_RESET = 'FORM_RESET'; 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // src/index.js 2 | import React, {PropTypes} from 'react'; 3 | import { createStore, applyMiddleware, compose } from 'redux'; 4 | import { connect } from 'react-redux'; 5 | import { Provider } from 'react-redux'; 6 | import thunk from 'redux-thunk'; 7 | import createLogger from 'redux-logger'; 8 | import Form from './components/Form'; 9 | import * as actions from './actions'; 10 | import store from './store'; 11 | import injectTapEventPlugin from 'react-tap-event-plugin'; 12 | 13 | injectTapEventPlugin(); 14 | 15 | const SmartForm = connect(state => state, actions)(Form); 16 | 17 | const reduxMiddleware = applyMiddleware(thunk, createLogger()); 18 | 19 | export default props => ( 20 | 21 | 22 | 23 | ); 24 | 25 | export {default as Text} from './components/Text'; 26 | export {default as SubmitButton} from './components/SubmitButton'; 27 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import * as c from './constants'; 2 | import assign from 'lodash.assign'; 3 | 4 | const initialState = { //define initial state - an empty form 5 | values: {} 6 | }; 7 | 8 | export default (state = initialState, action) => { 9 | switch (action.type) { 10 | 11 | case c.FORM_UPDATE_VALUE: 12 | return assign({}, state, { 13 | values: assign({}, state.values, { 14 | [action.name]: action.value 15 | }) 16 | }); 17 | 18 | case c.FORM_RESET: 19 | return initialState; 20 | 21 | default: 22 | return state; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/validators.js: -------------------------------------------------------------------------------- 1 | // src/validators.js 2 | import validUrl from 'valid-url'; 3 | import emailValidator from 'email-validator'; 4 | 5 | export function required(value) { 6 | return !value ? ['This field cannot be empty'] : []; 7 | } 8 | 9 | export function url(value) { 10 | return value && !validUrl.isWebUri(value) ? ['This URL is invalid'] : []; 11 | } 12 | 13 | export function email(value) { 14 | return !emailValidator.validate(value) ? ['This email address is invalid']: []; 15 | } 16 | --------------------------------------------------------------------------------