├── .gitignore
├── README.md
├── gulpfile.js
├── package.json
└── src
├── assets
└── product.png
├── index.html
└── js
├── actions
└── app-actions.js
├── components
├── app-template.js
├── app.js
├── cart
│ ├── app-cart.js
│ ├── app-decreaseitem.js
│ ├── app-increaseitem.js
│ └── app-removefromcart.js
├── catalog
│ ├── app-addtocart.js
│ ├── app-catalog.js
│ └── app-catalogitem.js
├── header
│ ├── app-cartsummary.js
│ └── app-header.js
└── product
│ └── app-catalogdetail.js
├── constants
└── app-constants.js
├── dispatchers
└── app-dispatcher.js
├── main.js
├── mixins
└── StoreWatchMixin.js
└── stores
└── app-store.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | .idea/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | ## Egghead React Flux Example App
4 |
5 | This app requires node.js!
6 |
7 | With node installed, you will also need to install **gulp** globally:
8 |
9 | `npm i -g gulp`
10 |
11 | Now, from the project directory of the master branch install the local dependencies:
12 |
13 | `npm install`
14 |
15 | Now run `gulp` in the project folder. This builds the project in the `dist` folder and watches for any changes. You can serve the `dist` folder. httpster is a great option for this (`npm i -g httpster`).
16 |
17 | Find the [React Flux Architecture video lesson series on egghead.io](https://egghead.io/series/react-flux-architecture).
18 |
19 |
20 | You can switch branches to switch to a particular lesson:
21 |
22 | * [Lesson 1: Development Environment Setup](https://egghead.io/lessons/react-development-environment-setup)
23 | * `git checkout 01-dev-enviroment-setup`
24 | * [Lesson 2: Overview and Dispatchers](https://egghead.io/lessons/react-flux-overview-and-dispatchers)
25 | * `git checkout 02-dispatchers`
26 | * [Lesson 3: Actions](https://egghead.io/lessons/react-actions)
27 | * `git checkout 03-actions`
28 | * [Lesson 4: Stores](https://egghead.io/lessons/react-flux-stores)
29 | * `git checkout 04-stores`
30 | * [Lesson 5: Component/Views](https://egghead.io/lessons/react-flux-components-views)
31 | * `git checkout 05-components-views`
32 | * [Lesson 6: Project Organization](https://egghead.io/lessons/react-react-flux-project-organization)
33 | * `git checkout 06-project-organization`
34 | * [Lesson 7: Routing with react-router-component](https://egghead.io/lessons/react-react-flux-routing-with-react-router-component)
35 | * `git checkout 07-routing`
36 | * [Lesson 8: Remove Duplicate Code with Mixins](https://egghead.io/lessons/react-react-flux-remove-duplicate-code-with-mixins)
37 | * `git checkout 08-mixins`
38 | * [Lesson 9: Wrapping Up](https://egghead.io/lessons/react-react-flux-wrapping-up)
39 | * `git checkout 09-wrap-up`
40 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var browserify = require('browserify');
3 | var reactify = require('reactify');
4 | var source = require('vinyl-source-stream');
5 |
6 | gulp.task('browserify', function() {
7 | browserify('./src/js/main.js')
8 | .transform('reactify')
9 | .bundle()
10 | .pipe(source('main.js'))
11 | .pipe(gulp.dest('dist/js'));
12 | });
13 |
14 | gulp.task('copy',function() {
15 | gulp.src('src/index.html')
16 | .pipe(gulp.dest('dist'));
17 | gulp.src('src/assets/**/*.*')
18 | .pipe(gulp.dest('dist/assets'));
19 | });
20 |
21 | gulp.task('default',['browserify', 'copy'], function() {
22 | return gulp.watch('src/**/*.*', ['browserify', 'copy'])
23 | });
24 |
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "egghead-flux-example",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "gulpfile.js",
6 | "dependencies": {
7 | "browserify": "^10.2.6",
8 | "envify": "^3.4.0",
9 | "flux": "^2.1.1",
10 | "gulp": "^3.9.0",
11 | "react": "^0.14.0",
12 | "react-async": "^2.1.0",
13 | "react-dom": "^0.14.0",
14 | "react-router-component": "^0.27.0",
15 | "reactify": "^1.1.1",
16 | "vinyl-source-stream": "^1.1.0"
17 | },
18 | "devDependencies": {},
19 | "scripts": {
20 | "test": "echo \"Error: no test specified\" && exit 1"
21 | },
22 | "author": "",
23 | "license": "ISC"
24 | }
--------------------------------------------------------------------------------
/src/assets/product.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eggheadio/egghead-react-flux-example/2ed06a5d289dbbce486de476c2225b16872dc406/src/assets/product.png
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Document
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/js/actions/app-actions.js:
--------------------------------------------------------------------------------
1 | var AppConstants = require('../constants/app-constants');
2 | var AppDispatcher = require('../dispatchers/app-dispatcher');
3 |
4 | var AppActions = {
5 | addItem: function(item){
6 | AppDispatcher.handleViewAction({
7 | actionType: AppConstants.ADD_ITEM,
8 | item: item
9 | })
10 | },
11 | removeItem: function(index){
12 | AppDispatcher.handleViewAction({
13 | actionType: AppConstants.REMOVE_ITEM,
14 | index: index
15 | })
16 | },
17 | increaseItem: function(index){
18 | AppDispatcher.handleViewAction({
19 | actionType: AppConstants.INCREASE_ITEM,
20 | index: index
21 | })
22 | },
23 | decreaseItem: function(index){
24 | AppDispatcher.handleViewAction({
25 | actionType: AppConstants.DECREASE_ITEM,
26 | index: index
27 | })
28 | }
29 | }
30 |
31 | module.exports = AppActions;
32 |
--------------------------------------------------------------------------------
/src/js/components/app-template.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var Header = require('./header/app-header.js');
3 |
4 | var Template = React.createClass({
5 | render:function(){
6 | return (
7 |
8 |
9 | {this.props.children}
10 |
11 | );
12 | }
13 | });
14 |
15 | module.exports = Template;
16 |
--------------------------------------------------------------------------------
/src/js/components/app.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var Catalog = require('./catalog/app-catalog');
3 | var Cart = require('./cart/app-cart');
4 | var Router = require('react-router-component');
5 | var CatalogDetail = require('./product/app-catalogdetail');
6 | var Template = require('./app-template.js');
7 | var Locations = Router.Locations;
8 | var Location = Router.Location;
9 |
10 | var App = React.createClass({
11 | render:function(){
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | }
22 | });
23 |
24 | module.exports = App;
25 |
--------------------------------------------------------------------------------
/src/js/components/cart/app-cart.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var AppStore = require('../../stores/app-store.js');
3 | var RemoveFromCart = require('./app-removefromcart.js');
4 | var Increase = require('./app-decreaseitem')
5 | var Decrease = require('./app-increaseitem')
6 | var StoreWatchMixin = require('../../mixins/StoreWatchMixin');
7 | var Link = require('react-router-component').Link
8 |
9 | function cartItems(){
10 | return {items: AppStore.getCart()}
11 | }
12 |
13 | var Cart = React.createClass({
14 | mixins:[StoreWatchMixin(cartItems)],
15 | render:function(){
16 | var total = 0;
17 | var items = this.state.items.map(function(item, i){
18 | var subtotal = item.cost * item.qty;
19 | total += subtotal;
20 | return (
21 |
22 | |
23 | {item.title} |
24 | {item.qty} |
25 |
26 |
27 |
28 | |
29 | ${subtotal} |
30 |
31 | );
32 | })
33 | return (
34 |
35 |
36 |
37 |
38 | |
39 | Item |
40 | Qty |
41 | |
42 | Subtotal |
43 |
44 |
45 |
46 | {items}
47 |
48 |
49 |
50 | Total |
51 | ${total} |
52 |
53 |
54 |
55 |
Continue Shopping
56 |
57 | )
58 | }
59 | });
60 |
61 | module.exports = Cart
62 |
--------------------------------------------------------------------------------
/src/js/components/cart/app-decreaseitem.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var AppActions = require('../../actions/app-actions');
3 |
4 | var DecreaseItem = React.createClass({
5 | handler: function(){
6 | AppActions.decreaseItem(this.props.index)
7 | },
8 | render:function(){
9 | return
10 | }
11 | });
12 |
13 | module.exports = DecreaseItem;
14 |
--------------------------------------------------------------------------------
/src/js/components/cart/app-increaseitem.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var AppActions = require('../../actions/app-actions');
3 |
4 | var IncreaseItem = React.createClass({
5 | handler: function(){
6 | AppActions.increaseItem(this.props.index)
7 | },
8 | render:function(){
9 | return
10 | }
11 | });
12 |
13 | module.exports = IncreaseItem;
14 |
--------------------------------------------------------------------------------
/src/js/components/cart/app-removefromcart.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var AppActions = require('../../actions/app-actions');
3 |
4 | var RemoveFromCart = React.createClass({
5 | handler: function(){
6 | AppActions.removeItem(this.props.index)
7 | },
8 | render:function(){
9 | return
10 | }
11 | });
12 |
13 | module.exports = RemoveFromCart;
14 |
--------------------------------------------------------------------------------
/src/js/components/catalog/app-addtocart.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var AppActions = require('../../actions/app-actions');
3 |
4 | var AddToCart = React.createClass({
5 | handler: function(){
6 | AppActions.addItem(this.props.item)
7 | },
8 | render:function(){
9 | return
10 | }
11 | });
12 |
13 | module.exports = AddToCart;
14 |
--------------------------------------------------------------------------------
/src/js/components/catalog/app-catalog.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var AppStore = require('../../stores/app-store.js');
3 | var AddToCart = require('./app-addtocart.js')
4 | var StoreWatchMixin = require('../../mixins/StoreWatchMixin');
5 | var CatalogItem = require('../catalog/app-catalogitem');
6 |
7 |
8 | function getCatalog(){
9 | return {items: AppStore.getCatalog()}
10 | }
11 |
12 | var Catalog = React.createClass({
13 | mixins:[StoreWatchMixin(getCatalog)],
14 | render:function(){
15 | var items = this.state.items.map(function(item){
16 | return
17 |
18 | })
19 | return (
20 |
21 | {items}
22 |
23 | )
24 | }
25 | });
26 |
27 | module.exports = Catalog
28 |
--------------------------------------------------------------------------------
/src/js/components/catalog/app-catalogitem.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var Link = require('react-router-component').Link;
3 | var AddToCart = require('./app-addtocart');
4 |
5 | var CatalogItem = React.createClass({
6 | render:function(){
7 | var itemStyle = {
8 | borderBottom:'1px solid #ccc',
9 | paddingBottom:15
10 | };
11 | return (
12 |
13 |
{this.props.item.title}
14 |

15 |
{this.props.item.summary}
16 |
${this.props.item.cost} {this.props.item.inCart && '(' + this.props.item.qty + ' in cart)'}
17 |
18 |
Learn More
19 |
20 |
21 |
22 |
23 | )
24 | }
25 | });
26 |
27 | module.exports = CatalogItem;
28 |
--------------------------------------------------------------------------------
/src/js/components/header/app-cartsummary.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var Link = require('react-router-component').Link;
3 | var AppStore = require('../../stores/app-store.js');
4 | var StoreWatchMixin = require('../../mixins/StoreWatchMixin');
5 |
6 | function cartTotals(){
7 | return AppStore.getCartTotals();
8 | }
9 |
10 | var CartSummary = React.createClass({
11 | mixins: [StoreWatchMixin(cartTotals)],
12 | render:function(){
13 | return (
14 |
15 |
16 | Cart Items: {this.state.qty} / ${this.state.total}
17 |
18 |
19 | );
20 | }
21 | });
22 |
23 | module.exports = CartSummary;
24 |
--------------------------------------------------------------------------------
/src/js/components/header/app-header.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var CartSummary = require('./app-cartsummary.js');
3 |
4 | var Header = React.createClass({
5 | render:function(){
6 | return (
7 |
8 |
Lets Shop
9 |
10 |
11 |
12 |
13 |
14 | );
15 | }
16 | });
17 |
18 | module.exports = Header;
19 |
--------------------------------------------------------------------------------
/src/js/components/product/app-catalogdetail.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var AppStore = require('../../stores/app-store.js');
3 | var AddToCart = require('../catalog/app-addtocart.js')
4 | var StoreWatchMixin = require('../../mixins/StoreWatchMixin');
5 | var Link = require('react-router-component').Link;
6 |
7 | function getCatalogItem(component){
8 | var thisItem;
9 | AppStore.getCatalog().forEach(function(item){
10 | if(item.id.toString() === component.props.item){
11 | thisItem = item
12 | }
13 | });
14 | return {item: thisItem}
15 | }
16 |
17 | var CatalogDetail = React.createClass({
18 | mixins:[StoreWatchMixin(getCatalogItem)],
19 | render:function(){
20 | return (
21 |
22 |
{this.state.item.title}
23 |

24 |
{this.state.item.description}
25 |
${this.state.item.cost} {this.state.item.inCart && '(' + this.state.item.qty + ' in cart)'}
26 |
27 |
28 |
Continue Shopping
29 |
30 |
31 | );
32 | }
33 | });
34 |
35 | module.exports = CatalogDetail;
36 |
--------------------------------------------------------------------------------
/src/js/constants/app-constants.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | ADD_ITEM: 'ADD_ITEM',
3 | REMOVE_ITEM: 'REMOVE_ITEM',
4 | INCREASE_ITEM: 'INCREASE_ITEM',
5 | DECREASE_ITEM: 'DECREASE_ITEM'
6 | };
7 |
--------------------------------------------------------------------------------
/src/js/dispatchers/app-dispatcher.js:
--------------------------------------------------------------------------------
1 | var Dispatcher = require('flux').Dispatcher;
2 | var assign = require('react/lib/Object.assign');
3 |
4 | var AppDispatcher = assign(new Dispatcher(), {
5 | handleViewAction: function(action){
6 | console.log('action', action);
7 | this.dispatch({
8 | source: 'VIEW_ACTION',
9 | action: action
10 | })
11 | }
12 | });
13 |
14 | module.exports = AppDispatcher;
15 |
--------------------------------------------------------------------------------
/src/js/main.js:
--------------------------------------------------------------------------------
1 | var App = require('./components/app');
2 | var React = require('react');
3 | var ReactDOM = require('react-dom');
4 |
5 | ReactDOM.render(, document.getElementById('main'));
--------------------------------------------------------------------------------
/src/js/mixins/StoreWatchMixin.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var AppStore = require('../stores/app-store');
3 |
4 | var StoreWatchMixin = function(cb){
5 | return {
6 | getInitialState:function(){
7 | return cb(this)
8 | },
9 | componentWillMount:function(){
10 | AppStore.addChangeListener(this._onChange)
11 | },
12 | componentWillUnmount:function(){
13 | AppStore.removeChangeListener(this._onChange)
14 | },
15 | _onChange: function(){
16 | this.setState(cb(this))
17 | }
18 | }
19 | }
20 |
21 | module.exports = StoreWatchMixin;
22 |
--------------------------------------------------------------------------------
/src/js/stores/app-store.js:
--------------------------------------------------------------------------------
1 | var AppDispatcher = require('../dispatchers/app-dispatcher');
2 | var AppConstants = require('../constants/app-constants');
3 | var assign = require('react/lib/Object.assign');
4 | var EventEmitter = require('events').EventEmitter;
5 |
6 | var CHANGE_EVENT = 'change';
7 |
8 | var _catalog = [];
9 |
10 | for(var i=1; i<9; i++){
11 | _catalog.push({
12 | 'id': 'Widget' + i,
13 | 'title':'Widget #' + i,
14 | 'summary': 'This is an awesome widget!',
15 | 'description': 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Ducimus, commodi.',
16 | 'cost': i,
17 | 'img': '/assets/product.png'
18 | });
19 | }
20 |
21 | var _cartItems = [];
22 |
23 | function _removeItem(index){
24 | _cartItems[index].inCart = false;
25 | _cartItems.splice(index, 1);
26 | }
27 |
28 | function _increaseItem(index){
29 | _cartItems[index].qty++;
30 | }
31 |
32 | function _decreaseItem(index){
33 | if(_cartItems[index].qty>1){
34 | _cartItems[index].qty--;
35 | }
36 | else {
37 | _removeItem(index);
38 | }
39 | }
40 |
41 | function _addItem(item){
42 | if(!item.inCart){
43 | item['qty'] = 1;
44 | item['inCart'] = true;
45 | _cartItems.push(item);
46 | }
47 | else {
48 | _cartItems.forEach(function(cartItem, i){
49 | if(cartItem.id===item.id){
50 | _increaseItem(i);
51 | }
52 | });
53 | }
54 | }
55 |
56 | function _cartTotals(){
57 | var qty =0, total = 0;
58 | _cartItems.forEach(function(cartItem){
59 | qty+=cartItem.qty;
60 | total+=cartItem.qty*cartItem.cost;
61 | });
62 | return {'qty': qty, 'total': total};
63 | }
64 |
65 | var AppStore = assign(EventEmitter.prototype, {
66 | emitChange: function(){
67 | this.emit(CHANGE_EVENT)
68 | },
69 |
70 | addChangeListener: function(callback){
71 | this.on(CHANGE_EVENT, callback)
72 | },
73 |
74 | removeChangeListener: function(callback){
75 | this.removeListener(CHANGE_EVENT, callback)
76 | },
77 |
78 | getCart: function(){
79 | return _cartItems
80 | },
81 |
82 | getCatalog: function(){
83 | return _catalog
84 | },
85 |
86 | getCartTotals: function(){
87 | return _cartTotals()
88 | },
89 |
90 | dispatcherIndex: AppDispatcher.register(function(payload){
91 | var action = payload.action; // this is our action from handleViewAction
92 | switch(action.actionType){
93 | case AppConstants.ADD_ITEM:
94 | _addItem(payload.action.item);
95 | break;
96 |
97 | case AppConstants.REMOVE_ITEM:
98 | _removeItem(payload.action.index);
99 | break;
100 |
101 | case AppConstants.INCREASE_ITEM:
102 | _increaseItem(payload.action.index);
103 | break;
104 |
105 | case AppConstants.DECREASE_ITEM:
106 | _decreaseItem(payload.action.index);
107 | break;
108 | }
109 |
110 | AppStore.emitChange();
111 |
112 | return true;
113 | })
114 |
115 | })
116 |
117 | module.exports = AppStore;
118 |
--------------------------------------------------------------------------------