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