├── .gitignore ├── README.md ├── app ├── RestHelper.js ├── components │ ├── GroceryItem.jsx │ ├── GroceryItemList.jsx │ └── GroceryListAddItem.jsx ├── dispatcher.js ├── index.ejs ├── main.jsx ├── stores │ ├── GroceryItemActionCreator.jsx │ └── GroceryItemStore.jsx └── styles.css ├── bower.json ├── gulpfile.js ├── package.json ├── server ├── database.js ├── main.js ├── models │ └── GroceryItem.js ├── routes │ └── items.js └── seed.js └── snippets.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | test/coverage 4 | .tmp -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Usage Instructions 2 | 3 | #### To run the app, 4 | 1. Make sure `mongodb` is running in the background. 5 | 2. Install global dependencies with `npm install -g gulp bower react-tools browserify` 6 | 3. Install dependencies with `npm install; bower install` 7 | 4. Run app with `gulp serve` 8 | 9 | 10 | ### Glossary 11 | 12 | ## Dispatcher 13 | A simple emitter. Object register with the emitter to listen for an event, and when the event occurs, the objects are notified. 14 | Something tells the dispatcher it wants to listen for an event with a command called `register` or `on`. 15 | Here is an example: 16 | ```javascript 17 | exampleDispatcher.on('app-start',function(){alert("Hello")}); 18 | `setTimeout(function(){exampleDispatcher.emit('app-start')},1000);` // hello 19 | ``` 20 | The dispatcher's `on` or `register` function will either take two arguments, or take one argument and return a promise. The first argument is a string called the `type` of the listener. The `type` is basically the name of the event, like `sound-done` or `button-clicked`. The second argument, or the promise returned, is resolve every time the event occurs. 21 | Note that some promise architectures do not allow promises to be resolved more than once. To avoid this confusion, it is recommended that you use a callback. 22 | It is important to understand dispatcher are just a loose set of architecture rules and there are no hard and fast requirements. 23 | 24 | ## Store 25 | A utility that listens for particular kind of dispatcher events and is responsible for the handling of one element. 26 | For example, in an Address Book app, you would have a `Contacts` store. 27 | Stores fire change events, allow consumers to listen for change events, and allows direct interaction with the elements in the store. 28 | A contact store might, for example, have a function to add, edit or delete contacts that can be used by various components in your app. 29 | Stores usually contain a local copy of the database or part of the database, to allow for instant response to user interaction. Stores will then negotiate the update with a long-term storage solution in a custom manner. 30 | 31 | ## State 32 | A React component's state. A component can change it's own state, but not the state of other components. In order to change the state of another component, an app must make the appropriate request to a store. 33 | 34 | ## Props 35 | The properties of a React component. A component cannot change its own properties. The properties of a React component can only be set by its parent. In order to change the props of a component which is not the child of the caller, a request must be passed to the appropriate store. 36 | 37 | ## JSX 38 | A React-specific variant of JavaScript. JSX can be compiled into JavaScript code containing React components. JSX must be compiled as it cannot be read by web browsers. 39 | 40 | ### JSX Transformer 41 | A tool used to transform JSX without any backend. Can be used in the browser. Not used for production. 42 | 43 | ### Reactify 44 | A useful Browserify plugin which compiles JSX in to JavaScript as it is being browserified. 45 | -------------------------------------------------------------------------------- /app/RestHelper.js: -------------------------------------------------------------------------------- 1 | let $ = require('jquery'); 2 | 3 | module.exports = { 4 | get(url){ 5 | return new Promise(function(success,error){ 6 | $.ajax({ 7 | url:url, 8 | dataType:"json", 9 | success, 10 | error 11 | }); 12 | }); 13 | }, 14 | del(url){ 15 | return new Promise(function(success,error){ 16 | $.ajax({ 17 | url:url, 18 | type:'DELETE', 19 | success, 20 | error 21 | }) 22 | }) 23 | }, 24 | post(url,data){ 25 | return new Promise(function(success,error){ 26 | $.ajax({ 27 | url, 28 | type:'POST', 29 | data, 30 | success, 31 | error 32 | }) 33 | }) 34 | }, 35 | patch(url,data){ 36 | return new Promise(function(success,error){ 37 | $.ajax({ 38 | url, 39 | type:'PATCH', 40 | data, 41 | success, 42 | error 43 | }) 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/components/GroceryItem.jsx: -------------------------------------------------------------------------------- 1 | var dispatcher = require("./../dispatcher.js"); 2 | var groceryAction = require("./../stores/GroceryItemActionCreator.jsx"); 3 | var React = require('react/addons'); 4 | var cx = React.addons.classSet; 5 | 6 | module.exports = React.createClass({ 7 | 8 | togglePurchased:function(e){ 9 | e.preventDefault(); 10 | 11 | if (!this.props.item.purchased){ 12 | groceryAction.buy(this.props.item); 13 | } else { 14 | groceryAction.unbuy(this.props.item); 15 | } 16 | }, 17 | delete:function(e){ 18 | e.preventDefault(); 19 | groceryAction.delete(this.props.item); 20 | }, 21 | render:function(){ 22 | return ( 23 |
24 |
25 |

