├── src ├── utils │ ├── .gitkeep │ └── defaultImport.js ├── containers │ ├── index.js │ ├── App.js │ ├── DevTools.js │ ├── CounterPage.js │ └── Root.js ├── index.js ├── routes.js ├── components │ ├── Main.js │ └── Counter.js ├── reducers │ ├── index.js │ └── counter.js ├── actions │ └── counter.js └── store │ └── configureStore.js ├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── index.html ├── .babelrc ├── .eslintrc ├── .editorconfig ├── test ├── actions │ └── counter.spec.js ├── reducers │ └── counter.spec.js ├── components │ └── Counter.spec.js └── containers │ └── CounterPage.spec.js ├── webpack.config.js ├── server.js ├── LICENSE ├── package.json └── README.md /src/utils/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | coverage 5 | dist 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.0.0" 4 | script: 5 | - npm run lint 6 | - npm test 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.0 (2015/x/x) 2 | 3 | #### Features 4 | 5 | - **A:** 6 | - **B:** 7 | 8 | #### Bugs Fixed 9 | 10 | - **C:** 11 | -------------------------------------------------------------------------------- /src/containers/index.js: -------------------------------------------------------------------------------- 1 | export { default as Root } from './Root' 2 | export { default as App } from './App' 3 | export { default as CounterPage } from './CounterPage' 4 | -------------------------------------------------------------------------------- /src/utils/defaultImport.js: -------------------------------------------------------------------------------- 1 | export default function defaultImport (module) { 2 | if (module.__esModule && module.default) { 3 | return module.default 4 | } else { 5 | return module 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Redux counter example 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/containers/App.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import Main from '../components/Main' 3 | 4 | function mapStateToProps (/* state */) { 5 | return {} 6 | } 7 | 8 | export default connect(mapStateToProps)(Main) 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"], 3 | "plugins": ["transform-object-rest-spread", "transform-class-properties"], 4 | "env": { 5 | "development": { 6 | "presets": ["react-hmre"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "parser": "babel-eslint", 4 | "ecmaFeatures": { 5 | "jsx": true 6 | }, 7 | "env": { 8 | "browser": true, 9 | "mocha": true, 10 | "node": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable*/ 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import Root from './containers/Root' 5 | import { browserHistory } from 'react-router' 6 | /*eslint-enable*/ 7 | 8 | ReactDOM.render( 9 | , 10 | document.getElementById('root') 11 | ) 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.{json,js,yml}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [.eslintrc] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable*/ 2 | import React from 'react' 3 | import { Route } from 'react-router' 4 | import App from './containers/App' 5 | import * as containers from './containers' 6 | /*eslint-enable*/ 7 | 8 | const { 9 | CounterPage 10 | } = containers 11 | 12 | export default ( 13 | 14 | 15 | 16 | ) 17 | -------------------------------------------------------------------------------- /src/components/Main.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | export default class Main extends Component { 4 | 5 | static propTypes = { 6 | children: PropTypes.any.isRequired 7 | }; 8 | 9 | render () { 10 | return ( 11 |
12 | {/* this will render the child routes */} 13 | {React.cloneElement(this.props.children, this.props)} 14 |
15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/containers/DevTools.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable*/ 2 | import React from 'react' 3 | import { createDevTools } from 'redux-devtools' 4 | import LogMonitor from 'redux-devtools-log-monitor' 5 | import DockMonitor from 'redux-devtools-dock-monitor' 6 | /*eslint-enable*/ 7 | 8 | export default createDevTools( 9 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /src/containers/CounterPage.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux' 2 | import { connect } from 'react-redux' 3 | import Counter from '../components/Counter' 4 | import * as CounterActions from '../actions/counter' 5 | 6 | function mapStateToProps (state) { 7 | return { 8 | count: state.counter.present.count 9 | } 10 | } 11 | 12 | function mapDispatchToProps (dispatch) { 13 | return bindActionCreators(CounterActions, dispatch) 14 | } 15 | 16 | export default connect(mapStateToProps, mapDispatchToProps)(Counter) 17 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import counter from './counter' 3 | import { 4 | INCREMENT_COUNTER, DECREMENT_COUNTER, 5 | UNDO_COUNTER, REDO_COUNTER 6 | } from '../actions/counter' 7 | import undoable, { includeAction } from 'redux-undo' 8 | 9 | const rootReducer = combineReducers({ 10 | counter: undoable(counter, { 11 | filter: includeAction([INCREMENT_COUNTER, DECREMENT_COUNTER]), 12 | limit: 10, 13 | debug: true, 14 | undoType: UNDO_COUNTER, 15 | redoType: REDO_COUNTER 16 | }) 17 | }) 18 | 19 | export default rootReducer 20 | -------------------------------------------------------------------------------- /src/actions/counter.js: -------------------------------------------------------------------------------- 1 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER' 2 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER' 3 | 4 | export const UNDO_COUNTER = 'UNDO_COUNTER' 5 | export const REDO_COUNTER = 'REDO_COUNTER' 6 | 7 | export function increment () { 8 | return { 9 | type: INCREMENT_COUNTER 10 | } 11 | } 12 | 13 | export function decrement () { 14 | return { 15 | type: DECREMENT_COUNTER 16 | } 17 | } 18 | 19 | export function undo () { 20 | return { 21 | type: UNDO_COUNTER 22 | } 23 | } 24 | 25 | export function redo () { 26 | return { 27 | type: REDO_COUNTER 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/reducers/counter.js: -------------------------------------------------------------------------------- 1 | import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions/counter' 2 | 3 | export default function counter (state = { count: 0 }, action) { 4 | switch (action.type) { 5 | case INCREMENT_COUNTER: 6 | // State mutations are bad, in dev mode, we detect them and throw an error. 7 | // Try it out by uncommenting the line below and running `npm run dev`! 8 | // state.mutation = true 9 | return { ...state, count: state.count + 1 } 10 | case DECREMENT_COUNTER: 11 | return { ...state, count: state.count - 1 } 12 | default: 13 | return state 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/actions/counter.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-expressions: 0 */ 2 | import { expect } from 'chai' 3 | import * as actions from '../../src/actions/counter' 4 | 5 | describe('actions', () => { 6 | it('increment should create increment action', () => { 7 | expect(actions.increment()).to.deep.equal({ type: actions.INCREMENT_COUNTER }) 8 | }) 9 | 10 | it('decrement should create decrement action', () => { 11 | expect(actions.decrement()).to.deep.equal({ type: actions.DECREMENT_COUNTER }) 12 | }) 13 | 14 | it('undo should create undo action', () => { 15 | expect(actions.undo()).to.deep.equal({ type: actions.UNDO_COUNTER }) 16 | }) 17 | 18 | it('redo should create redo action', () => { 19 | expect(actions.redo()).to.deep.equal({ type: actions.REDO_COUNTER }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'cheap-module-eval-source-map', 6 | entry: [ 7 | 'webpack-hot-middleware/client', 8 | './src/index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/static/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.HotModuleReplacementPlugin(), 18 | new webpack.NoErrorsPlugin(), 19 | new webpack.DefinePlugin({ 20 | __DEVTOOLS__: !!process.env.DEBUG 21 | }) 22 | ], 23 | resolve: { 24 | extensions: ['', '.js'] 25 | }, 26 | module: { 27 | loaders: [{ 28 | test: /\.js$/, 29 | loaders: ['babel'], 30 | exclude: /node_modules/ 31 | }] 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /test/reducers/counter.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import counter from '../../src/reducers/counter' 3 | import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../../src/actions/counter' 4 | 5 | describe('reducers', () => { 6 | describe('counter', () => { 7 | it('should handle initial state', () => { 8 | expect(counter(undefined, {}).count).to.equal(0) 9 | }) 10 | 11 | const testState = { count: 1 } 12 | 13 | it('should handle INCREMENT_COUNTER', () => { 14 | expect(counter(testState, { type: INCREMENT_COUNTER }).count).to.equal(2) 15 | }) 16 | 17 | it('should handle DECREMENT_COUNTER', () => { 18 | expect(counter(testState, { type: DECREMENT_COUNTER }).count).to.equal(0) 19 | }) 20 | 21 | it('should handle unknown action type', () => { 22 | expect(counter(testState, { type: 'unknown' }).count).to.equal(1) 23 | }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/components/Counter.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable*/ 2 | import React, { Component, PropTypes } from 'react' 3 | /*eslint-enable*/ 4 | 5 | export default class Counter extends Component { 6 | 7 | static propTypes = { 8 | increment: PropTypes.func.isRequired, 9 | decrement: PropTypes.func.isRequired, 10 | undo: PropTypes.func.isRequired, 11 | redo: PropTypes.func.isRequired, 12 | count: PropTypes.number.isRequired 13 | }; 14 | 15 | render () { 16 | const { increment, decrement, count, undo, redo } = this.props 17 | return ( 18 |

19 | Clicked: {count} times 20 | {' '} 21 | 22 | {' '} 23 | 24 | {' '} 25 | 26 | {' '} 27 | 28 |

29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | 3 | var express = require('express'); 4 | 5 | var app = express(); 6 | 7 | app.use(require('morgan')('short')); 8 | 9 | (function initWebpack() { 10 | var webpack = require('webpack'); 11 | var webpackConfig = require('./webpack.config'); 12 | var compiler = webpack(webpackConfig); 13 | 14 | app.use(require('webpack-dev-middleware')(compiler, { 15 | noInfo: true, publicPath: webpackConfig.output.publicPath 16 | })); 17 | 18 | app.use(require('webpack-hot-middleware')(compiler, { 19 | log: console.log, path: '/__webpack_hmr', heartbeat: 10 * 1000 20 | })); 21 | })(); 22 | 23 | app.get('/', function root(req, res) { 24 | res.sendFile(__dirname + '/index.html'); 25 | }); 26 | 27 | if (require.main === module) { 28 | var server = http.createServer(app); 29 | server.listen(process.env.PORT || 3000, function onListen() { 30 | var address = server.address(); 31 | console.log('Listening on: %j', address); 32 | console.log(' -> that probably means: http://localhost:%d', address.port); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Daniel Bugl (https://github.com/omnidan) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/containers/Root.js: -------------------------------------------------------------------------------- 1 | /* global __DEVTOOLS__ */ 2 | /*eslint-disable*/ 3 | import React, { Component, PropTypes } from 'react' 4 | import { Provider } from 'react-redux' 5 | import { Router } from 'react-router' 6 | import configureStore from '../store/configureStore' 7 | import routes from '../routes' 8 | import defaultImport from '../utils/defaultImport' 9 | /*eslint-enable*/ 10 | 11 | const store = configureStore() 12 | 13 | function createElements (history) { 14 | const elements = [ 15 | 16 | ] 17 | 18 | if (typeof __DEVTOOLS__ !== 'undefined' && __DEVTOOLS__) { 19 | /*eslint-disable*/ 20 | const DevTools = defaultImport(require('./DevTools')) 21 | /*eslint-enable*/ 22 | elements.push() 23 | } 24 | 25 | return elements 26 | } 27 | 28 | export default class Root extends Component { 29 | 30 | static propTypes = { 31 | history: PropTypes.object.isRequired 32 | }; 33 | 34 | render () { 35 | return ( 36 | 37 |
38 | {createElements(this.props.history)} 39 |
40 |
41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | /* global __DEVTOOLS__ */ 2 | import { createStore, applyMiddleware, compose } from 'redux' 3 | // reducer 4 | import rootReducer from '../reducers' 5 | // middleware 6 | import thunkMiddleware from 'redux-thunk' 7 | import promiseMiddleware from 'redux-promise' 8 | import createLogger from 'redux-logger' 9 | 10 | import reduxImmutableStateInvariant from 'redux-immutable-state-invariant' 11 | import defaultImport from '../utils/defaultImport' 12 | 13 | const enforceImmutableMiddleware = reduxImmutableStateInvariant() 14 | 15 | const loggerMiddleware = createLogger({ 16 | level: 'info', 17 | collapsed: true 18 | }) 19 | 20 | let createStoreWithMiddleware 21 | 22 | if (typeof __DEVTOOLS__ !== 'undefined' && __DEVTOOLS__) { 23 | const { persistState } = require('redux-devtools') 24 | const DevTools = defaultImport(require('../containers/DevTools')) 25 | createStoreWithMiddleware = compose( 26 | applyMiddleware( 27 | enforceImmutableMiddleware, 28 | thunkMiddleware, 29 | promiseMiddleware, 30 | loggerMiddleware 31 | ), 32 | DevTools.instrument(), 33 | persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/)) 34 | )(createStore) 35 | } else { 36 | createStoreWithMiddleware = compose( 37 | applyMiddleware(thunkMiddleware, promiseMiddleware) 38 | )(createStore) 39 | } 40 | 41 | /** 42 | * Creates a preconfigured store. 43 | */ 44 | export default function configureStore (initialState) { 45 | const store = createStoreWithMiddleware(rootReducer, initialState) 46 | 47 | if (module.hot) { 48 | // Enable Webpack hot module replacement for reducers 49 | module.hot.accept('../reducers', () => { 50 | const nextRootReducer = defaultImport(require('../reducers/index')) 51 | 52 | store.replaceReducer(nextRootReducer) 53 | }) 54 | } 55 | 56 | return store 57 | } 58 | -------------------------------------------------------------------------------- /test/components/Counter.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-expressions: 0 */ 2 | import { expect } from 'chai' 3 | import { spy } from 'sinon' 4 | import jsdom from 'jsdom-global' 5 | /*eslint-disable*/ 6 | import React from 'react' 7 | import TestUtils from 'react-addons-test-utils' 8 | import Counter from '../../src/components/Counter' 9 | /*eslint-enable*/ 10 | 11 | function setup () { 12 | const actions = { 13 | increment: spy(), 14 | incrementIfOdd: spy(), 15 | incrementAsync: spy(), 16 | decrement: spy(), 17 | undo: spy(), 18 | redo: spy() 19 | } 20 | const component = TestUtils.renderIntoDocument() 21 | return { 22 | component: component, 23 | actions: actions, 24 | buttons: TestUtils.scryRenderedDOMComponentsWithTag(component, 'button').map(button => { 25 | return button 26 | }), 27 | p: TestUtils.findRenderedDOMComponentWithTag(component, 'p') 28 | } 29 | } 30 | 31 | describe('Counter component', () => { 32 | jsdom() 33 | 34 | it('should display count', () => { 35 | const { p } = setup() 36 | expect(p.textContent).to.match(/^Clicked: 1 times/) 37 | }) 38 | 39 | it('first button should call increment', () => { 40 | const { buttons, actions } = setup() 41 | TestUtils.Simulate.click(buttons[0]) 42 | expect(actions.increment.called).to.be.true 43 | }) 44 | 45 | it('second button should call decrement', () => { 46 | const { buttons, actions } = setup() 47 | TestUtils.Simulate.click(buttons[1]) 48 | expect(actions.decrement.called).to.be.true 49 | }) 50 | 51 | it('third button should call undo', () => { 52 | const { buttons, actions } = setup() 53 | TestUtils.Simulate.click(buttons[2]) 54 | expect(actions.undo.called).to.be.true 55 | }) 56 | 57 | it('fourth button should call redo', () => { 58 | const { buttons, actions } = setup() 59 | TestUtils.Simulate.click(buttons[3]) 60 | expect(actions.redo.called).to.be.true 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /test/containers/CounterPage.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import jsdom from 'jsdom-global' 3 | /*eslint-disable*/ 4 | import React from 'react' 5 | import TestUtils from 'react-addons-test-utils' 6 | import { Provider } from 'react-redux' 7 | import CounterPage from '../../src/containers/CounterPage' 8 | import configureStore from '../../src/store/configureStore' 9 | /*eslint-enable*/ 10 | 11 | function setup (initialState) { 12 | const store = configureStore(initialState) 13 | const app = TestUtils.renderIntoDocument( 14 | 15 | 16 | 17 | ) 18 | return { 19 | app: app, 20 | buttons: TestUtils.scryRenderedDOMComponentsWithTag(app, 'button').map(button => { 21 | return button 22 | }), 23 | p: TestUtils.findRenderedDOMComponentWithTag(app, 'p') 24 | } 25 | } 26 | 27 | describe('containers', () => { 28 | jsdom() 29 | 30 | describe('App', () => { 31 | it('should display initial count', () => { 32 | const { p } = setup() 33 | expect(p.textContent).to.match(/^Clicked: 0 times/) 34 | }) 35 | 36 | it('should display updated count after increment button click', () => { 37 | const { buttons, p } = setup() 38 | TestUtils.Simulate.click(buttons[0]) 39 | expect(p.textContent).to.match(/^Clicked: 1 times/) 40 | }) 41 | 42 | it('should display updated count after descrement button click', () => { 43 | const { buttons, p } = setup() 44 | TestUtils.Simulate.click(buttons[1]) 45 | expect(p.textContent).to.match(/^Clicked: -1 times/) 46 | }) 47 | 48 | it('should undo increment action on undo button click', () => { 49 | const { buttons, p } = setup() 50 | TestUtils.Simulate.click(buttons[0]) 51 | expect(p.textContent).to.match(/^Clicked: 1 times/) 52 | TestUtils.Simulate.click(buttons[2]) 53 | expect(p.textContent).to.match(/^Clicked: 0 times/) 54 | }) 55 | 56 | it('should redo after undo on redo button click', () => { 57 | const { buttons, p } = setup() 58 | TestUtils.Simulate.click(buttons[0]) 59 | expect(p.textContent).to.match(/^Clicked: 1 times/) 60 | TestUtils.Simulate.click(buttons[2]) 61 | expect(p.textContent).to.match(/^Clicked: 0 times/) 62 | TestUtils.Simulate.click(buttons[3]) 63 | expect(p.textContent).to.match(/^Clicked: 1 times/) 64 | }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-undo-boilerplate", 3 | "version": "1.0.0-beta2", 4 | "description": "a magical boilerplate with hot reloading and awesome error handling™ that uses webpack, redux, react and redux-undo", 5 | "main": "lib/index.js", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/omnidan/redux-undo-boilerplate.git" 10 | }, 11 | "scripts": { 12 | "start": "node server.js", 13 | "dev": "DEBUG=true node server.js", 14 | "clean": "rimraf lib dist coverage", 15 | "lint": "eslint src test", 16 | "test": "NODE_ENV=test mocha --compilers js:babel-core/register --recursive", 17 | "test:watch": "NODE_ENV=test npm test -- --watch", 18 | "test:cov": "babel-node $(npm bin)/isparta cover $(npm bin)/_mocha -- --recursive", 19 | "check": "npm run lint && npm run test", 20 | "build": " ", 21 | "preversion": "npm run clean && npm run check", 22 | "version": "npm run build", 23 | "postversion": "git push && git push --tags && npm run clean", 24 | "prepublish": "npm run clean && npm run build" 25 | }, 26 | "author": "Daniel Bugl (https://github.com/omnidan)", 27 | "engines": { 28 | "node": ">=0.10.0" 29 | }, 30 | "keywords": [ 31 | "redux", 32 | "react", 33 | "redux-undo", 34 | "boilerplate", 35 | "redux-boilerplate", 36 | "redux-undo-boilerplate", 37 | "predictable", 38 | "functional", 39 | "immutable", 40 | "hot", 41 | "live", 42 | "replay", 43 | "undo", 44 | "redo", 45 | "time travel", 46 | "flux" 47 | ], 48 | "devDependencies": { 49 | "babel-core": "^6.4.5", 50 | "babel-eslint": "^5.0.0-beta8", 51 | "babel-loader": "^6.2.1", 52 | "babel-plugin-transform-class-properties": "^6.4.0", 53 | "babel-plugin-transform-object-rest-spread": "^6.3.13", 54 | "babel-preset-es2015": "^6.3.13", 55 | "babel-preset-react": "^6.3.13", 56 | "babel-preset-react-hmre": "^1.0.1", 57 | "chai": "^3.5.0", 58 | "eslint": "^1.10.3", 59 | "eslint-config-airbnb": "4.0.0", 60 | "eslint-config-standard": "^4.4.0", 61 | "eslint-plugin-react": "^3.16.1", 62 | "eslint-plugin-standard": "^1.3.1", 63 | "express": "^4.13.4", 64 | "history": "^2.0.0-rc2", 65 | "isparta": "^4.0.0", 66 | "jsdom": "^8.0.1", 67 | "jsdom-global": "^1.6.1", 68 | "mocha": "*", 69 | "morgan": "^1.6.1", 70 | "proxyquire": "^1.7.3", 71 | "react-addons-test-utils": "^0.14.7", 72 | "redbox-react": "^1.2.0", 73 | "redux-devtools": "^3.0.2", 74 | "redux-devtools-dock-monitor": "^1.0.1", 75 | "redux-devtools-log-monitor": "^1.0.2", 76 | "redux-immutable-state-invariant": "^1.2.0", 77 | "rimraf": "^2.5.1", 78 | "sinon": "^1.17.3", 79 | "webpack": "^1.12.12", 80 | "webpack-dev-middleware": "^1.5.1", 81 | "webpack-hot-middleware": "^2.6.4" 82 | }, 83 | "dependencies": { 84 | "react": "^0.14.7", 85 | "react-dom": "^0.14.7", 86 | "react-redux": "^4.1.2", 87 | "react-router": "^2.0.0-rc5", 88 | "redux": "^3.1.6", 89 | "redux-logger": "^2.4.0", 90 | "redux-promise": "^0.5.1", 91 | "redux-thunk": "^1.0.3", 92 | "redux-undo": "^1.0.0-beta2" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-undo-boilerplate 2 | 3 | ![Version 1.0.0-beta2](https://img.shields.io/badge/version-1.0.0--beta2-blue.svg?style=flat-square) [![Build Status](https://img.shields.io/travis/omnidan/redux-undo-boilerplate/master.svg?style=flat-square)](https://travis-ci.org/omnidan/redux-undo-boilerplate) [![Dependency Status](https://img.shields.io/david/omnidan/redux-undo-boilerplate.svg?style=flat-square)](https://david-dm.org/omnidan/redux-undo-boilerplate) [![devDependency Status](https://david-dm.org/omnidan/redux-undo-boilerplate/dev-status.svg?style=flat-square)](https://david-dm.org/omnidan/redux-undo-boilerplate#info=devDependencies) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](http://standardjs.com/) [![https://gratipay.com/omnidan/](https://img.shields.io/gratipay/omnidan.svg?style=flat-square)](https://gratipay.com/omnidan/) 4 | 5 | _a magical boilerplate with [hot reloading](#what-happens-if-i-change-some-code) and [awesome error handling™](#what-happens-if-i-make-a-typo--syntax-error) that uses [webpack](https://github.com/webpack/webpack), [redux](https://github.com/rackt/redux), [react](https://github.com/facebook/react) and [redux-undo](https://github.com/omnidan/redux-undo)_ 6 | 7 | 8 | ## Installation 9 | 10 | You need to have `npm` installed (it comes with [node.js](https://nodejs.org/)). 11 | 12 | ```sh 13 | npm install 14 | ``` 15 | 16 | 17 | ## Running 18 | 19 | During development, run: 20 | 21 | ```sh 22 | npm run dev 23 | ``` 24 | 25 | Which enables some development tools. 26 | 27 | In production, run: 28 | 29 | ```sh 30 | npm start 31 | ``` 32 | 33 | These commands (unless configured otherwise) start a web server at: [http://localhost:3000](http://localhost:3000) 34 | 35 | 36 | ## Demo 37 | 38 | [![https://i.imgur.com/M2KR4uo.gif](https://i.imgur.com/M2KR4uo.gif)](https://i.imgur.com/M2KR4uo.gif) 39 | 40 | ### What happens if I change some code? 41 | 42 | Save the file in your editor and immediately see the changes reflected in your 43 | browser - coding has never been more efficient. What a beautiful world we live 44 | in nowadays. 45 | 46 | [![http://i.imgur.com/VCxUA2b.gif](http://i.imgur.com/VCxUA2b.gif)](http://i.imgur.com/VCxUA2b.gif) 47 | 48 | ### What happens if I make a typo / syntax error? 49 | 50 | Many of us know this: You accidentally type in the wrong window once, add a 51 | random character to your code and when you run it again you're like "WTF this 52 | just worked?!" - let `webpack-hot-middleware` help you out with this: 53 | 54 | [![http://i.imgur.com/DTnGNFE.gif](http://i.imgur.com/DTnGNFE.gif)](http://i.imgur.com/DTnGNFE.gif) 55 | 56 | ### What happens if I mutate the state directly? 57 | 58 | Mutating the state directly causes lots of bugs with Redux. There are no 59 | immutables in JavaScript, so we can't make sure this doesn't happen unless we 60 | use something like [Immutable.js](https://facebook.github.io/immutable-js/). 61 | 62 | If you run this boilerplate in dev mode (`npm run dev`), it will tell you when 63 | you [mutate something directly](https://github.com/omnidan/redux-undo-boilerplate/blob/master/src/reducers/counter.js#L9): 64 | 65 | [![https://i.imgur.com/y02EDxc.png](https://i.imgur.com/y02EDxc.png)](https://i.imgur.com/y02EDxc.png) 66 | 67 | 68 | ## Testing 69 | 70 | ```sh 71 | npm test 72 | ``` 73 | 74 | 75 | ## Thanks 76 | 77 | Special thanks to these awesome projects/people making this possible :heart: 78 | 79 | * [React](https://facebook.github.io/react/) 80 | * [Redux](https://rackt.github.io/redux/) 81 | * [Babel](https://babeljs.io/) - for ES6 support 82 | * [redux-boilerplate](https://github.com/chentsulin/redux-boilerplate) by [chentsulin](https://github.com/chentsulin) - this boilerplate is based off his project 83 | * [babel-plugin-react-transform](https://github.com/gaearon/babel-plugin-react-transform) by [gaearon](https://github.com/gaearon) - as a base for the hot reloading and error handling 84 | * [react-transform-catch-errors](https://github.com/gaearon/react-transform-catch-errors) by [gaearon](https://github.com/gaearon) - error handling 85 | * [react-transform-hmr](https://github.com/gaearon/react-transform-hmr) by [gaearon](https://github.com/gaearon) - hot reloading 86 | * [redux-devtools](https://github.com/gaearon/redux-devtools) by [gaearon](https://github.com/gaearon) 87 | * [redux-immutable-state-invariant](https://github.com/leoasis/redux-immutable-state-invariant) by [leoasis](https://github.com/leoasis) - detect state mutations 88 | 89 | 90 | ## License 91 | 92 | redux-boilerplate: MIT © [C.T. Lin](https://github.com/chentsulin) 93 | 94 | redux-undo-boilerplate: MIT © [Daniel Bugl](https://github.com/omnidan) 95 | --------------------------------------------------------------------------------