├── 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 | Undo
26 | {' '}
27 | Redo
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 |  [](https://travis-ci.org/omnidan/redux-undo-boilerplate) [](https://david-dm.org/omnidan/redux-undo-boilerplate) [](https://david-dm.org/omnidan/redux-undo-boilerplate#info=devDependencies) [](http://standardjs.com/) [](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)
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)
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)
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)
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 |
--------------------------------------------------------------------------------