├── .babelrc ├── .gitignore ├── index.html ├── package.json ├── server.js ├── src ├── actions.js ├── index.js ├── rx-extensions.js ├── store.js └── views │ ├── about.js │ ├── app.js │ ├── home.js │ ├── inbox.js │ ├── main.js │ └── profile.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "loose": ["all"] 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.backup 2 | .idea 3 | *.iml 4 | node_modules 5 | build -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rx-react-flux", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "babel-core": "latest", 13 | "babel-loader": "latest", 14 | "component-inspector": "^1.2.0", 15 | "react-hot-loader": "latest", 16 | "webpack": "latest", 17 | "webpack-dev-server": "latest" 18 | }, 19 | "dependencies": { 20 | "falcor": "latest", 21 | "history": "latest", 22 | "immutable": "latest", 23 | "lodash": "latest", 24 | "lodash-fp": "latest", 25 | "ramda": "latest", 26 | "react": "latest", 27 | "react-dom": "latest", 28 | "react-loader": "latest", 29 | "react-router": "latest", 30 | "rx": "latest" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var WebpackDevServer = require('webpack-dev-server'); 3 | var config = require('./webpack.config'); 4 | 5 | new WebpackDevServer(webpack(config), { 6 | publicPath: config.output.publicPath, 7 | hot: true, 8 | historyApiFallback: true 9 | }).listen(3000, 'localhost', function (err, result) { 10 | if (err) { 11 | console.log(err); 12 | } 13 | 14 | console.log('Listening at localhost:3000'); 15 | }); -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | import {ReplaySubject} from 'rx'; 2 | 3 | export default { 4 | changeFirstName: new ReplaySubject(), 5 | changeLastName: new ReplaySubject(), 6 | changeCountry: new ReplaySubject(), 7 | addFriend: new ReplaySubject(), 8 | removeFriend: new ReplaySubject(), 9 | save: new ReplaySubject(), 10 | undo: new ReplaySubject(), 11 | redo: new ReplaySubject() 12 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'react-dom'; 3 | import { createHistory, useBasename } from 'history' 4 | 5 | import Person from './views/main'; 6 | import About from './views/about'; 7 | import Inbox from './views/inbox'; 8 | import Home from './views/home'; 9 | import App from './views/app'; 10 | 11 | import Store from './store'; 12 | 13 | import {Router, Route, IndexRoute} from 'react-router'; 14 | 15 | const routes = ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | 27 | 28 | render(routes, document.getElementById('app')); -------------------------------------------------------------------------------- /src/rx-extensions.js: -------------------------------------------------------------------------------- 1 | import Rx from 'rx'; 2 | import curryRight from 'lodash/function/curryRight'; 3 | 4 | /** 5 | * 6 | */ 7 | export default class RxExtensions extends Rx.Observable { 8 | constructor(...args) { 9 | super(args); 10 | } 11 | 12 | /** 13 | * 14 | * @param args 15 | * @returns {Rx.Observable} 16 | */ 17 | static when(...args) { 18 | return super.zip( 19 | ...super.from(args).partition((_, i) => i % 2 === 0), 20 | (stream, callback) => ({stream, callback}) 21 | ).toArray() 22 | .flatMapLatest(pairs => 23 | super.when(...pairs.map(p => 24 | ((Array.isArray(p.stream) ? p.stream : [p.stream]).reduce((prev, next) => prev.and(next))) 25 | .thenDo(...args => p.callback(...args)) 26 | ) 27 | ) 28 | ).publish().refCount(); 29 | } 30 | 31 | /** 32 | * 33 | * @param initial 34 | * @param args 35 | * @returns {Rx.Observable|Rx.Observable} 36 | */ 37 | static update(initial, ...args) { 38 | return super.zip( 39 | ...super.from(args).partition((_, i) => i % 2 === 0), 40 | (stream, callback) => ({stream, callback}) 41 | ).toArray() 42 | .flatMapLatest(pairs => 43 | super.when(...pairs.map(p => 44 | ((Array.isArray(p.stream) ? p.stream : [p.stream]).reduce((prev, next) => prev.and(next))) 45 | .thenDo(curryRight((prev, ...args) => p.callback(...[prev, ...args]), 2)) 46 | ) 47 | ) 48 | ).startWith(initial).scan((prev, f) => f(prev)).publish().refCount(); 49 | } 50 | } -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import {update, fromPromise, fromEvent, from} from './rx-extensions'; 2 | import {fromJS} from 'immutable'; 3 | import {changeFirstName, changeLastName, changeCountry, addFriend, removeFriend, save, undo, redo} from './actions'; 4 | 5 | let defaultPerson = { 6 | firstName: 'Denis', 7 | lastName: 'Stoyanov', 8 | country: { 9 | name: 'UA' 10 | }, 11 | friends: ['Alex'] 12 | }; 13 | 14 | const emulateAjaxWithDelay = (key) => new Promise(resolve => 15 | setTimeout(() => resolve(fromJS(JSON.parse(localStorage.getItem(key)) || defaultPerson)), 1000) 16 | ); 17 | 18 | const saveToLocalStorage = (key, value) => { 19 | localStorage.setItem(key, JSON.stringify(value.toJSON())); 20 | 21 | return fromJS(JSON.parse(localStorage.getItem(key, value))); 22 | }; 23 | 24 | export default (key) => fromPromise(emulateAjaxWithDelay(key)) 25 | .merge(fromEvent(window, 'storage').map(({newValue}) => fromJS(JSON.parse(newValue)))) 26 | .flatMap(p => update(p, 27 | changeFirstName, (person, firstName) => person.set('firstName', firstName), 28 | changeLastName, (person, lastName) => person.set('lastName', lastName), 29 | changeCountry, (person, country) => person.setIn(['country', 'name'], country), 30 | addFriend, (person, friend) => person.set('friends', person.get('friends').push(friend)), 31 | removeFriend, (person, friendIndex) => person.set('friends', person.get('friends').splice(friendIndex, 1)), 32 | save.debounce(200), (person, _) => saveToLocalStorage('person', person), 33 | undo, (person, historyPerson) => historyPerson, 34 | redo, (person, futurePerson) => futurePerson 35 | ) 36 | ) -------------------------------------------------------------------------------- /src/views/about.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => (

