├── .babelrc ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── server.js ├── src ├── App │ ├── App.js │ ├── PersonForm │ │ ├── PersonAddress.js │ │ ├── PersonForm.js │ │ ├── PersonInfo.js │ │ ├── PersonTask.js │ │ └── index.js │ └── index.js ├── components │ ├── InputCheckbox.js │ ├── InputCheckboxes.js │ ├── InputField.js │ ├── InputRadio.js │ ├── asForm.js │ └── index.js ├── data │ └── person.js ├── index.html └── index.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-1"], 3 | "plugins": ["transform-decorators-legacy"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "modules": true, 4 | "experimentalObjectRestSpread": true, 5 | "jsx": true 6 | }, 7 | 8 | "parser": "babel-eslint", 9 | 10 | "env": { 11 | "browser": true, 12 | "es6": true, 13 | "node": true, 14 | "mocha": true 15 | }, 16 | 17 | "extends": "eslint-config-standard", 18 | 19 | "plugins": [ 20 | "standard", 21 | "react" 22 | ], 23 | 24 | "globals": { 25 | "expect": false, 26 | "sinon": false, 27 | "inject": false 28 | }, 29 | 30 | "rules": { 31 | "react/display-name": 1, 32 | "react/forbid-prop-types": 1, 33 | "react/jsx-boolean-value": 1, 34 | "react/jsx-closing-bracket-location": 0, 35 | "react/jsx-curly-spacing": 1, 36 | "react/jsx-indent-props": 1, 37 | "react/jsx-max-props-per-line": 0, 38 | "react/jsx-no-bind": 1, 39 | "react/jsx-no-duplicate-props": 1, 40 | "react/jsx-no-literals": 0, 41 | "react/jsx-no-undef": 1, 42 | "jsx-quotes": 0, 43 | "react/jsx-sort-prop-types": 0, 44 | "react/jsx-sort-props": 0, 45 | "react/jsx-uses-react": 1, 46 | "react/jsx-uses-vars": 1, 47 | "react/no-danger": 1, 48 | "react/no-did-mount-set-state": 1, 49 | "react/no-did-update-set-state": 1, 50 | "react/no-direct-mutation-state": 1, 51 | "react/no-multi-comp": 1, 52 | "react/no-set-state": 0, 53 | "react/no-unknown-property": 1, 54 | "react/prefer-es6-class": 1, 55 | "react/prop-types": 1, 56 | "react/react-in-jsx-scope": 1, 57 | "react/require-extension": 1, 58 | "react/self-closing-comp": 1, 59 | "react/sort-comp": 1, 60 | "react/wrap-multilines": 1, 61 | "no-console": 1 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | 13 | # Compiled binary addons (http://nodejs.org/api/addons.html) 14 | build/Release 15 | 16 | # Dependency directory 17 | # Commenting this out is preferred by some people, see 18 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 19 | node_modules 20 | 21 | # Users Environment Variables 22 | .lock-wscript 23 | dist 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 KEVIN PURNELLE 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Handling react forms with mobx 3 | 4 | This is a small app to explain how to tackle forms in React with Mobx. 5 | 6 | It is to be used with the [Handling forms in React with Mobx article](https://blog.risingstack.com/handling-react-forms-with-mobx-observables/) on the [RisingStack blog](https://blog.risingstack.com/). 7 | 8 | 9 | ## Following the article 10 | The repo has various tags that correspond to a step in the article. 11 | As we progress in the article, I indicate which tag should be checked out. 12 | 13 | 14 | ## How to run the app 15 | 16 | * git clone 17 | * npm install 18 | * npm start 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobx-react-boilerplate", 3 | "version": "1.0.0", 4 | "description": "Small app to show form handling with mobx and React for the article https://blog.risingstack.com/handling-react-forms-with-mobx-observables/", 5 | "scripts": { 6 | "start": "node server.js", 7 | "lint": "eslint src" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "" 12 | }, 13 | "keywords": [ 14 | "mobservable", 15 | "mobx", 16 | "react", 17 | "reactjs", 18 | "boilerplate", 19 | "webpack" 20 | ], 21 | "author": "Kevin Purnelle (http://github.com/aodev)", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/AoDev/react-forms-mobx/issues" 25 | }, 26 | "homepage": "https://github.com/AoDev/react-forms-mobx", 27 | "devDependencies": { 28 | "babel-core": "6.8.0", 29 | "babel-eslint": "6.0.4", 30 | "babel-loader": "6.2.4", 31 | "babel-plugin-transform-decorators-legacy": "1.3.4", 32 | "babel-preset-es2015": "6.6.0", 33 | "babel-preset-react": "6.5.0", 34 | "babel-preset-stage-1": "6.5.0", 35 | "babel-register": "6.8.0", 36 | "classnames": "2.2.5", 37 | "css-loader": "0.23.1", 38 | "eslint": "2.10.1", 39 | "eslint-config-standard": "5.3.1", 40 | "eslint-plugin-promise": "1.1.0", 41 | "eslint-plugin-react": "5.1.1", 42 | "eslint-plugin-standard": "1.3.2", 43 | "html-webpack-plugin": "2.17.0", 44 | "mobx": "2.1.6", 45 | "mobx-react": "3.1.0", 46 | "react": "15.0.2", 47 | "react-dom": "15.0.2", 48 | "react-hot-loader": "1.3.0", 49 | "webpack": "1.13.0", 50 | "webpack-dev-server": "1.14.1" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | var WebpackDevServer = require('webpack-dev-server') 3 | var config = require('./webpack.config') 4 | var PORT = process.env.PORT || 3333 5 | 6 | new WebpackDevServer(webpack(config), { 7 | publicPath: config.output.publicPath, 8 | hot: true, 9 | historyApiFallback: true 10 | }) 11 | .listen(PORT, 'localhost', function (err, result) { 12 | if (err) { 13 | console.log(err) 14 | } 15 | console.log('Listening at localhost:' + PORT) 16 | }) 17 | -------------------------------------------------------------------------------- /src/App/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import PersonForm from './PersonForm' 3 | import {observable, toJSON} from 'mobx' 4 | import personData from '../data/person' 5 | 6 | export default class App extends Component { 7 | constructor (props) { 8 | super(props) 9 | this.person = observable(personData) 10 | this.submitForm = this.submitForm.bind(this) 11 | } 12 | 13 | submitForm (event) { 14 | event.preventDefault() 15 | console.log('Sending form', JSON.stringify(toJSON(this.person), null, 2)) 16 | } 17 | 18 | render () { 19 | return ( 20 |
21 |

