├── .gitignore
├── LICENSE
├── README.md
├── index.html
├── package.json
├── screenshot.gif
├── server.js
├── src
├── App.js
├── TimeTravelList.js
├── index.js
├── time-travel
│ ├── TimeTravelList.js
│ ├── TimeTravelSlider.js
│ └── index.js
└── util.js
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 |
25 | # Dependency directory
26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
27 | node_modules
28 |
29 | .tern-port
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Young
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React-Bacon-Timetravel-Example
2 | React.js + Bacon.js with Timetravel Example
3 |
4 | 
5 |
6 | # Usage
7 |
8 | ```
9 | npm install
10 | npm start
11 | open http://localhost:3000
12 | ```
13 |
14 | # Lisence
15 |
16 | MIT
17 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Sample App
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "React-Bacon-Timetravel-Example",
3 | "version": "1.0.0",
4 | "description": "React.js + Bacon.js with Timetravel Example",
5 | "scripts": {
6 | "start": "node server.js",
7 | "lint": "eslint src"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/littlehaker/React-Bacon-Timetravel-Example.git"
12 | },
13 | "keywords": [
14 | "react",
15 | "reactjs",
16 | "boilerplate",
17 | "hot",
18 | "reload",
19 | "hmr",
20 | "live",
21 | "edit",
22 | "webpack"
23 | ],
24 | "author": "Uno Young ",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/littlehaker/React-Bacon-Timetravel-Example/issues"
28 | },
29 | "homepage": "https://github.com/littlehaker/React-Bacon-Timetravel-Example",
30 | "devDependencies": {
31 | "baconjs": "^0.7.71",
32 | "react-hyperscript": "^2.1.0",
33 | "babel-core": "^5.4.7",
34 | "babel-eslint": "^3.1.9",
35 | "babel-loader": "^5.1.2",
36 | "eslint-plugin-react": "^2.3.0",
37 | "react-hot-loader": "^1.3.0",
38 | "webpack": "^1.9.6",
39 | "webpack-dev-server": "^1.8.2"
40 | },
41 | "dependencies": {
42 | "react": "^0.13.3"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/screenshot.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlehaker/React-Bacon-Timetravel-Example/bde3a6e67b71f6ce4f428e70e7a1c692f15cd5b6/screenshot.gif
--------------------------------------------------------------------------------
/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 | });
16 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import h from 'react-hyperscript';
3 | import Bacon from 'baconjs';
4 |
5 | export default class App extends Component {
6 | componentWillMount() {
7 | this.unsubscribe = this.props.state$.onValue((state) => {
8 | this.setState(state);
9 | });
10 | }
11 | componentWillUnmount() {
12 | this.unsubscribe();
13 | }
14 | dispatch(action_type, payload) {
15 | this.props.action$.push({
16 | type: action_type,
17 | payload: payload
18 | });
19 | }
20 | render() {
21 | return h('div', [
22 | h('span', 'Count: ' + this.state.count),
23 | h('button', {onClick: this.dispatch.bind(this, 'inc', {step: 10})}, '+'),
24 | h('button', {onClick: this.dispatch.bind(this, 'dec', {step: 1})}, '-'),
25 | ]);
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/src/TimeTravelList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import h from 'react-hyperscript';
3 | import Bacon from 'baconjs';
4 |
5 | export default class TimeTravelList extends Component {
6 | componentWillMount() {
7 | var state$ = Bacon.combineTemplate({
8 | actions: this.props.timetravel.actions$,
9 | states: this.props.timetravel.states$,
10 | index: this.props.timetravel.index$
11 | });
12 | this.unsubscribe = state$.onValue((state) => {
13 | this.setState(state);
14 | });
15 | }
16 | componentWillUnmount() {
17 | this.unsubscribe();
18 | }
19 | onClick(i) {
20 | this.props.timetravel.timelineAction$.push({type: 'goto', payload: {index: i}});
21 | }
22 | renderItem(item, i) {
23 | return h('li', {key: i, onClick: this.onClick.bind(this, i), className: i == this.state.index ? 'active': null}, [
24 | // (i + 1).toString(),
25 | // JSON.stringify(item),
26 | item.type,
27 | JSON.stringify(item.payload),
28 | JSON.stringify(this.state.states[i]),
29 | i == this.state.index ? '<' : null
30 | ]);
31 | }
32 | render() {
33 | return h('ol', [
34 | this.state.actions.map(this.renderItem.bind(this))
35 | ]);
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import App from './App';
3 | import h from 'react-hyperscript';
4 | import Bacon from 'baconjs';
5 | import { filterAction } from './util';
6 |
7 | import timeTravel, { TimeTravelList, TimeTravelSlider } from './time-travel/index';
8 |
9 | // Action
10 | let action$ = new Bacon.Bus();
11 |
12 | let count$ = Bacon.update(
13 | 0, // <-- Init value
14 | [action$.filter(filterAction('inc'))], (count, action) => count + action.payload.step, // <-- Reducer
15 | [action$.filter(filterAction('dec'))], (count, action) => count - action.payload.step
16 | );
17 |
18 | // Store
19 | let state$ = Bacon.combineTemplate({ // <-- Similar as combineReducers
20 | count: count$
21 | });
22 |
23 | let timetravel = timeTravel(state$, action$);
24 | state$ = timetravel.state$;
25 |
26 | React.render(h('div', [
27 | h(App, {state$, action$}),
28 | // TimeTravel widget
29 | h('hr'),
30 | h(TimeTravelSlider, {timetravel}),
31 | h('hr'),
32 | h(TimeTravelList, {timetravel})
33 | ]), document.getElementById('root'));
34 |
--------------------------------------------------------------------------------
/src/time-travel/TimeTravelList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import h from 'react-hyperscript';
3 | import Bacon from 'baconjs';
4 |
5 | export default class TimeTravelList extends Component {
6 | componentWillMount() {
7 | var state$ = Bacon.combineTemplate({
8 | actions: this.props.timetravel.actions$,
9 | states: this.props.timetravel.states$,
10 | index: this.props.timetravel.index$
11 | });
12 | this.unsubscribe = state$.onValue((state) => {
13 | this.setState(state);
14 | });
15 | }
16 | componentWillUnmount() {
17 | this.unsubscribe();
18 | }
19 | onClick(i) {
20 | this.props.timetravel.timelineAction$.push({type: 'goto', payload: {index: i}});
21 | }
22 | renderItem(item, i) {
23 | return h('li', {
24 | key: i,
25 | onClick: this.onClick.bind(this, i)
26 | }, [
27 | `Action ${item.type} ${JSON.stringify(item.payload)} => Store ${JSON.stringify(this.state.states[i])} ${i == this.state.index ? '<' : ''}`
28 | ]);
29 | }
30 | render() {
31 | return h('ol', [
32 | this.state.actions.map(this.renderItem.bind(this))
33 | ]);
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/src/time-travel/TimeTravelSlider.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import h from 'react-hyperscript';
3 | import Bacon from 'baconjs';
4 |
5 | export default class TimeTravelSlider extends Component {
6 | componentWillMount() {
7 | let state$ = Bacon.combineTemplate({
8 | actions: this.props.timetravel.actions$,
9 | index: this.props.timetravel.index$
10 | });
11 | this.unsubscribe = state$.onValue((state) => this.setState(state));
12 | }
13 | componentWillUnmount() {
14 | this.unsubscribe();
15 | }
16 | onChange(e) {
17 | this.props.timetravel.timelineAction$.push({
18 | type: 'goto',
19 | payload: {
20 | index: e.currentTarget.value
21 | }
22 | });
23 | }
24 | render() {
25 | return h('div', [
26 | h('input', {type: 'range', min: 0, max: this.state.actions.length - 1, value: this.state.index, onChange: this.onChange.bind(this)})
27 | ]);
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/src/time-travel/index.js:
--------------------------------------------------------------------------------
1 | import Bacon from 'baconjs';
2 | import { filterAction } from '../util';
3 |
4 | import TimeTravelList from './TimeTravelList';
5 | import TimeTravelSlider from './TimeTravelSlider';
6 |
7 | export default function timeTravel(state$, action$) {
8 | let states$ = state$.scan([], (states, state) => states.concat(state));
9 | let timelineAction$ = new Bacon.Bus();
10 |
11 | let actions$ = Bacon.update(
12 | [{type: '@@init', payload: {}}],
13 | [action$], (actions, action) => actions.concat(action)
14 | );
15 |
16 | let index$ = Bacon.update(
17 | 0,
18 | [timelineAction$.filter(filterAction('goto'))], (index, timelineAction) => timelineAction.payload.index,
19 | [actions$, action$], (index, actions) => {
20 | if (index == actions.length - 2) {
21 | return actions.length - 1;
22 | } else {
23 | return index;
24 | }
25 | }
26 | );
27 |
28 | let computedState$ = Bacon.combineWith((states, index) => states[index], states$, index$);
29 |
30 | return {
31 | state$: computedState$,
32 | states$,
33 | index$,
34 | actions$,
35 | timelineAction$,
36 | };
37 | };
38 |
39 | export { TimeTravelSlider, TimeTravelList };
40 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | function filterAction(action_type) {
2 | return function(action) {
3 | return action.type === action_type;
4 | };
5 | }
6 |
7 | module.exports.filterAction = filterAction;
8 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 |
4 | module.exports = {
5 | devtool: 'eval',
6 | entry: [
7 | 'webpack-dev-server/client?http://localhost:3000',
8 | 'webpack/hot/only-dev-server',
9 | './src/index'
10 | ],
11 | output: {
12 | path: path.join(__dirname, 'dist'),
13 | filename: 'bundle.js',
14 | publicPath: '/static/'
15 | },
16 | plugins: [
17 | new webpack.HotModuleReplacementPlugin()
18 | ],
19 | module: {
20 | loaders: [{
21 | test: /\.js$/,
22 | loaders: ['react-hot', 'babel'],
23 | include: path.join(__dirname, 'src')
24 | }]
25 | }
26 | };
27 |
--------------------------------------------------------------------------------