About

); -------------------------------------------------------------------------------- /src/views/app.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Link} from 'react-router'; 3 | 4 | export default class extends Component { 5 | constructor() { 6 | super(arguments); 7 | } 8 | 9 | render() { 10 | return ( 11 |
12 | 27 | {this.props.children} 28 |
29 | ); 30 | } 31 | } -------------------------------------------------------------------------------- /src/views/home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => (

Home

) -------------------------------------------------------------------------------- /src/views/inbox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => (

Inbox

); -------------------------------------------------------------------------------- /src/views/main.js: -------------------------------------------------------------------------------- 1 | import {CompositeDisposable} from 'rx'; 2 | import I, {Map, List} from 'immutable'; 3 | import React, {Component, PropTypes} from 'react'; 4 | import Loader from 'react-loader'; 5 | import Actions from '../actions'; 6 | import Store from '../store'; 7 | import ViewProfile from './profile'; 8 | 9 | class View extends Component { 10 | static propTypes = { 11 | person: PropTypes.object.isRequired, 12 | loaded: PropTypes.bool.isRequired, 13 | friend: PropTypes.string.isRequired, 14 | history: PropTypes.object.isRequired, 15 | future: PropTypes.object.isRequired, 16 | removeFriend: PropTypes.func.isRequired, 17 | addFriend: PropTypes.func.isRequired, 18 | undo: PropTypes.func.isRequired, 19 | redo: PropTypes.func.isRequired, 20 | handleChange: PropTypes.func.isRequired, 21 | onSave: PropTypes.func.isRequired 22 | }; 23 | 24 | render() { 25 | let {person, loaded, friend, history, future, removeFriend, addFriend, undo, redo, handleChange, onSave} = this.props; 26 | 27 | if(!person || !(person instanceof Map) || loaded) { 28 | let options = { 29 | lines: 17, 30 | length: 20, 31 | width: 15, 32 | radius: 50, 33 | corners: 1, 34 | rotate: 0, 35 | direction: 1, 36 | color: '#000', 37 | speed: 1, 38 | trail: 60, 39 | shadow: false, 40 | hwaccel: false, 41 | zIndex: 2e9, 42 | top: '50%', 43 | left: '50%', 44 | scale: 1.00 45 | }; 46 | 47 | return
; 48 | } else { 49 | let friendsView = (friends) => { 50 | if(friends.length > 0) { 51 | return (
52 |
53 |

Your Friends

54 |
55 |
    56 | { 57 | friends.map((friend, i) => 58 |
  • 59 | {friend} 60 | 61 |
  • 62 | ) 63 | } 64 |
65 |
66 |
67 | Friends {friends.length} 68 |
69 |
70 |
); 71 | } else { 72 | return (
No friends.
); 73 | } 74 | }; 75 | 76 | return (
77 |
78 |
79 |
80 |
81 |
82 |
83 | 84 |
85 |
86 | 87 |
88 |
89 |
90 |
91 | 92 |
93 | 98 |
99 |
100 |
101 | 102 |
103 | 108 |
109 |
110 |
111 | 112 |
113 | 118 |
119 |
120 |
121 | 122 |
123 |
124 |
125 |
126 | 132 |
133 |
134 |
135 |
136 | 139 |
140 |
141 | { friendsView(person.get('friends').toArray()) } 142 |
143 |
144 |
145 |
146 |
147 | 148 |
149 |
150 |
151 |
152 |
); 153 | } 154 | } 155 | } 156 | 157 | export default class extends Component { 158 | state = { 159 | person: {}, 160 | friend: "", 161 | loaded: false, 162 | history: List(), 163 | future: List() 164 | }; 165 | 166 | constructor(props) { 167 | super(props); 168 | this.loadData(); 169 | } 170 | 171 | loadData = () => { 172 | this.disposables = new CompositeDisposable(); 173 | 174 | this.disposables.add(Store('person').subscribe(person => 175 | this.setState({ 176 | person, 177 | loaded: false 178 | }) 179 | )); 180 | }; 181 | 182 | onSave = (e) => { 183 | e.preventDefault(); 184 | this.setState({ 185 | loaded: true, 186 | history: new List(), 187 | future: new List() 188 | }); 189 | Actions.save.onNext(); 190 | }; 191 | 192 | undo = (e) => { 193 | e.preventDefault(); 194 | if (this.state.history.size < 1) return; 195 | this.setState({ 196 | history: this.state.history.pop(), 197 | future: this.state.future.push(this.state.person) 198 | }); 199 | Actions.undo.onNext(this.state.history.last()); 200 | }; 201 | 202 | redo = (e) => { 203 | e.preventDefault(); 204 | if (this.state.future.size < 1) return; 205 | this.setState({ 206 | history: this.state.history.push(this.state.person), 207 | future: this.state.future.pop() 208 | }); 209 | Actions.redo.onNext(this.state.future.last()); 210 | }; 211 | 212 | handleChange = (e) => { 213 | let {name, value}= e.target; 214 | 215 | if(name !== 'friend') { 216 | this.setState({ 217 | history: this.state.history.push(this.state.person) 218 | }); 219 | } 220 | 221 | switch(name) { 222 | case 'firstName': 223 | Actions.changeFirstName.onNext(value); 224 | break; 225 | case 'lastName': 226 | Actions.changeLastName.onNext(value); 227 | break; 228 | case 'countryName': 229 | Actions.changeCountry.onNext(value); 230 | break; 231 | case 'friend': 232 | this.setState({friend: value}); 233 | break; 234 | } 235 | }; 236 | 237 | addFriend = () => { 238 | this.setState({ 239 | history: this.state.history.push(this.state.person) 240 | }); 241 | 242 | Actions.addFriend.onNext(this.state.friend); 243 | this.setState({friend: ""}); 244 | }; 245 | 246 | removeFriend = (friendIndex) => { 247 | this.setState({ 248 | history: this.state.history.push(this.state.person) 249 | }); 250 | Actions.removeFriend.onNext(friendIndex) 251 | }; 252 | 253 | render() { 254 | return 262 | } 263 | } -------------------------------------------------------------------------------- /src/views/profile.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Map} from 'immutable'; 3 | 4 | export default class extends Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render() { 10 | if(this.props.person instanceof Map) { 11 | return ( 12 |
13 |

14 | FirstName { this.props.person.get('firstName') }
15 | LastName { this.props.person.get('lastName') }
16 | Country { this.props.person.get('country').get('name') } 17 |

18 |

Friends

19 |
    20 | {this.props.person.get('friends').toArray().map((friend, i) => (
  • {friend}
  • ))} 21 |
22 |
23 | ); 24 | } else { 25 | return (
Loading...
); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | cache: true, 6 | target: 'web', 7 | debug: true, 8 | watch: true, 9 | devtool: '#inline-source-map', 10 | entry: [ 11 | 'webpack-dev-server/client?http://localhost:3000', 12 | 'webpack/hot/only-dev-server', 13 | './src/index' 14 | ], 15 | 16 | output: { 17 | path: path.join(__dirname, 'build'), 18 | filename: 'bundle.js', 19 | publicPath: '/static/' 20 | }, 21 | 22 | plugins: [ 23 | new webpack.HotModuleReplacementPlugin(), 24 | new webpack.NoErrorsPlugin() 25 | ], 26 | 27 | module: { 28 | loaders: [{ 29 | test: function (filename) { 30 | if (filename.indexOf('node_modules') !== -1) { 31 | return false; 32 | } else { 33 | return /\.js$/.test(filename) !== -1; 34 | } 35 | }, 36 | loaders: ['react-hot', 'babel'] 37 | }] 38 | } 39 | }; --------------------------------------------------------------------------------