├── LICENSE ├── README.md ├── counter-react-redux ├── .babelrc ├── .gitignore ├── package.json ├── src │ ├── index.html │ └── index.js └── webpack.config.js ├── counter-vanila ├── .babelrc ├── .gitignore ├── package.json ├── src │ ├── index.html │ └── index.js └── webpack.config.js └── todo-redux ├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── package.json ├── src ├── actions │ ├── todos.js │ └── types.js ├── components │ ├── ClearCompletedButton.js │ ├── Filters.js │ ├── Footer.js │ ├── Header.js │ ├── MainSection.js │ ├── TodoCount.js │ ├── TodoInput.js │ ├── TodoItem.js │ └── TodoList.js ├── constants │ └── filters.js ├── containers │ ├── FilteredTodoList.js │ ├── TodoApp.js │ └── TodoList.js ├── index.html ├── index.js └── reducers │ ├── filterTodo.js │ ├── index.js │ ├── newTodo.js │ └── todos.js ├── test ├── .eslintrc ├── actions │ └── todos.spec.js ├── components │ ├── ClearCompletedButton.spec.js │ ├── Filters.spec.js │ ├── Footer.spec.js │ ├── Header.spec.js │ ├── MainSection.spec.js │ ├── TodoCount.spec.js │ ├── TodoInput.spec.js │ ├── TodoItem.spec.js │ └── TodoList.spec.js ├── containers │ └── FilteredTodoList.spec.js ├── reducers │ ├── filterTodo.spec.js │ ├── newTodo.spec.js │ └── todos.spec.js └── test-helper.js ├── webpack.config.js └── yarn.lock /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Supasate Choochaisri 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 | # Intro To Redux @ ReactJS Bangkok 1.0.0 2 | -------------------------------------------------------------------------------- /counter-react-redux/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "es2015", 5 | "stage-0" 6 | ] 7 | } -------------------------------------------------------------------------------- /counter-react-redux/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | dist -------------------------------------------------------------------------------- /counter-react-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --hot --inline", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "babel-loader": "^6.2.5", 14 | "babel-preset-es2015": "^6.14.0", 15 | "babel-preset-react": "^6.11.1", 16 | "babel-preset-stage-0": "^6.5.0", 17 | "html-webpack-plugin": "^2.22.0", 18 | "react-hot-loader": "^1.3.0", 19 | "webpack": "^1.13.2", 20 | "webpack-dev-server": "^1.15.0" 21 | }, 22 | "dependencies": { 23 | "react": "^15.3.1", 24 | "react-dom": "^15.3.1", 25 | "react-redux": "^4.4.5", 26 | "redux": "^3.5.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /counter-react-redux/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Counter 4 | 5 | 6 |
7 | -------------------------------------------------------------------------------- /counter-react-redux/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDom from 'react-dom' 3 | 4 | import { createStore } from 'redux' 5 | import { Provider, connect } from 'react-redux' 6 | 7 | const add = (value) => { 8 | return { type: 'ADD', payload: value } 9 | } 10 | const Counter = (props) => ( 11 |
12 | 13 | 14 |
15 | ) 16 | 17 | const mapStateToProps = (state) => { 18 | return { 19 | counter: state.counter, 20 | } 21 | } 22 | 23 | const mapDispatchToProps = (dispatch) => { 24 | return { 25 | count: () => dispatch(add(1)) 26 | } 27 | } 28 | 29 | const reducer = (state = { counter: 0}, action) => { 30 | if (action.type === 'ADD') { 31 | return { 32 | counter: state.counter + action.payload, 33 | } 34 | } 35 | return state 36 | } 37 | 38 | const CounterContainer = connect(mapStateToProps, mapDispatchToProps)(Counter) 39 | const store = createStore(reducer) 40 | 41 | ReactDom.render(, document.getElementById('root')) -------------------------------------------------------------------------------- /counter-react-redux/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var HtmlWebpackPlugin = require('html-webpack-plugin') 3 | 4 | module.exports = { 5 | entry: path.join(__dirname, 'src', 'index.js'), 6 | output: { 7 | filename: 'bundle.js', 8 | path: path.join(__dirname, 'dist'), 9 | publicPath: '/dist/', 10 | }, 11 | module: { 12 | loaders: [ 13 | { test: /\.jsx?/, exclude: /node_modules/, loaders: ['react-hot', 'babel']} 14 | ] 15 | }, 16 | plugins: [ 17 | new HtmlWebpackPlugin({ 18 | template: path.join(__dirname, 'src', 'index.html') 19 | }) 20 | ] 21 | } -------------------------------------------------------------------------------- /counter-vanila/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react", 5 | "stage-0" 6 | ] 7 | } -------------------------------------------------------------------------------- /counter-vanila/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | dist -------------------------------------------------------------------------------- /counter-vanila/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "usage-with-react", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "babel-loader": "^6.2.5", 14 | "babel-preset-es2015": "^6.14.0", 15 | "babel-preset-react": "^6.11.1", 16 | "babel-preset-stage-0": "^6.5.0", 17 | "html-webpack-plugin": "^2.22.0", 18 | "webpack": "^1.13.2", 19 | "webpack-dev-server": "^1.15.0" 20 | }, 21 | "dependencies": { 22 | "react": "^15.3.1", 23 | "react-dom": "^15.3.1", 24 | "redux": "^3.5.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /counter-vanila/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Counter 6 | 7 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /counter-vanila/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { createStore } from 'redux' 4 | 5 | const add = (value) => { 6 | return { type: 'ADD', payload: value } 7 | } 8 | 9 | const mapStateToProps = (state) => { 10 | return { 11 | counter: state.counter, 12 | } 13 | } 14 | 15 | const mapDispatchToProps = (dispatch) => { 16 | return { 17 | count: () => dispatch(add(1)) 18 | } 19 | } 20 | 21 | const reducer = (state = { counter: 0}, action) => { 22 | if (action.type === 'ADD') { 23 | return { 24 | counter: state.counter + action.payload, 25 | } 26 | } 27 | return state 28 | } 29 | 30 | const store = createStore(reducer) 31 | const connect = (mapStateToProps, mapDispatchToProps) => { 32 | return (Presentational) => { 33 | return class extends Component { 34 | constructor(props) { 35 | super(props) 36 | const stateProps = mapStateToProps(store.getState()) 37 | const dispatchProps = mapDispatchToProps(store.dispatch) 38 | this.state = { 39 | ...stateProps, 40 | ...dispatchProps, 41 | } 42 | } 43 | componentWillMount() { 44 | store.subscribe(() => { 45 | const stateProps = mapStateToProps(store.getState()) 46 | console.log(stateProps) 47 | this.setState(stateProps) 48 | }) 49 | } 50 | render() { 51 | return 52 | } 53 | } 54 | } 55 | } 56 | 57 | const Counter = (props) => ( 58 |
59 | 60 | 61 |
62 | ) 63 | 64 | const CounterContainer = connect(mapStateToProps, mapDispatchToProps)(Counter) 65 | ReactDOM.render(, document.getElementById('root')) 66 | -------------------------------------------------------------------------------- /counter-vanila/webpack.config.js: -------------------------------------------------------------------------------- 1 | var HtmlWebpackPlugin = require('html-webpack-plugin') 2 | var path = require('path') 3 | module.exports = { 4 | devtool: 'eval-source-map', 5 | entry: path.join(__dirname, 'src', 'index.js'), 6 | output: { 7 | filename: 'bundle.js', 8 | path: path.join(__dirname, 'dist'), 9 | publicPath: '/dist/', 10 | }, 11 | module: { 12 | loaders: [ 13 | { 14 | test: /\.jsx?$/, 15 | loaders: ['babel?compact=false'], 16 | } 17 | ] 18 | }, 19 | plugins: [ 20 | new HtmlWebpackPlugin({ 21 | template: path.join(__dirname, 'src', 'index.html'), 22 | }), 23 | ] 24 | } -------------------------------------------------------------------------------- /todo-redux/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "es2015", 5 | "stage-0", 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /todo-redux/.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /todo-redux/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "browser": true, 5 | "es6": true, 6 | }, 7 | "extends": [ 8 | "airbnb", 9 | ], 10 | "parserOptions": { 11 | "ecmaFeatures": { 12 | "experimentalObjectRestSpread": true 13 | } 14 | }, 15 | "plugins": [ 16 | "react", 17 | "import", 18 | ], 19 | "rules": { 20 | "semi": ["error", "never"], 21 | "react/jsx-filename-extension": "off", 22 | "react/require-extension": "off", 23 | "import/no-extraneous-dependencies": "off", 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /todo-redux/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | npm-debug.log 4 | *.swp 5 | -------------------------------------------------------------------------------- /todo-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-redux", 3 | "version": "1.0.0", 4 | "description": "A TodoMVC with Redux", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rm -rf dist", 8 | "start": "webpack-dashboard -- webpack-dev-server --hot --inline --content-base dist --config ./webpack.config.js", 9 | "lint": "eslint .", 10 | "test": "mocha --compilers js:babel-register --require ./test/test-helper --recursive ", 11 | "posttest": "npm run lint", 12 | "build": "npm run test && webpack" 13 | }, 14 | "author": "Supasate Choochaisri", 15 | "license": "MIT", 16 | "dependencies": { 17 | "classnames": "^2.2.5", 18 | "react": "^15.3.1", 19 | "react-dom": "^15.3.1", 20 | "react-redux": "^4.4.5", 21 | "redux": "^3.5.2" 22 | }, 23 | "devDependencies": { 24 | "babel-core": "^6.13.2", 25 | "babel-loader": "^6.2.5", 26 | "babel-preset-es2015": "^6.13.2", 27 | "babel-preset-react": "^6.11.1", 28 | "babel-preset-stage-0": "^6.5.0", 29 | "chai": "^3.5.0", 30 | "chai-enzyme": "^0.5.1", 31 | "css-loader": "^0.24.0", 32 | "enzyme": "^2.4.1", 33 | "eslint": "^3.3.1", 34 | "eslint-config-airbnb": "^10.0.1", 35 | "eslint-plugin-import": "^1.14.0", 36 | "eslint-plugin-jsx-a11y": "^2.1.0", 37 | "eslint-plugin-react": "^6.1.2", 38 | "html-webpack-plugin": "^2.22.0", 39 | "mocha": "^3.0.2", 40 | "react-hot-loader": "^1.3.0", 41 | "redux-mock-store": "^1.1.4", 42 | "sinon": "^1.17.5", 43 | "sinon-chai": "^2.8.0", 44 | "style-loader": "^0.13.1", 45 | "todomvc-app-css": "^2.0.6", 46 | "webpack": "^1.13.2", 47 | "webpack-dashboard": "^0.1.7", 48 | "webpack-dev-server": "^1.15.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /todo-redux/src/actions/todos.js: -------------------------------------------------------------------------------- 1 | import types from './types' 2 | 3 | const changeNewTodoText = (text) => ({ 4 | type: types.CHANGE_NEW_TODO_TEXT, 5 | payload: { 6 | text, 7 | }, 8 | }) 9 | 10 | const addTodo = (title) => ({ 11 | type: types.ADD_TODO, 12 | payload: { 13 | title, 14 | }, 15 | }) 16 | 17 | const toggleTodo = (id) => ({ 18 | type: types.TOGGLE_TODO, 19 | payload: { 20 | id, 21 | }, 22 | }) 23 | 24 | const toggleAllTodos = () => ({ 25 | type: types.TOGGLE_ALL_TODOS, 26 | }) 27 | 28 | const destroyTodo = (id) => ({ 29 | type: types.DESTROY_TODO, 30 | payload: { 31 | id, 32 | }, 33 | }) 34 | 35 | const filterTodo = (filter) => ({ 36 | type: types.FILTER_TODO, 37 | payload: { 38 | filter, 39 | }, 40 | }) 41 | 42 | const clearCompletedTodos = () => ({ 43 | type: types.CLEAR_COMPLETED_TODOS, 44 | }) 45 | 46 | export default { 47 | changeNewTodoText, 48 | addTodo, 49 | toggleTodo, 50 | toggleAllTodos, 51 | destroyTodo, 52 | filterTodo, 53 | clearCompletedTodos, 54 | } 55 | -------------------------------------------------------------------------------- /todo-redux/src/actions/types.js: -------------------------------------------------------------------------------- 1 | const CHANGE_NEW_TODO_TEXT = 'CHANGE_NEW_TODO_TEXT' 2 | const ADD_TODO = 'ADD_TODO' 3 | const TOGGLE_TODO = 'TOGGLE_TODO' 4 | const TOGGLE_ALL_TODOS = 'TOGGLE_ALL_TODOS' 5 | const DESTROY_TODO = 'DESTROY_TODO' 6 | const FILTER_TODO = 'FILTER_TODO' 7 | const CLEAR_COMPLETED_TODOS = 'CLEAR_COMPLETED' 8 | 9 | export default { 10 | CHANGE_NEW_TODO_TEXT, 11 | ADD_TODO, 12 | TOGGLE_TODO, 13 | TOGGLE_ALL_TODOS, 14 | DESTROY_TODO, 15 | FILTER_TODO, 16 | CLEAR_COMPLETED_TODOS, 17 | } 18 | -------------------------------------------------------------------------------- /todo-redux/src/components/ClearCompletedButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const ClearCompletedButton = (props) => ( 4 | 10 | ) 11 | 12 | ClearCompletedButton.propTypes = { 13 | onClearCompleted: React.PropTypes.func.isRequired, 14 | } 15 | 16 | export default ClearCompletedButton 17 | -------------------------------------------------------------------------------- /todo-redux/src/components/Filters.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classNames from 'classnames' 3 | import filters from '../constants/filters' 4 | 5 | const Filters = ({ selectedFilter, onSelectFilter }) => { 6 | const allFilterClass = classNames({ 7 | selected: selectedFilter === filters.ALL, 8 | }) 9 | 10 | const activeFilterClass = classNames({ 11 | selected: selectedFilter === filters.ACTIVE, 12 | }) 13 | 14 | const completedFilterClass = classNames({ 15 | selected: selectedFilter === filters.COMPLETED, 16 | }) 17 | 18 | return ( 19 | 47 | ) 48 | } 49 | 50 | Filters.propTypes = { 51 | selectedFilter: React.PropTypes.string.isRequired, 52 | onSelectFilter: React.PropTypes.func.isRequired, 53 | } 54 | 55 | export default Filters 56 | -------------------------------------------------------------------------------- /todo-redux/src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import TodoCount from './TodoCount' 3 | import Filters from './Filters' 4 | import ClearCompletedButton from './ClearCompletedButton' 5 | 6 | const Footer = (props) => { 7 | const clearButton = props.numCompletedItem > 0 ? 8 | : '' 9 | 10 | return ( 11 |
12 | 13 | 14 | {clearButton} 15 |
16 | ) 17 | } 18 | 19 | Footer.propTypes = { 20 | numActiveItem: React.PropTypes.number.isRequired, 21 | numCompletedItem: React.PropTypes.number.isRequired, 22 | filter: React.PropTypes.string.isRequired, 23 | onSelectFilter: React.PropTypes.func.isRequired, 24 | onClearCompleted: React.PropTypes.func.isRequired, 25 | } 26 | 27 | export default Footer 28 | -------------------------------------------------------------------------------- /todo-redux/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import TodoInput from './TodoInput' 3 | 4 | const Header = (props) => ( 5 |
6 |

Todo App

7 | 14 |
15 | ) 16 | 17 | Header.propTypes = { 18 | newTodoText: React.PropTypes.string, 19 | onChange: React.PropTypes.func.isRequired, 20 | onEnter: React.PropTypes.func.isRequired, 21 | } 22 | 23 | export default Header 24 | -------------------------------------------------------------------------------- /todo-redux/src/components/MainSection.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import FilteredTodoList from '../containers/FilteredTodoList' 3 | 4 | const MainSection = (props) => ( 5 |
6 | 7 | 8 |
9 | ) 10 | 11 | MainSection.propTypes = { 12 | onToggleAll: React.PropTypes.func.isRequired, 13 | onDestroy: React.PropTypes.func.isRequired, 14 | } 15 | 16 | export default MainSection 17 | -------------------------------------------------------------------------------- /todo-redux/src/components/TodoCount.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const TodoCount = (props) => { 4 | const numActiveItem = props.numActiveItem > 0 ? props.numActiveItem : 'No' 5 | const unit = props.numActiveItem > 1 ? 'items' : 'item' 6 | 7 | return ( 8 | 9 | {numActiveItem} 10 |   11 | {unit} 12 | left 13 | 14 | ) 15 | } 16 | 17 | TodoCount.propTypes = { 18 | numActiveItem: React.PropTypes.number.isRequired, 19 | } 20 | 21 | export default TodoCount 22 | -------------------------------------------------------------------------------- /todo-redux/src/components/TodoInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classNames from 'classnames' 3 | 4 | const ENTER_KEY = 13 5 | 6 | const TodoInput = (props) => { 7 | const handleChange = (event) => { 8 | props.onChange(event.target.value) 9 | } 10 | 11 | const handleKeyDown = (event) => { 12 | if (event.which === ENTER_KEY) { 13 | props.onEnter(event.target.value.trim()) 14 | if (props.newTodo) { 15 | props.onChange('') 16 | } 17 | } 18 | } 19 | 20 | return ( 21 | 30 | ) 31 | } 32 | 33 | TodoInput.propTypes = { 34 | text: React.PropTypes.string, 35 | newTodo: React.PropTypes.bool, 36 | placeholder: React.PropTypes.string, 37 | onChange: React.PropTypes.func.isRequired, 38 | onEnter: React.PropTypes.func.isRequired, 39 | } 40 | 41 | export default TodoInput 42 | -------------------------------------------------------------------------------- /todo-redux/src/components/TodoItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classNames from 'classnames' 3 | 4 | const TodoItem = ({ todo, onClick, onDestroy }) => ( 5 |
  • 10 |
    11 | 18 | 19 |
    21 |
  • 22 | ) 23 | 24 | TodoItem.propTypes = { 25 | todo: React.PropTypes.object.isRequired, 26 | onClick: React.PropTypes.func.isRequired, 27 | onDestroy: React.PropTypes.func.isRequired, 28 | } 29 | 30 | export default TodoItem 31 | -------------------------------------------------------------------------------- /todo-redux/src/components/TodoList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import TodoItem from './TodoItem' 3 | 4 | const TodoList = ({ todos, onItemClick, onDestroy }) => { 5 | const todoList = todos.map((todo) => 6 | onItemClick(todo.id)} 10 | onDestroy={() => onDestroy(todo.id)} 11 | /> 12 | ) 13 | 14 | return ( 15 |
      16 | {todoList} 17 |
    18 | ) 19 | } 20 | 21 | TodoList.propTypes = { 22 | todos: React.PropTypes.array.isRequired, 23 | onItemClick: React.PropTypes.func.isRequired, 24 | onDestroy: React.PropTypes.func.isRequired, 25 | } 26 | 27 | export default TodoList 28 | -------------------------------------------------------------------------------- /todo-redux/src/constants/filters.js: -------------------------------------------------------------------------------- 1 | const ALL = 'ALL' 2 | const ACTIVE = 'ACTIVE' 3 | const COMPLETED = 'COMPLETED' 4 | 5 | export default { 6 | ALL, 7 | ACTIVE, 8 | COMPLETED, 9 | } 10 | -------------------------------------------------------------------------------- /todo-redux/src/containers/FilteredTodoList.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import TodoList from '../components/TodoList' 3 | import filters from '../constants/filters' 4 | import actions from '../actions/todos' 5 | 6 | const mapStateToProps = (state) => { 7 | let todos 8 | 9 | if (state.filter === filters.ACTIVE) { 10 | todos = state.todos.filter((todo) => todo.completed === false) 11 | } else if (state.filter === filters.COMPLETED) { 12 | todos = state.todos.filter(todo => todo.completed === true) 13 | } else { 14 | todos = state.todos 15 | } 16 | 17 | return { 18 | todos, 19 | } 20 | } 21 | 22 | const mapDispatchToProps = (dispatch) => ({ 23 | onItemClick: (id) => { 24 | dispatch(actions.toggleTodo(id)) 25 | }, 26 | }) 27 | 28 | export default connect(mapStateToProps, mapDispatchToProps)(TodoList) 29 | -------------------------------------------------------------------------------- /todo-redux/src/containers/TodoApp.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { bindActionCreators } from 'redux' 3 | import { connect } from 'react-redux' 4 | import todoActions from '../actions/todos' 5 | import Header from '../components/Header' 6 | import MainSection from '../components/MainSection' 7 | import Footer from '../components/Footer' 8 | 9 | const TodoApp = (props) => { 10 | const onNewTodoTextChange = (text) => { 11 | props.actions.changeNewTodoText(text) 12 | } 13 | 14 | const onAddTodo = (text) => { 15 | if (text !== '') { 16 | props.actions.addTodo(text) 17 | } 18 | } 19 | 20 | const onToggleAllTodos = () => { 21 | props.actions.toggleAllTodos() 22 | } 23 | 24 | const onDestroyTodo = (id) => { 25 | props.actions.destroyTodo(id) 26 | } 27 | 28 | const onSelectFilter = (filter) => { 29 | props.actions.filterTodo(filter) 30 | } 31 | 32 | const onClearCompleted = () => { 33 | props.actions.clearCompletedTodos() 34 | } 35 | 36 | const footer = props.todos.length > 0 ? 37 |