26 | {this.props.item.name} 27 |

28 |
29 |
30 | 31 |
32 |
33 | 34 |
35 |
36 | ) 37 | } 38 | }) 39 | -------------------------------------------------------------------------------- /app/components/GroceryItemList.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let GroceryItem = require('./GroceryItem.jsx'), 4 | GroceryListAddItem = require('./GroceryListAddItem.jsx'), 5 | React = require('react/addons'); 6 | 7 | module.exports = React.createClass({ 8 | 9 | render:function(){ 10 | return ( 11 |
12 | {this.props.items.map((item,index)=>{ 13 | return ( 14 | 15 | ) 16 | })} 17 | 18 |
19 | ) 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /app/components/GroceryListAddItem.jsx: -------------------------------------------------------------------------------- 1 | var groceryAction = require("./../stores/GroceryItemActionCreator.jsx"); 2 | let React = require('react/addons'); 3 | 4 | module.exports = React.createClass({ 5 | getInitialState: function(){ 6 | return { 7 | input:'' 8 | } 9 | }, 10 | addItem:function(e){ 11 | e.preventDefault(); 12 | 13 | groceryAction.add({ 14 | name: this.state.input 15 | }); 16 | 17 | this.setState({ 18 | input:'' 19 | }) 20 | }, 21 | handleInputName:function(e){ 22 | this.setState({input : e.target.value}) 23 | }, 24 | render:function(){ 25 | return ( 26 |
27 |
28 | 34 | 35 |
36 |
37 | ) 38 | } 39 | }) 40 | -------------------------------------------------------------------------------- /app/dispatcher.js: -------------------------------------------------------------------------------- 1 | var listeners = {}; 2 | let guid = require('guid'); 3 | 4 | var dispatcher = { 5 | register(callback){ 6 | var id = guid.raw(); 7 | listeners[id] = callback; 8 | return id; 9 | }, 10 | dispatch(payload){ 11 | console.info('Dispatching...',payload.type); 12 | for (var id in listeners){ 13 | var listener = listeners[id]; 14 | listener(payload); 15 | } 16 | } 17 | }; 18 | module.exports = dispatcher; 19 | -------------------------------------------------------------------------------- /app/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Grocery Listify 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |

Grocerrific App

14 |
15 | <%- reactOutput %> 16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/main.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let dispatcher = require('./dispatcher.js'); 4 | let GroceryItemList = require('./components/GroceryItemList.jsx'); 5 | let React = require('react/addons'); 6 | let GroceryItemStore = require('./stores/GroceryItemStore.jsx'); 7 | 8 | var items = GroceryItemStore.getGroceryItems(); 9 | 10 | GroceryItemStore.onChange(()=>{ 11 | items = GroceryItemStore.getGroceryItems(); 12 | render(); 13 | }) 14 | function render(){ 15 | React.render(,mount); 16 | } 17 | -------------------------------------------------------------------------------- /app/stores/GroceryItemActionCreator.jsx: -------------------------------------------------------------------------------- 1 | var dispatcher = require("./../dispatcher.js"); 2 | 3 | module.exports = { 4 | add:function(item){ 5 | dispatcher.dispatch({ 6 | type:"grocery-item:add", 7 | payload:item 8 | }) 9 | }, 10 | buy:function(item){ 11 | dispatcher.dispatch({ 12 | type:"grocery-item:buy", 13 | payload:item 14 | }) 15 | }, 16 | unbuy:function(item){ 17 | dispatcher.dispatch({ 18 | type:"grocery-item:unbuy", 19 | payload:item 20 | }) 21 | }, 22 | delete:function(item){ 23 | dispatcher.dispatch({ 24 | type:"grocery-item:delete", 25 | payload:item 26 | }); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /app/stores/GroceryItemStore.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let dispatcher = require("./../dispatcher.js"); 3 | let {get,post,del,patch} = require("./../RestHelper.js"); 4 | 5 | function GroceryItemStore(){ 6 | 7 | let groceryItems = [], 8 | changeListeners = []; 9 | 10 | function triggerListeners(){ 11 | changeListeners.forEach(function(listener){ 12 | listener(groceryItems) ; 13 | }) 14 | }; 15 | 16 | get("api/items") 17 | .then((data)=>{ 18 | groceryItems = data; 19 | triggerListeners(); 20 | }); 21 | 22 | 23 | function removeGroceryItem(item){ 24 | var index = groceryItems.findIndex(x => x._id===item._id); 25 | var removed = groceryItems.splice(index,1)[0]; 26 | triggerListeners(); 27 | 28 | del(`api/items/${item._id}`) 29 | .catch(()=>{ 30 | groceryItems.splice(index,0,removed); 31 | triggerListeners(); 32 | }) 33 | } 34 | 35 | function addGroceryItem(item){ 36 | var i = groceryItems.push(item); 37 | triggerListeners(); 38 | 39 | post("/api/items",item) 40 | .then((g)=>{ 41 | item._id = g._id; 42 | }) 43 | .catch(()=>{ 44 | groceryItems.splice(i,1); 45 | }) 46 | } 47 | 48 | function setGroceryItemBought(item, isPurchased){ 49 | var item = groceryItems.find(function(i){return i._id===item._id}); 50 | item.purchased = isPurchased || false;; 51 | triggerListeners(); 52 | 53 | patch(`api/items/${item._id}`,item); 54 | } 55 | 56 | function getGroceryItems(){ 57 | return groceryItems; 58 | }; 59 | 60 | function onChange(listener){ 61 | changeListeners.push(listener); 62 | } 63 | 64 | dispatcher.register(function(event){ 65 | var split = event.type.split(':'); 66 | if (split[0]==='grocery-item'){ 67 | switch(split[1]) { 68 | case "add": 69 | addGroceryItem(event.payload); 70 | break; 71 | case "delete": 72 | removeGroceryItem(event.payload); 73 | break; 74 | case "buy": 75 | setGroceryItemBought(event.payload, true); 76 | break; 77 | case "unbuy": 78 | setGroceryItemBought(event.payload, false); 79 | break; 80 | } 81 | } 82 | }) 83 | 84 | 85 | return { 86 | getGroceryItems:getGroceryItems, 87 | onChange:onChange 88 | } 89 | } 90 | 91 | module.exports = new GroceryItemStore(); 92 | -------------------------------------------------------------------------------- /app/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | color: darkslategray; 3 | background-color: #fffefd; 4 | } 5 | 6 | body { 7 | padding-top: 3rem; 8 | } 9 | 10 | .purchased-false { 11 | height: 40px; 12 | background-color: red; 13 | } 14 | 15 | .purchased-true { 16 | height: 40px; 17 | background-color: green; 18 | } 19 | 20 | .strikethrough { 21 | text-decoration: line-through; 22 | color: slategray; 23 | } 24 | 25 | h1 { 26 | text-decoration: underline; 27 | text-decoration-style: wavy; 28 | } 29 | 30 | .grocery-addItem { 31 | margin-top: 1rem; 32 | padding-top: 1rem; 33 | } 34 | 35 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-express-examplar", 3 | "version": "0.0.0", 4 | "authors": [ 5 | "Daniel Stern " 6 | ], 7 | "license": "MIT", 8 | "ignore": [ 9 | "**/.*", 10 | "node_modules", 11 | "bower_components", 12 | "test", 13 | "tests" 14 | ], 15 | "dependencies": { 16 | "react": "~0.13.3", 17 | "es6-shim": "~0.32.0", 18 | "jquery": "~2.1.4", 19 | "skeleton": "~2.0.4" 20 | }, 21 | "resolutions": { 22 | "es6-shim": "~0.31.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var browserSync = require('browser-sync'); 2 | var gulp = require('gulp'); 3 | var LiveServer = require('gulp-live-server'); 4 | var source = require('vinyl-source-stream'); 5 | var babelify = require('babelify'); 6 | var browserify = require('browserify'); 7 | var reactify = require('reactify'); 8 | 9 | gulp.task('live-server',function(){ 10 | var server = new LiveServer('server/main.js'); 11 | server.start(); 12 | }); 13 | 14 | gulp.task('bundle',function(){ 15 | return browserify({ 16 | entries:'app/main.jsx', 17 | // entries:'app/components/GroceryListApp.jsx', 18 | debug:true, 19 | }) 20 | .transform(babelify) 21 | .transform(reactify) 22 | .transform(babelify.configure({ 23 | stage:0, 24 | sourceMaps:true 25 | })) 26 | .bundle() 27 | .pipe(source('app.js')) 28 | .pipe(gulp.dest('./.tmp')); 29 | }); 30 | 31 | gulp.task('temp',function(){ 32 | gulp.src(['app/index.html','app/*.css']) 33 | .pipe(gulp.dest('./.tmp')); 34 | 35 | gulp.src(['bower_components/**']) 36 | .pipe(gulp.dest('./.tmp/bower_components')); 37 | }); 38 | 39 | gulp.task('bundle-n-reload',['bundle'],browserSync.reload) 40 | 41 | gulp.task('observe-all',function(){ 42 | gulp.watch('app/**/*.*',['bundle-n-reload']); 43 | gulp.watch('app/*.*',['temp']); 44 | gulp.watch('./server/**/*.js',['live-server']); 45 | }); 46 | 47 | 48 | gulp.task('serve', ['live-server','bundle','temp','observe-all'], function() { 49 | browserSync.init(null, { 50 | proxy: "http://localhost:7777", 51 | port: 9001 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-express-examplar", 3 | "version": "1.0.0", 4 | "description": "A simple emitter. Object register with the emitter to listen for an event, and when the event occurs, the objects are notified.\r Something tells the dispatcher it wants to listen for an event with a command called `register` or `on`. \r Here is an example:\r ```javascript\r exampleDispatcher.on('app-start',function(){alert(\"Hello\")});\r `setTimeout(function(){exampleDispatcher.emit('app-start')},1000);` // hello\r ```\r The dispatcher's `on` or `register` function will either take two arguments, or take one argument and return a promise. The first argument is a string called the `type` of the listener. The `type` is basically the name of the event, like `sound-done` or `button-clicked`. The second argument, or the promise returned, is resolve every time the event occurs.\r Note that some promise architectures do not allow promises to be resolved more than once. To avoid this confusion, it is recommended that you use a callback.\r It is important to understand dispatcher are just a loose set of architecture rules and there are no hard and fast requirements.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "babelify": "^6.1.1", 13 | "browser-sync": "^2.7.2", 14 | "browserify": "^10.2.1", 15 | "gulp": "^3.8.11", 16 | "gulp-live-server": "0.0.14", 17 | "reactify": "^1.1.1" 18 | }, 19 | "dependencies": { 20 | "babel": "^5.5.6", 21 | "body-parser": "^1.12.4", 22 | "cors": "^2.6.0", 23 | "ejs": "^2.3.1", 24 | "express": "^4.12.4", 25 | "guid": "0.0.12", 26 | "jquery": "^2.1.4", 27 | "mongoose": "^4.0.3", 28 | "node-jsx": "^0.13.3", 29 | "react": "^0.13.3", 30 | "react-tools": "^0.13.3", 31 | "vinyl-source-stream": "^1.1.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server/database.js: -------------------------------------------------------------------------------- 1 | let mongoose = require('mongoose'); 2 | 3 | mongoose.connect('mongodb://localhost/grocery',function(){ 4 | require('./seed.js'); 5 | }); 6 | -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | //"use strict"; 2 | 3 | var express = require('express'); 4 | var cors = require('cors'); 5 | var parser = require('body-parser'); 6 | var GroceryItem = require('./models/GroceryItem.js'); 7 | var React = require('react/addons'); 8 | require('babel/register'); 9 | 10 | require('./database.js'); 11 | 12 | var app = new express(); 13 | 14 | app.use(cors()) 15 | .use(parser.urlencoded({ extended: false })) 16 | .use(parser.json()) 17 | .get('/',function(req,res){ 18 | 19 | var app = React.createFactory(require('./../app/components/GroceryItemList.jsx')); 20 | GroceryItem.find(function(error,doc){ 21 | var generated = React.renderToString(app({ 22 | items:doc 23 | })); 24 | res.render('./../app/index.ejs',{reactOutput:generated}); 25 | }) 26 | }) 27 | .use(express.static(__dirname + '/../.tmp')) 28 | .listen(7777); 29 | 30 | require('./routes/items.js')(app); 31 | module.exports = app; 32 | -------------------------------------------------------------------------------- /server/models/GroceryItem.js: -------------------------------------------------------------------------------- 1 | //"use strict"; 2 | var mongoose = require('mongoose'); 3 | 4 | var GroceryItemSchema = { 5 | name:String, 6 | id:String, 7 | cost:Number, 8 | purchased:Boolean 9 | }; 10 | 11 | var GroceryItem = mongoose.model('GroceryItem',GroceryItemSchema,'groceryItems'); 12 | 13 | module.exports = GroceryItem; 14 | -------------------------------------------------------------------------------- /server/routes/items.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(app){ 3 | 4 | var GroceryItem = require('./../models/GroceryItem.js'); 5 | 6 | app.route('/api/items') 7 | .get(function(req,res){ 8 | GroceryItem.find(function(error,doc){ 9 | res.send(doc); 10 | }) 11 | }) 12 | .post(function(req,res){ 13 | var groceryItem = new GroceryItem(req.body); 14 | groceryItem.save(function(err,data){ 15 | if (err) { 16 | res.status(501).send(); 17 | } else { 18 | res.status(200).send(data); 19 | } 20 | }); 21 | ; 22 | }); 23 | 24 | app.route('/api/items/:id') 25 | .get(function(req,res){ 26 | GroceryItem.find({_id:req.params.id},function(error,doc){ 27 | if (error){ 28 | return res.status(404).send(); 29 | } 30 | 31 | res.status(200) 32 | .send(doc); 33 | }) 34 | }) 35 | .delete(function(req,res){ 36 | GroceryItem.find({_id:req.params.id}) 37 | .remove(function(){ 38 | res.status(202) 39 | .send(); 40 | }) 41 | }) 42 | .patch(function(req,res){ 43 | GroceryItem.findOne({ 44 | _id:req.body._id 45 | },function(err,doc){ 46 | if (!doc){ 47 | return res.status(404).send(); 48 | } 49 | 50 | for (var key in req.body){ 51 | doc[key] = req.body[key]; 52 | }; 53 | doc.save(); 54 | res.status(200).send(doc); 55 | }) 56 | 57 | }); 58 | 59 | } 60 | -------------------------------------------------------------------------------- /server/seed.js: -------------------------------------------------------------------------------- 1 | let mongoose = require('mongoose'); 2 | let GroceryItem = require('./models/GroceryItem.js'); 3 | 4 | mongoose.connection.db.dropDatabase(); 5 | 6 | var initial = [{ 7 | name:"Ice Cream" 8 | },{ 9 | name:"Waffles" 10 | },{ 11 | name:"Candy", 12 | purchased:true 13 | },{ 14 | name:"Snarks" 15 | }]; 16 | 17 | initial.forEach(function(item){ 18 | new GroceryItem(item).save(); 19 | }); 20 | -------------------------------------------------------------------------------- /snippets.md: -------------------------------------------------------------------------------- 1 | ## Watch Gulpfile.js only with Nodemon and rerun when updating 2 | `nodemon --watch gulpfile.js --exec "gulp serve"` --------------------------------------------------------------------------------