Handling forms in React with Mobx

22 |
23 | 24 |
25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/App/PersonForm/PersonAddress.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import {observer} from 'mobx-react' 3 | import {InputField, asForm} from '../../components' 4 | 5 | @observer 6 | class PersonAddress extends Component { 7 | render () { 8 | const {address, updateProperty} = this.props 9 | return ( 10 |
11 | 12 | 13 | 14 |
15 | ) 16 | } 17 | } 18 | 19 | PersonAddress.propTypes = { 20 | address: PropTypes.shape({ 21 | city: PropTypes.string, 22 | postalCode: PropTypes.string, 23 | street: PropTypes.string 24 | }) 25 | } 26 | 27 | export default asForm(PersonAddress, 'address') 28 | -------------------------------------------------------------------------------- /src/App/PersonForm/PersonForm.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import {observer} from 'mobx-react' 3 | import PersonInfo from './PersonInfo' 4 | import PersonAddress from './PersonAddress' 5 | import PersonTask from './PersonTask' 6 | 7 | @observer 8 | export default class PersonForm extends Component { 9 | constructor (props) { 10 | super(props) 11 | this.updateProperty = this.updateProperty.bind(this) 12 | } 13 | 14 | updateProperty (key, value) { 15 | this.props.person[key] = value 16 | } 17 | 18 | render () { 19 | const {person, submitForm} = this.props 20 | return ( 21 |
22 |

My Person Form

23 |
24 |
25 |
26 |
27 | General info 28 | 29 |
30 |
31 |
32 |
33 | Address 34 | 35 |
36 |
37 |
38 |
39 |
40 |
41 | Tasks 42 | { 43 | person.tasks 44 | .map((task) => ) 45 | } 46 |
47 |
48 |
49 |

Send updated data

50 |

(check the console)

51 | 52 |
53 |
54 |
55 |
56 | ) 57 | } 58 | } 59 | 60 | PersonForm.propTypes = { 61 | person: PropTypes.shape({ 62 | fullName: PropTypes.string, 63 | job: PropTypes.string, 64 | email: PropTypes.string 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /src/App/PersonForm/PersonInfo.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import {observer} from 'mobx-react' 3 | import {InputField, InputRadio, InputCheckbox, InputCheckboxes, asForm} from '../../components' 4 | 5 | const sexes = ['female', 'male'] 6 | const mascots = ['bird', 'cat', 'dog', 'iguana', 'pig', 'other'] 7 | 8 | @observer 9 | class PersonInfo extends Component { 10 | render () { 11 | const {person, updateProperty, updateArray} = this.props 12 | return ( 13 |
14 | 15 | 16 | 17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 | ) 28 | } 29 | } 30 | 31 | PersonInfo.propTypes = { 32 | info: PropTypes.shape({ 33 | fullName: PropTypes.string, 34 | job: PropTypes.string, 35 | email: PropTypes.string 36 | }) 37 | } 38 | 39 | export default asForm(PersonInfo, 'person') 40 | -------------------------------------------------------------------------------- /src/App/PersonForm/PersonTask.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import {observer} from 'mobx-react' 3 | import {InputField, asForm} from '../../components' 4 | 5 | @observer 6 | class PersonTask extends Component { 7 | render () { 8 | const {task, updateProperty} = this.props 9 | return ( 10 |
11 | 12 | 13 |
14 | ) 15 | } 16 | } 17 | 18 | PersonTask.propTypes = { 19 | task: PropTypes.shape({ 20 | name: PropTypes.string, 21 | dueDate: PropTypes.string 22 | }) 23 | } 24 | 25 | export default asForm(PersonTask, 'task') 26 | -------------------------------------------------------------------------------- /src/App/PersonForm/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './PersonForm' 2 | -------------------------------------------------------------------------------- /src/App/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './App' 2 | -------------------------------------------------------------------------------- /src/components/InputCheckbox.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import {observer} from 'mobx-react' 3 | 4 | @observer 5 | export default class InputCheckbox extends Component { 6 | constructor (props) { 7 | super(props) 8 | this.onChange = this.onChange.bind(this) 9 | } 10 | 11 | onChange (event) { 12 | this.props.onChange(this.props.name, event.target.checked) 13 | } 14 | 15 | render () { 16 | const {name, value, id} = this.props 17 | return ( 18 |
19 | 23 |
24 | ) 25 | } 26 | } 27 | 28 | InputCheckbox.propTypes = { 29 | onChange: PropTypes.func.isRequired, 30 | name: PropTypes.string.isRequired 31 | } 32 | -------------------------------------------------------------------------------- /src/components/InputCheckboxes.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import {observer} from 'mobx-react' 3 | 4 | @observer 5 | export default class InputCheckboxes extends Component { 6 | constructor (props) { 7 | super(props) 8 | this.onChange = this.onChange.bind(this) 9 | } 10 | 11 | onChange (event) { 12 | this.props.onChange(this.props.name, event.target.value) 13 | } 14 | 15 | render () { 16 | const {items, name, checkedItems} = this.props 17 | return ( 18 |
19 | {name} 20 | { 21 | items.map((item) => { 22 | return ( 23 |
24 | 29 |
30 | ) 31 | }) 32 | } 33 |
34 | ) 35 | } 36 | } 37 | 38 | InputCheckboxes.propTypes = { 39 | onChange: PropTypes.func.isRequired, 40 | name: PropTypes.string.isRequired 41 | } 42 | -------------------------------------------------------------------------------- /src/components/InputField.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import {observer} from 'mobx-react' 3 | 4 | @observer 5 | export default class InputField extends Component { 6 | constructor (props) { 7 | super(props) 8 | this.onChange = this.onChange.bind(this) 9 | } 10 | 11 | onChange (event) { 12 | this.props.onChange(event.target.name, event.target.value) 13 | } 14 | 15 | render () { 16 | const input = this.props 17 | return ( 18 |
19 | 20 | 27 |
28 | ) 29 | } 30 | } 31 | 32 | InputField.propTypes = { 33 | onChange: PropTypes.func.isRequired 34 | } 35 | 36 | InputField.defaultProps = { 37 | type: 'text' 38 | } 39 | -------------------------------------------------------------------------------- /src/components/InputRadio.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import {observer} from 'mobx-react' 3 | 4 | @observer 5 | export default class InputRadio extends Component { 6 | constructor (props) { 7 | super(props) 8 | this.onChange = this.onChange.bind(this) 9 | } 10 | 11 | onChange (event) { 12 | this.props.onChange(this.props.name, event.target.value) 13 | } 14 | 15 | render () { 16 | const {items, name, value} = this.props 17 | return ( 18 |
19 | {name} 20 | { 21 | items.map((item) => { 22 | return ( 23 |
24 | 29 |
30 | ) 31 | }) 32 | } 33 |
34 | ) 35 | } 36 | } 37 | 38 | InputRadio.propTypes = { 39 | onChange: PropTypes.func.isRequired, 40 | name: PropTypes.string.isRequired 41 | } 42 | -------------------------------------------------------------------------------- /src/components/asForm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * asForm Higher Order Component 3 | */ 4 | import React, {Component} from 'react' 5 | 6 | export default function asForm (MyComponent, formDataProp) { 7 | return class Form extends Component { 8 | constructor (props) { 9 | super(props) 10 | this.updateProperty = this.updateProperty.bind(this) 11 | this.updateArray = this.updateArray.bind(this) 12 | } 13 | 14 | updateProperty (key, value) { 15 | this.props[formDataProp][key] = value 16 | } 17 | 18 | updateArray (key, value) { 19 | const array = this.props[formDataProp][key] 20 | const index = array.indexOf(value) 21 | 22 | if (array.indexOf(value) > -1) { 23 | array.splice(index, 1) 24 | } else { 25 | array.push(value) 26 | } 27 | } 28 | 29 | render () { 30 | return ( 31 | 34 | ) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import asForm from './asForm' 2 | import InputCheckbox from './InputCheckbox' 3 | import InputCheckboxes from './InputCheckboxes' 4 | import InputField from './InputField' 5 | import InputRadio from './InputRadio' 6 | 7 | export { 8 | asForm, 9 | InputCheckbox, 10 | InputCheckboxes, 11 | InputField, 12 | InputRadio 13 | } 14 | -------------------------------------------------------------------------------- /src/data/person.js: -------------------------------------------------------------------------------- 1 | const person = { 2 | fullName: 'John Doe', 3 | job: 'Web Developer', 4 | email: 'john.doe@example.com', 5 | sex: 'male', 6 | address: { 7 | city: 'Budapest', 8 | postalCode: '1000', 9 | street: 'Reactive street' 10 | }, 11 | alive: true, 12 | mascots: ['bird', 'dog'], 13 | tasks: [ 14 | { 15 | id: '1', 16 | name: 'Write the article about forms', 17 | dueDate: 'yesterday' 18 | }, 19 | { 20 | id: '2', 21 | name: 'Eat something', 22 | dueDate: 'Now' 23 | }, 24 | { 25 | id: '3', 26 | name: 'Go to sleep', 27 | dueDate: 'Soon' 28 | } 29 | ] 30 | } 31 | 32 | export default person 33 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Handling forms with React and Mobx 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import App from './App' 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | 5 | ReactDOM.render( 6 | , document.getElementById('app-root') 7 | ) 8 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | var HtmlWebpackPlugin = require('html-webpack-plugin') 4 | var PORT = process.env.PORT || 3333 5 | var PRODUCTION = process.env.NODE_ENV === 'production' 6 | var SRC_DIR = path.resolve(__dirname, './src') 7 | 8 | var config = { 9 | entry: [ 10 | 'webpack-dev-server/client?http://localhost:' + PORT, 11 | 'webpack/hot/only-dev-server', 12 | './src/index' 13 | ], 14 | output: { 15 | path: path.join(__dirname, 'dist'), 16 | filename: 'app.js', 17 | publicPath: '/' 18 | }, 19 | plugins: [ 20 | new webpack.HotModuleReplacementPlugin(), 21 | new webpack.NoErrorsPlugin(), 22 | new HtmlWebpackPlugin({ template: './src/index.html' }) 23 | ], 24 | resolve: { 25 | extensions: ['', '.js', '.jsx'] 26 | }, 27 | module: { 28 | loaders: [ 29 | { 30 | test: /\.jsx?$/, 31 | loaders: ['react-hot', 'babel'], 32 | include: SRC_DIR 33 | } 34 | ] 35 | } 36 | } 37 | 38 | if (!PRODUCTION) { 39 | config.devtool = 'eval' 40 | } 41 | 42 | module.exports = config 43 | --------------------------------------------------------------------------------