├── .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 ();
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 | };
--------------------------------------------------------------------------------