├── static ├── bootstrap └── index.html ├── .gitignore ├── .eslintrc ├── README.md ├── src ├── App.jsx ├── BugAdd.jsx ├── BugFilter.jsx ├── BugEdit.jsx └── BugList.jsx ├── webpack.config.js ├── package.json └── server.js /static/bootstrap: -------------------------------------------------------------------------------- 1 | ../node_modules/bootstrap/dist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | static/app.bundle.js 3 | static/app.bundle.js.map 4 | static/vendor.bundle.js 5 | static/vendor.bundle.js.map 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "no-console": "off", 5 | "react/jsx-closing-bracket-location": [1, "after-props"], 6 | "padded-blocks": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mern-es6 2 | ES6 / Webpack version of react-tutorial-mern 3 | 4 | ### Install 5 | 6 | After cloning the git repo: 7 | 8 | `npm install` 9 | 10 | ### Build 11 | 12 | `npm run webpack` 13 | 14 | ### Build - development 15 | 16 | `npm run webpack-watch` 17 | 18 | This will not exit, and will watch for modified files and re-pack the bundles. 19 | 20 | ### Run 21 | 22 | `npm start` 23 | 24 | This will start the webserver on port 3000. 25 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bug Tracker - a React tutorial using MERN 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Router, Route, Redirect, hashHistory } from 'react-router'; 4 | 5 | import BugList from './BugList.jsx'; 6 | import BugEdit from './BugEdit.jsx'; 7 | 8 | const NoMatch = () =>

No match to the route

