├── test ├── .eslintrc ├── setup.js ├── actions │ └── todos.spec.js ├── components │ ├── Header.spec.js │ ├── TodoTextInput.spec.js │ ├── Footer.spec.js │ ├── TodoItem.spec.js │ └── MainSection.spec.js └── reducers │ └── todos.spec.js ├── .gitignore ├── constants ├── TodoFilters.js └── ActionTypes.js ├── reducers ├── index.js └── todos.js ├── index.html ├── .babelrc ├── README.md ├── index.js ├── actions └── todos.js ├── store └── configureStore.js ├── components ├── Header.js ├── TodoTextInput.js ├── TodoItem.js ├── Footer.js └── MainSection.js ├── server.js ├── containers ├── DevTools.js └── App.js ├── webpack.config.js └── package.json /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | dist 5 | lib 6 | coverage 7 | .idea 8 | -------------------------------------------------------------------------------- /constants/TodoFilters.js: -------------------------------------------------------------------------------- 1 | export const SHOW_ALL = 'show_all' 2 | export const SHOW_COMPLETED = 'show_completed' 3 | export const SHOW_ACTIVE = 'show_active' 4 | -------------------------------------------------------------------------------- /reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import todos from './todos' 3 | 4 | const rootReducer = combineReducers({ 5 | todos 6 | }) 7 | 8 | export default rootReducer 9 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import { jsdom } from 'jsdom' 2 | 3 | global.document = jsdom('') 4 | global.window = document.defaultView 5 | global.navigator = global.window.navigator 6 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux TodoMVC example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export const ADD_TODO = 'ADD_TODO' 2 | export const DELETE_TODO = 'DELETE_TODO' 3 | export const EDIT_TODO = 'EDIT_TODO' 4 | export const COMPLETE_TODO = 'COMPLETE_TODO' 5 | export const COMPLETE_ALL = 'COMPLETE_ALL' 6 | export const CLEAR_COMPLETED = 'CLEAR_COMPLETED' 7 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "plugins": [["react-transform", { 6 | "transforms": [{ 7 | "transform": "react-transform-hmr", 8 | "imports": ["react"], 9 | "locals": ["module"] 10 | }] 11 | }]] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a demo/example of how to use redux-devtools-chart-monitor in your React/Redux app. 2 | 3 | - Live demo: [here](http://romseguy.github.io/redux-store-visualizer/) 4 | - Grab the monitor: [redux-devtools-chart-monitor](https://github.com/romseguy/redux-devtools-chart-monitor) 5 | - Under the hood: [d3-state-visualizer](https://github.com/romseguy/d3-state-visualizer) 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | import DevTools from './containers/DevTools' 5 | import App from './containers/App' 6 | import configureStore from './store/configureStore' 7 | import 'todomvc-app-css/index.css' 8 | 9 | const store = configureStore() 10 | 11 | render( 12 | 13 |
14 | 15 | 16 |
17 |
, 18 | document.getElementById('root') 19 | ) 20 | -------------------------------------------------------------------------------- /actions/todos.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes' 2 | 3 | export function addTodo(text) { 4 | return { type: types.ADD_TODO, text } 5 | } 6 | 7 | export function deleteTodo(id) { 8 | return { type: types.DELETE_TODO, id } 9 | } 10 | 11 | export function editTodo(id, text) { 12 | return { type: types.EDIT_TODO, id, text } 13 | } 14 | 15 | export function completeTodo(id) { 16 | return { type: types.COMPLETE_TODO, id } 17 | } 18 | 19 | export function completeAll() { 20 | return { type: types.COMPLETE_ALL } 21 | } 22 | 23 | export function clearCompleted() { 24 | return { type: types.CLEAR_COMPLETED } 25 | } 26 | -------------------------------------------------------------------------------- /store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose } from 'redux' 2 | import rootReducer from '../reducers' 3 | import DevTools from '../containers/DevTools'; 4 | 5 | const finalCreateStore = compose( 6 | DevTools.instrument() 7 | )(createStore); 8 | 9 | export default function configureStore(initialState) { 10 | const store = finalCreateStore(rootReducer, initialState) 11 | 12 | if (module.hot) { 13 | // Enable Webpack hot module replacement for reducers 14 | module.hot.accept('../reducers', () => { 15 | const nextReducer = require('../reducers') 16 | store.replaceReducer(nextReducer) 17 | }) 18 | } 19 | 20 | return store 21 | } 22 | -------------------------------------------------------------------------------- /components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import TodoTextInput from './TodoTextInput' 3 | 4 | class Header extends Component { 5 | handleSave(text) { 6 | if (text.length !== 0) { 7 | this.props.addTodo(text) 8 | } 9 | } 10 | 11 | render() { 12 | return ( 13 |
14 |