; 9 | 10 | ReactDOM.render( 11 | ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | ), 19 | document.getElementById('main') 20 | ); 21 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: { 6 | app: './src/App.jsx', 7 | vendor: ['react','react-dom','react-router','react-bootstrap'], 8 | }, 9 | output: { 10 | path: path.resolve(__dirname, 'static'), 11 | filename: 'app.bundle.js' 12 | }, 13 | plugins: [ 14 | new webpack.optimize.CommonsChunkPlugin( 15 | /* chunkName= */"vendor", 16 | /* filename= */"vendor.bundle.js") 17 | ], 18 | module: { 19 | loaders: [ 20 | { 21 | test: /\.js$/, 22 | loader: 'babel-loader', 23 | query: { 24 | presets: ['es2015'], 25 | plugins: ['transform-object-assign'] 26 | } 27 | }, 28 | { 29 | test: /\.jsx$/, 30 | loader: 'babel-loader', 31 | query: { 32 | presets: ['react','es2015'], 33 | plugins: ['transform-object-assign'] 34 | } 35 | }, 36 | ] 37 | }, 38 | stats: { 39 | colors: true 40 | }, 41 | devtool: 'source-map' 42 | }; 43 | 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mern-es6", 3 | "version": "1.0.0", 4 | "description": "React Tutorial using MERN stack - ES6/Webpack variation", 5 | "main": "webapp.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "webpack": "webpack", 9 | "webpack-watch": "webpack --watch", 10 | "lint": "eslint 'src/*.jsx'" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/vasansr/mern-es6.git" 15 | }, 16 | "keywords": [ 17 | "react", 18 | "tutorial", 19 | "MERN", 20 | "ES6" 21 | ], 22 | "author": "Vasan Subramanian", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/vasansr/mern-es6/issues" 26 | }, 27 | "homepage": "https://github.com/vasansr/mern-es6#readme", 28 | "dependencies": { 29 | "body-parser": "^1.15.0", 30 | "bootstrap": "^3.3.6", 31 | "express": "^4.13.4", 32 | "mongodb": "^2.1.14" 33 | }, 34 | "devDependencies": { 35 | "babel-cli": "^6.6.5", 36 | "babel-core": "^6.7.4", 37 | "babel-loader": "^6.2.4", 38 | "babel-plugin-transform-object-assign": "^6.5.0", 39 | "babel-preset-es2015": "^6.6.0", 40 | "babel-preset-react": "^6.5.0", 41 | "eslint": "^2.7.0", 42 | "eslint-config-airbnb": "^6.2.0", 43 | "eslint-plugin-react": "^4.2.3", 44 | "jquery": "^2.2.2", 45 | "react": "^0.14.8", 46 | "react-addons-update": "^0.14.8", 47 | "react-bootstrap": "^0.29.2", 48 | "react-dom": "^0.14.8", 49 | "react-router": "^2.0.1", 50 | "webpack": "^1.12.14" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/BugAdd.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Panel, FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap'; 3 | 4 | /* 5 | * Todo: convert this to a modal 6 | */ 7 | export default class BugAdd extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | // no auto-binding. This is the recommended way, since it is bound only once per instance. 11 | this.handleSubmit = this.handleSubmit.bind(this); 12 | } 13 | 14 | handleSubmit(e) { 15 | console.log('Got submit:', e); 16 | e.preventDefault(); 17 | // This can't be a stateless since we'll need a ref for inputDomNode 18 | // Can't do getInputDOMNode using a ref, because there's no way to set the value 19 | // That's why one should prefer controlled forms. 20 | const form = document.forms.bugAdd; 21 | this.props.addBug({ owner: form.owner.value, title: form.title.value, 22 | status: 'New', priority: 'P1' }); 23 | // clear the form for the next input 24 | form.owner.value = ''; form.title.value = ''; 25 | } 26 | 27 | render() { 28 | console.log('Rendering BugAdd'); 29 | return ( 30 | 31 |
32 | 33 | But Title 34 | 35 | 36 | 37 | Owner 38 | 39 | 40 | 41 |
42 |
43 | ); 44 | } 45 | } 46 | 47 | BugAdd.propTypes = { 48 | addBug: React.PropTypes.func.isRequired, 49 | }; 50 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | let express = require('express'); 4 | let bodyParser = require('body-parser'); 5 | let MongoClient = require('mongodb').MongoClient; 6 | let ObjectId = require('mongodb').ObjectID; 7 | 8 | let app = express(); 9 | let db; 10 | 11 | app.use(express.static('static')); 12 | 13 | /* 14 | * Get a list of filtered records 15 | */ 16 | app.get('/api/bugs', function(req, res) { 17 | console.log("Query string", req.query); 18 | let filter = {}; 19 | if (req.query.priority) 20 | filter.priority = req.query.priority; 21 | if (req.query.status) 22 | filter.status = req.query.status; 23 | 24 | db.collection("bugs").find(filter).toArray(function(err, docs) { 25 | res.json(docs); 26 | }); 27 | }); 28 | 29 | app.use(bodyParser.json()); 30 | 31 | /* 32 | * Insert a record 33 | */ 34 | app.post('/api/bugs/', function(req, res) { 35 | console.log("Req body:", req.body); 36 | let newBug = req.body; 37 | db.collection("bugs").insertOne(newBug, function(err, result) { 38 | if (err) console.log(err); 39 | let newId = result.insertedId; 40 | db.collection("bugs").find({_id: newId}).next(function(err, doc) { 41 | if (err) console.log(err); 42 | res.json(doc); 43 | }); 44 | }); 45 | }); 46 | 47 | /* 48 | * Get a single record 49 | */ 50 | app.get('/api/bugs/:id', function(req, res) { 51 | db.collection("bugs").findOne({_id: ObjectId(req.params.id)}, function(err, bug) { 52 | res.json(bug); 53 | }); 54 | }); 55 | 56 | /* 57 | * Modify one record, given its ID 58 | */ 59 | app.put('/api/bugs/:id', function(req, res) { 60 | let bug = req.body; 61 | // ensure we don't have the _id itself as a field, it's disallowed to modfiy the 62 | // _id. 63 | delete (bug._id); 64 | console.log("Modifying bug:", req.params.id, bug); 65 | let oid = ObjectId(req.params.id); 66 | db.collection("bugs").updateOne({_id: oid}, bug, function(err, result) { 67 | if (err) console.log(err); 68 | db.collection("bugs").find({_id: oid}).next(function(err, doc) { 69 | if (err) console.log(err); 70 | res.send(doc); 71 | }); 72 | }); 73 | }); 74 | 75 | MongoClient.connect('mongodb://localhost/bugsdb', function(err, dbConnection) { 76 | db = dbConnection; 77 | let server = app.listen(3000, function() { 78 | let port = server.address().port; 79 | console.log("Started server at port", port); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/BugFilter.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Panel, Grid, Row, Col, FormGroup, ControlLabel, FormControl, Button, ButtonToolbar } 3 | from 'react-bootstrap'; 4 | 5 | export default class BugFilter extends React.Component { 6 | static get propTypes() { 7 | return { 8 | initFilter: React.PropTypes.object.isRequired, 9 | submitHandler: React.PropTypes.func.isRequired, 10 | }; 11 | } 12 | 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | status: this.props.initFilter.status, 17 | priority: this.props.initFilter.priority, 18 | }; 19 | this.submit = this.submit.bind(this); 20 | this.onChangeStatus = this.onChangeStatus.bind(this); 21 | this.onChangePriority = this.onChangePriority.bind(this); 22 | } 23 | 24 | componentWillReceiveProps(newProps) { 25 | if (newProps.initFilter.status === this.state.status 26 | && newProps.initFilter.priority === this.state.priority) { 27 | console.log('BugFilter: componentWillReceiveProps, no change'); 28 | return; 29 | } 30 | console.log('BugFilter: componentWillReceiveProps, new filter:', newProps.initFilter); 31 | this.setState({ status: newProps.initFilter.status, priority: newProps.initFilter.priority }); 32 | } 33 | 34 | onChangeStatus(e) { 35 | this.setState({ status: e.target.value }); 36 | } 37 | 38 | onChangePriority(e) { 39 | this.setState({ priority: e.target.value }); 40 | } 41 | 42 | submit(e) { 43 | e.preventDefault(); 44 | const newFilter = {}; 45 | if (this.state.priority) newFilter.priority = this.state.priority; 46 | if (this.state.status) newFilter.status = this.state.status; 47 | this.props.submitHandler(newFilter); 48 | } 49 | 50 | render() { 51 | console.log('Rendering BugFilter, state=', this.state); 52 | return ( 53 | 54 | 55 | 56 | 57 | 58 | Priority 59 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | Status 71 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |   83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/BugEdit.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import update from 'react-addons-update'; 3 | import { Link } from 'react-router'; 4 | 5 | import { Panel, FormGroup, FormControl, ControlLabel, Button, ButtonToolbar, Alert } 6 | from 'react-bootstrap'; 7 | 8 | export default class BugEdit extends React.Component { 9 | 10 | constructor(props) { 11 | super(props); 12 | this.submit = this.submit.bind(this); 13 | this.onChange = this.onChange.bind(this); 14 | this.dismissAlert = this.dismissAlert.bind(this); 15 | 16 | this.state = { successVisible: false, bug: {} }; 17 | } 18 | 19 | componentDidMount() { 20 | this.loadData(); 21 | } 22 | 23 | componentDidUpdate(prevProps) { 24 | console.log('BugEdit: componentDidUpdate', prevProps.params.id, this.props.params.id); 25 | if (this.props.params.id !== prevProps.params.id) { 26 | this.loadData(); 27 | } 28 | } 29 | 30 | onChange(e) { 31 | /* 32 | * Since state is immutable, we need a copy. If we modify this.state.bug itself and 33 | * set it as the new state, It will seem to work, but we'll 34 | * run into problems later, especially when comparing current and new state 35 | * within Lifecycle methods. 36 | */ 37 | const changes = {}; 38 | changes[e.target.name] = { $set: e.target.value }; 39 | const modifiedBug = update(this.state.bug, changes); 40 | /* 41 | * Without react-addons-update, this is how it could have been achieved: 42 | * 43 | var modifiedBug = Object.assign({}, this.state.bug); 44 | modifiedBug[e.target.name] = e.target.value; 45 | * 46 | * This works, but it doesn't scale well to deeply nested fields within the document. 47 | * 48 | */ 49 | 50 | this.setState({ bug: modifiedBug }); 51 | } 52 | 53 | loadData() { 54 | fetch(`/api/bugs/${this.props.params.id}`).then(response => response.json()).then(bug => { 55 | this.setState({ bug }); // all the attributes of the bug are top level state items 56 | }); 57 | } 58 | 59 | submit(e) { 60 | e.preventDefault(); 61 | 62 | fetch(`/api/bugs/${this.props.params.id}`, { 63 | method: 'PUT', 64 | headers: { 'Content-Type': 'application/json' }, 65 | body: JSON.stringify(this.state.bug), 66 | 67 | }).then(response => response.json()).then(bug => { 68 | this.setState({ bug }); 69 | this.setState({ successVisible: true }); 70 | this.dismissTimer = setTimeout(this.dismissAlert, 5000); 71 | }); 72 | } 73 | 74 | dismissAlert() { 75 | this.setState({ successVisible: false }); 76 | } 77 | 78 | render() { 79 | const success = ( 80 | 81 | Bug saved to DB successfully. 82 | 83 | ); 84 | const bug = this.state.bug; 85 | return ( 86 |
87 | 88 |
89 | 90 | Priority 91 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | Status 100 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | Title 109 | 110 | 111 | 112 | Owner 113 | 115 | 116 | 117 | 118 | Back 119 | 120 |
121 |
122 | {this.state.successVisible ? success : null} 123 |
124 | ); 125 | } 126 | } 127 | 128 | BugEdit.propTypes = { 129 | params: React.PropTypes.object.isRequired, 130 | }; 131 | 132 | -------------------------------------------------------------------------------- /src/BugList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import update from 'react-addons-update'; 3 | import { Link } from 'react-router'; 4 | 5 | import BugFilter from './BugFilter.jsx'; 6 | import BugAdd from './BugAdd.jsx'; 7 | 8 | /* 9 | * BugRow and BugTable are stateless, so they can be defined as pure functions 10 | * that only render. Both the following do the same, but with slightly different 11 | * styles. 12 | */ 13 | const BugRow = (props) => ( 14 | 15 | 16 | {/* Using ES6 string templates feature */} 17 | {props.bug._id} 18 | 19 | {props.bug.title} 20 | {props.bug.owner} 21 | {props.bug.status} 22 | {props.bug.priority} 23 | 24 | ); 25 | 26 | BugRow.propTypes = { 27 | bug: React.PropTypes.object.isRequired, 28 | }; 29 | 30 | function BugTable(props) { 31 | // console.log("Rendering bug table, num items:", props.bugs.length); 32 | let bugRows = props.bugs.map((bug, i) => ); 33 | return ( 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | {bugRows} 45 |
IdTitleOwnerStatusPriority
46 | ); 47 | } 48 | 49 | BugTable.propTypes = { 50 | bugs: React.PropTypes.array.isRequired, 51 | }; 52 | 53 | export default class BugList extends React.Component { 54 | /* 55 | * In ES6, static members can only be functions. What we're doing here is to define 56 | * an accessor, so that contextTypes appears as a member variable to its callers. 57 | * It's anyway a const so we don't need a setter. 58 | */ 59 | static get contextTypes() { 60 | return { router: React.PropTypes.object.isRequired }; 61 | } 62 | 63 | static get propTypes() { 64 | return { location: React.PropTypes.object.isRequired }; 65 | } 66 | 67 | constructor() { 68 | super(); 69 | /* 70 | * Using ES6 way of intializing state 71 | */ 72 | this.state = { bugs: [] }; 73 | /* 74 | * React on ES6 has no auto-binding. We have to bind each class method. Doing it in 75 | * the constructor is the recommended way, since it is bound only once per instance. 76 | * No need to bind loadData() since that's never called from an event, only from other 77 | * methods which are already bound. 78 | */ 79 | this.addBug = this.addBug.bind(this); 80 | this.changeFilter = this.changeFilter.bind(this); 81 | } 82 | 83 | componentDidMount() { 84 | console.log('BugList: componentDidMount'); 85 | this.loadData(); 86 | } 87 | 88 | componentDidUpdate(prevProps) { 89 | const oldQuery = prevProps.location.query; 90 | const newQuery = this.props.location.query; 91 | // todo: comparing shallow objects -- better way? 92 | // todo: when do we get called even when there's no change? 93 | if (oldQuery.priority === newQuery.priority && 94 | oldQuery.status === newQuery.status) { 95 | console.log('BugList: componentDidUpdate, no change in filter, not updating'); 96 | return; 97 | } 98 | console.log('BugList: componentDidUpdate, loading data with new filter'); 99 | this.loadData(); 100 | } 101 | 102 | loadData() { 103 | fetch(`/api/bugs/${this.props.location.search}`).then(response => 104 | response.json() 105 | ).then(bugs => { 106 | this.setState({ bugs }); 107 | }).catch(err => { 108 | console.log(err); 109 | // In a real app, we'd inform the user as well. 110 | }); 111 | } 112 | 113 | changeFilter(newFilter) { 114 | /* 115 | * 1.x of react-router does not support context.router. We'll need to do it 116 | * this way if we're using an earlier version of the react-router: 117 | * this.props.history.push({search: '?' + $.param(newFilter)}) 118 | */ 119 | 120 | /* 121 | * jQuery.param would have done this in one line for us, but we don't want 122 | * to include the entire library for just this. 123 | */ 124 | const search = Object.keys(newFilter) 125 | .filter(k => newFilter[k] !== '') 126 | .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(newFilter[k])}`) 127 | .join('&'); 128 | 129 | this.context.router.push({ search: `?${search}` }); 130 | } 131 | 132 | addBug(newBug) { 133 | console.log('Adding bug:', newBug); 134 | 135 | fetch('/api/bugs', { 136 | method: 'POST', 137 | headers: { 'Content-Type': 'application/json' }, 138 | body: JSON.stringify(newBug), 139 | 140 | }).then(res => res.json()).then(bug => { 141 | /* 142 | * We should not modify the state directly, it's immutable. So, we make a copy. 143 | * A deep copy is not required, since we are not modifying any bug. We are 144 | * only appending to the array, but we can't do a 'push'. If we do that, 145 | * any method referring to the current state will get wrong data. 146 | * In essence, the current state should show the old list of bugs, but the 147 | * new state should show the new list. 148 | */ 149 | // let modifiedBugs = this.state.bugs.concat(bug); 150 | /* 151 | * Earlier, we were supposed to use import react/addons, which is now deprecated 152 | * in favour of using import react-addons-{addon}, since this is more efficient 153 | * for bundlers such as browserify and webpack, even though the code mostly resides 154 | * within the react npm itself. 155 | */ 156 | const modifiedBugs = update(this.state.bugs, { $push: [bug] }); 157 | this.setState({ bugs: modifiedBugs }); 158 | 159 | }).catch(err => { 160 | // ideally, show error to user also. 161 | console.log('Error adding bug:', err); 162 | }); 163 | } 164 | 165 | render() { 166 | console.log('Rendering BugList, num items:', this.state.bugs.length); 167 | return ( 168 |
169 |

Bug Tracker

170 | 171 | 172 | 173 |
174 | ); 175 | } 176 | } 177 | 178 | --------------------------------------------------------------------------------