todos

15 | 18 |
19 | ) 20 | } 21 | } 22 | 23 | Header.propTypes = { 24 | addTodo: PropTypes.func.isRequired 25 | } 26 | 27 | export default Header 28 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | var webpackDevMiddleware = require('webpack-dev-middleware') 3 | var webpackHotMiddleware = require('webpack-hot-middleware') 4 | var config = require('./webpack.config') 5 | 6 | var app = new (require('express'))() 7 | var port = 3000 8 | 9 | var compiler = webpack(config) 10 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })) 11 | app.use(webpackHotMiddleware(compiler)) 12 | 13 | app.get("/", function(req, res) { 14 | res.sendFile(__dirname + '/index.html') 15 | }) 16 | 17 | app.listen(port, function(error) { 18 | if (error) { 19 | console.error(error) 20 | } else { 21 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port) 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /containers/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { createDevTools } from 'redux-devtools'; 4 | 5 | import ChartMonitor from 'redux-devtools-chart-monitor'; 6 | import DockMonitor from 'redux-devtools-dock-monitor'; 7 | 8 | const tooltipOptions = { 9 | disabled: false, 10 | offset: {left: 30, top: 10}, 11 | indentationSize: 2, 12 | style: { 13 | 'background-color': 'lightgrey', 14 | 'opacity': '0.7', 15 | 'border-radius': '5px', 16 | 'padding': '5px' 17 | } 18 | } 19 | 20 | const DevTools = createDevTools( 21 | 26 | 30 | 31 | ); 32 | 33 | export default DevTools; 34 | -------------------------------------------------------------------------------- /containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { bindActionCreators } from 'redux' 3 | import { connect } from 'react-redux' 4 | import Header from '../components/Header' 5 | import MainSection from '../components/MainSection' 6 | import * as TodoActions from '../actions/todos' 7 | 8 | class App extends Component { 9 | render() { 10 | const { todos, actions } = this.props 11 | return ( 12 |
13 |
14 | 15 |
16 | ) 17 | } 18 | } 19 | 20 | App.propTypes = { 21 | todos: PropTypes.array.isRequired, 22 | actions: PropTypes.object.isRequired 23 | } 24 | 25 | function mapStateToProps(state) { 26 | return { 27 | todos: state.todos 28 | } 29 | } 30 | 31 | function mapDispatchToProps(dispatch) { 32 | return { 33 | actions: bindActionCreators(TodoActions, dispatch) 34 | } 35 | } 36 | 37 | export default connect( 38 | mapStateToProps, 39 | mapDispatchToProps 40 | )(App) 41 | -------------------------------------------------------------------------------- /test/actions/todos.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import * as types from '../../constants/ActionTypes' 3 | import * as actions from '../../actions/todos' 4 | 5 | describe('todo actions', () => { 6 | it('addTodo should create ADD_TODO action', () => { 7 | expect(actions.addTodo('Use Redux')).toEqual({ 8 | type: types.ADD_TODO, 9 | text: 'Use Redux' 10 | }) 11 | }) 12 | 13 | it('deleteTodo should create DELETE_TODO action', () => { 14 | expect(actions.deleteTodo(1)).toEqual({ 15 | type: types.DELETE_TODO, 16 | id: 1 17 | }) 18 | }) 19 | 20 | it('editTodo should create EDIT_TODO action', () => { 21 | expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({ 22 | type: types.EDIT_TODO, 23 | id: 1, 24 | text: 'Use Redux everywhere' 25 | }) 26 | }) 27 | 28 | it('completeTodo should create COMPLETE_TODO action', () => { 29 | expect(actions.completeTodo(1)).toEqual({ 30 | type: types.COMPLETE_TODO, 31 | id: 1 32 | }) 33 | }) 34 | 35 | it('completeAll should create COMPLETE_ALL action', () => { 36 | expect(actions.completeAll()).toEqual({ 37 | type: types.COMPLETE_ALL 38 | }) 39 | }) 40 | 41 | it('clearCompleted should create CLEAR_COMPLETED action', () => { 42 | expect(actions.clearCompleted('Use Redux')).toEqual({ 43 | type: types.CLEAR_COMPLETED 44 | }) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /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 | './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 | ], 20 | module: { 21 | loaders: [{ 22 | test: /\.js$/, 23 | loaders: [ 'babel' ], 24 | exclude: /node_modules/, 25 | include: __dirname 26 | }, { 27 | test: /\.css?$/, 28 | loaders: [ 'style', 'raw' ], 29 | include: __dirname 30 | }] 31 | } 32 | } 33 | 34 | 35 | // When inside Redux repo, prefer src to compiled version. 36 | // You can safely delete these lines in your project. 37 | var reduxSrc = path.join(__dirname, '..', '..', 'src') 38 | var reduxNodeModules = path.join(__dirname, '..', '..', 'node_modules') 39 | var fs = require('fs') 40 | if (fs.existsSync(reduxSrc) && fs.existsSync(reduxNodeModules)) { 41 | // Resolve Redux to source 42 | module.exports.resolve = { alias: { 'redux': reduxSrc } } 43 | // Compile Redux from source 44 | module.exports.module.loaders.push({ 45 | test: /\.js$/, 46 | loaders: [ 'babel' ], 47 | include: reduxSrc 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /reducers/todos.js: -------------------------------------------------------------------------------- 1 | import { ADD_TODO, DELETE_TODO, EDIT_TODO, COMPLETE_TODO, COMPLETE_ALL, CLEAR_COMPLETED } from '../constants/ActionTypes' 2 | 3 | const initialState = [ 4 | { 5 | text: 'Use Redux', 6 | completed: false, 7 | id: 0 8 | } 9 | ] 10 | 11 | export default function todos(state = initialState, action) { 12 | switch (action.type) { 13 | case ADD_TODO: 14 | return [ 15 | { 16 | id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1, 17 | completed: false, 18 | text: action.text 19 | }, 20 | ...state 21 | ] 22 | 23 | case DELETE_TODO: 24 | return state.filter(todo => 25 | todo.id !== action.id 26 | ) 27 | 28 | case EDIT_TODO: 29 | return state.map(todo => 30 | todo.id === action.id ? 31 | Object.assign({}, todo, { text: action.text }) : 32 | todo 33 | ) 34 | 35 | case COMPLETE_TODO: 36 | return state.map(todo => 37 | todo.id === action.id ? 38 | Object.assign({}, todo, { completed: !todo.completed }) : 39 | todo 40 | ) 41 | 42 | case COMPLETE_ALL: 43 | const areAllMarked = state.every(todo => todo.completed) 44 | return state.map(todo => Object.assign({}, todo, { 45 | completed: !areAllMarked 46 | })) 47 | 48 | case CLEAR_COMPLETED: 49 | return state.filter(todo => todo.completed === false) 50 | 51 | default: 52 | return state 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /components/TodoTextInput.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import classnames from 'classnames' 3 | 4 | class TodoTextInput extends Component { 5 | constructor(props, context) { 6 | super(props, context) 7 | this.state = { 8 | text: this.props.text || '' 9 | } 10 | } 11 | 12 | handleSubmit(e) { 13 | const text = e.target.value.trim() 14 | if (e.which === 13) { 15 | this.props.onSave(text) 16 | if (this.props.newTodo) { 17 | this.setState({ text: '' }) 18 | } 19 | } 20 | } 21 | 22 | handleChange(e) { 23 | this.setState({ text: e.target.value }) 24 | } 25 | 26 | handleBlur(e) { 27 | if (!this.props.newTodo) { 28 | this.props.onSave(e.target.value) 29 | } 30 | } 31 | 32 | render() { 33 | return ( 34 | 46 | ) 47 | } 48 | } 49 | 50 | TodoTextInput.propTypes = { 51 | onSave: PropTypes.func.isRequired, 52 | text: PropTypes.string, 53 | placeholder: PropTypes.string, 54 | editing: PropTypes.bool, 55 | newTodo: PropTypes.bool 56 | } 57 | 58 | export default TodoTextInput 59 | -------------------------------------------------------------------------------- /test/components/Header.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React from 'react' 3 | import TestUtils from 'react-addons-test-utils' 4 | import Header from '../../components/Header' 5 | import TodoTextInput from '../../components/TodoTextInput' 6 | 7 | function setup() { 8 | const props = { 9 | addTodo: expect.createSpy() 10 | } 11 | 12 | const renderer = TestUtils.createRenderer() 13 | renderer.render(
) 14 | const output = renderer.getRenderOutput() 15 | 16 | return { 17 | props: props, 18 | output: output, 19 | renderer: renderer 20 | } 21 | } 22 | 23 | describe('components', () => { 24 | describe('Header', () => { 25 | it('should render correctly', () => { 26 | const { output } = setup() 27 | 28 | expect(output.type).toBe('header') 29 | expect(output.props.className).toBe('header') 30 | 31 | const [ h1, input ] = output.props.children 32 | 33 | expect(h1.type).toBe('h1') 34 | expect(h1.props.children).toBe('todos') 35 | 36 | expect(input.type).toBe(TodoTextInput) 37 | expect(input.props.newTodo).toBe(true) 38 | expect(input.props.placeholder).toBe('What needs to be done?') 39 | }) 40 | 41 | it('should call addTodo if length of text is greater than 0', () => { 42 | const { output, props } = setup() 43 | const input = output.props.children[1] 44 | input.props.onSave('') 45 | expect(props.addTodo.calls.length).toBe(0) 46 | input.props.onSave('Use Redux') 47 | expect(props.addTodo.calls.length).toBe(1) 48 | }) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-todomvc-example", 3 | "version": "0.0.0", 4 | "description": "Redux TodoMVC example", 5 | "scripts": { 6 | "start": "node server.js", 7 | "test": "NODE_ENV=test mocha --recursive --compilers js:babel-core/register --require ./test/setup.js", 8 | "test:watch": "npm test -- --watch", 9 | "build": "NODE_ENV=production webpack --config webpack.prod.config.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/rackt/redux.git" 14 | }, 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/rackt/redux/issues" 18 | }, 19 | "homepage": "http://rackt.github.io/redux", 20 | "dependencies": { 21 | "classnames": "^2.2.5", 22 | "react": "^15.4.1", 23 | "react-dom": "^15.4.1", 24 | "react-redux": "^4.4.6", 25 | "redux": "^3.6.0" 26 | }, 27 | "devDependencies": { 28 | "babel-core": "^6.18.2", 29 | "babel-loader": "^6.2.8", 30 | "babel-plugin-react-transform": "^2.0.2", 31 | "babel-preset-es2015": "^6.18.0", 32 | "babel-preset-react": "^6.16.0", 33 | "expect": "^1.20.2", 34 | "express": "^4.14.0", 35 | "jsdom": "^9.8.3", 36 | "mocha": "^3.1.2", 37 | "node-libs-browser": "^2.0.0", 38 | "raw-loader": "^0.5.1", 39 | "react-addons-test-utils": "^15.4.1", 40 | "react-transform-hmr": "^1.0.4", 41 | "redux-devtools": "^3.3.1", 42 | "redux-devtools-chart-monitor": "^1.6.0", 43 | "redux-devtools-dock-monitor": "^1.1.1", 44 | "style-loader": "^0.13.1", 45 | "todomvc-app-css": "^2.0.6", 46 | "webpack": "^1.13.3", 47 | "webpack-dev-middleware": "^1.8.4", 48 | "webpack-hot-middleware": "^2.13.2" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /components/TodoItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import classnames from 'classnames' 3 | import TodoTextInput from './TodoTextInput' 4 | 5 | class TodoItem extends Component { 6 | constructor(props, context) { 7 | super(props, context) 8 | this.state = { 9 | editing: false 10 | } 11 | } 12 | 13 | handleDoubleClick() { 14 | this.setState({ editing: true }) 15 | } 16 | 17 | handleSave(id, text) { 18 | if (text.length === 0) { 19 | this.props.deleteTodo(id) 20 | } else { 21 | this.props.editTodo(id, text) 22 | } 23 | this.setState({ editing: false }) 24 | } 25 | 26 | render() { 27 | const { todo, completeTodo, deleteTodo } = this.props 28 | 29 | let element 30 | if (this.state.editing) { 31 | element = ( 32 | this.handleSave(todo.id, text)} /> 35 | ) 36 | } else { 37 | element = ( 38 |
39 | completeTodo(todo.id)} /> 43 | 46 |
49 | ) 50 | } 51 | 52 | return ( 53 |
  • 57 | {element} 58 |
  • 59 | ) 60 | } 61 | } 62 | 63 | TodoItem.propTypes = { 64 | todo: PropTypes.object.isRequired, 65 | editTodo: PropTypes.func.isRequired, 66 | deleteTodo: PropTypes.func.isRequired, 67 | completeTodo: PropTypes.func.isRequired 68 | } 69 | 70 | export default TodoItem 71 | -------------------------------------------------------------------------------- /components/Footer.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import classnames from 'classnames' 3 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters' 4 | 5 | const FILTER_TITLES = { 6 | [SHOW_ALL]: 'All', 7 | [SHOW_ACTIVE]: 'Active', 8 | [SHOW_COMPLETED]: 'Completed' 9 | } 10 | 11 | class Footer extends Component { 12 | renderTodoCount() { 13 | const { activeCount } = this.props 14 | const itemWord = activeCount === 1 ? 'item' : 'items' 15 | 16 | return ( 17 | 18 | {activeCount || 'No'} {itemWord} left 19 | 20 | ) 21 | } 22 | 23 | renderFilterLink(filter) { 24 | const title = FILTER_TITLES[filter] 25 | const { filter: selectedFilter, onShow } = this.props 26 | 27 | return ( 28 | onShow(filter)}> 31 | {title} 32 | 33 | ) 34 | } 35 | 36 | renderClearButton() { 37 | const { completedCount, onClearCompleted } = this.props 38 | if (completedCount > 0) { 39 | return ( 40 | 44 | ) 45 | } 46 | } 47 | 48 | render() { 49 | return ( 50 |
    51 | {this.renderTodoCount()} 52 |
      53 | {[ SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED ].map(filter => 54 |
    • 55 | {this.renderFilterLink(filter)} 56 |
    • 57 | )} 58 |
    59 | {this.renderClearButton()} 60 |
    61 | ) 62 | } 63 | } 64 | 65 | Footer.propTypes = { 66 | completedCount: PropTypes.number.isRequired, 67 | activeCount: PropTypes.number.isRequired, 68 | filter: PropTypes.string.isRequired, 69 | onClearCompleted: PropTypes.func.isRequired, 70 | onShow: PropTypes.func.isRequired 71 | } 72 | 73 | export default Footer 74 | -------------------------------------------------------------------------------- /components/MainSection.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import TodoItem from './TodoItem' 3 | import Footer from './Footer' 4 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters' 5 | 6 | const TODO_FILTERS = { 7 | [SHOW_ALL]: () => true, 8 | [SHOW_ACTIVE]: todo => !todo.completed, 9 | [SHOW_COMPLETED]: todo => todo.completed 10 | } 11 | 12 | class MainSection extends Component { 13 | constructor(props, context) { 14 | super(props, context) 15 | this.state = { filter: SHOW_ALL } 16 | } 17 | 18 | handleClearCompleted() { 19 | const atLeastOneCompleted = this.props.todos.some(todo => todo.completed) 20 | if (atLeastOneCompleted) { 21 | this.props.actions.clearCompleted() 22 | } 23 | } 24 | 25 | handleShow(filter) { 26 | this.setState({ filter }) 27 | } 28 | 29 | renderToggleAll(completedCount) { 30 | const { todos, actions } = this.props 31 | if (todos.length > 0) { 32 | return ( 33 | 37 | ) 38 | } 39 | } 40 | 41 | renderFooter(completedCount) { 42 | const { todos } = this.props 43 | const { filter } = this.state 44 | const activeCount = todos.length - completedCount 45 | 46 | if (todos.length) { 47 | return ( 48 |