├── .gitignore ├── .babelrc ├── .coveralls.yml ├── test ├── mocha.opts ├── createActions.specs.js ├── createReducer.specs.js ├── createAction.specs.js └── createAsyncAction.specs.js ├── examples └── todomvc │ ├── src │ ├── constants │ │ ├── TodoFilters.js │ │ └── ActionTypes.js │ ├── reducers │ │ ├── index.js │ │ ├── todos.js │ │ └── todos.spec.js │ ├── components │ │ ├── Loading.js │ │ ├── Header.js │ │ ├── TodoTextInput.js │ │ ├── Header.spec.js │ │ ├── TodoItem.js │ │ ├── Footer.js │ │ ├── MainSection.js │ │ ├── TodoTextInput.spec.js │ │ ├── Footer.spec.js │ │ ├── TodoItem.spec.js │ │ └── MainSection.spec.js │ ├── index.js │ ├── containers │ │ └── App.js │ ├── shared │ │ └── loading.js │ └── actions │ │ ├── index.js │ │ └── index.spec.js │ ├── .gitignore │ ├── README.md │ ├── public │ └── index.html │ └── package.json ├── .travis.yml ├── webpack.config.js ├── package.json ├── README_zh_CN.MD ├── src └── index.js └── README.MD /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | coverage/ 4 | lib/ -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-1"] 3 | } 4 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: DhTQydq8d4ErpMN5Nt9JQUU1XggMx7V8t -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel-register,jsx:babel-register 2 | --recursive 3 | --ui bdd -------------------------------------------------------------------------------- /examples/todomvc/src/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 | -------------------------------------------------------------------------------- /examples/todomvc/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | -------------------------------------------------------------------------------- /examples/todomvc/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import todos from './todos' 3 | import {loadingReducer} from '../shared/loading' 4 | 5 | const rootReducer = combineReducers({ 6 | todos, 7 | loading: loadingReducer 8 | }) 9 | 10 | export default rootReducer 11 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Loading = ({show})=> { 4 | return show && ( 5 |
6 |

This is global loading mask...

7 |
8 | ) 9 | } 10 | 11 | export default Loading; -------------------------------------------------------------------------------- /examples/todomvc/src/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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | install: 5 | | 6 | npm install -g npm@latest 7 | npm --version 8 | npm prune 9 | npm install --registry http://registry.npmjs.org 10 | script: 11 | - npm run test:coverage 12 | after_script: 13 | - cat ./coverage/lcov.info | ./node_modules/.bin/coveralls -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: [ 3 | './src/index.js' 4 | ], 5 | output: { 6 | path: 'lib/', 7 | filename: 'index.js', 8 | library: 'redux-action-tools', 9 | libraryTarget: 'umd' 10 | }, 11 | module: { 12 | loaders: [{ 13 | test: /\.js$/, 14 | loaders: ['babel'], 15 | exclude: /node_modules/ 16 | }] 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /examples/todomvc/README.md: -------------------------------------------------------------------------------- 1 | # TodoMVC example 2 | 3 | This example is mainly copied from [Redux TodoMVC example](https://github.com/reactjs/redux/tree/master/examples/todomvc) 4 | 5 | I've changed all actions to [FSA actions](https://github.com/acdlite/flux-standard-action); 6 | 7 | And the addTodo action has been changed to an async action. 8 | 9 | I will add some more usage of redux-action-tools in this example (such as usage of meta) in future commits. -------------------------------------------------------------------------------- /examples/todomvc/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { createStore, applyMiddleware } from 'redux' 4 | import { Provider } from 'react-redux' 5 | import thunk from 'redux-thunk' 6 | import logger from 'redux-logger' 7 | import App from './containers/App' 8 | import reducer from './reducers' 9 | import loadingMiddleware from './shared/loading' 10 | import 'todomvc-app-css/index.css' 11 | 12 | const store = createStore( 13 | reducer, 14 | applyMiddleware( 15 | loadingMiddleware, 16 | thunk, 17 | logger, 18 | ) 19 | ) 20 | 21 | render( 22 | 23 | 24 | , 25 | document.getElementById('root') 26 | ) 27 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import TodoTextInput from './TodoTextInput' 4 | 5 | export default class Header extends Component { 6 | static propTypes = { 7 | addTodo: PropTypes.func.isRequired 8 | } 9 | 10 | handleSave = text => { 11 | if (text.length !== 0) { 12 | this.props.addTodo(text) 13 | } 14 | } 15 | 16 | render() { 17 | return ( 18 |
19 |

todos

20 | 23 |
24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/todomvc/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux TodoMVC Example 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/todomvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "enzyme": "^2.8.0", 7 | "react-addons-test-utils": "^15.3.0", 8 | "react-scripts": "^0.9.5" 9 | }, 10 | "dependencies": { 11 | "classnames": "^2.2.5", 12 | "lodash": "^4.17.4", 13 | "prop-types": "^15.5.8", 14 | "react": "^15.5.0", 15 | "react-dom": "^15.5.0", 16 | "react-redux": "^5.0.3", 17 | "react-test-renderer": "^15.5.4", 18 | "redux": "^3.5.2", 19 | "redux-action-tools": "^1.2.2", 20 | "redux-logger": "^3.0.6", 21 | "redux-thunk": "^2.2.0", 22 | "todomvc-app-css": "^2.1.0" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "react-scripts build", 27 | "eject": "react-scripts eject", 28 | "test": "react-scripts test" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/todomvc/src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { bindActionCreators } from 'redux' 4 | import { connect } from 'react-redux' 5 | import Header from '../components/Header' 6 | import MainSection from '../components/MainSection' 7 | import Loading from '../components/Loading' 8 | import * as TodoActions from '../actions' 9 | 10 | const App = ({todos, loading, actions}) => ( 11 |
12 |
13 | 14 | 15 |
16 | ) 17 | 18 | App.propTypes = { 19 | todos: PropTypes.array.isRequired, 20 | actions: PropTypes.object.isRequired 21 | } 22 | 23 | const mapStateToProps = state => ({ 24 | todos: state.todos, 25 | loading: state.loading, 26 | }) 27 | 28 | const mapDispatchToProps = dispatch => ({ 29 | actions: bindActionCreators(TodoActions, dispatch) 30 | }) 31 | 32 | export default connect( 33 | mapStateToProps, 34 | mapDispatchToProps 35 | )(App) 36 | -------------------------------------------------------------------------------- /examples/todomvc/src/shared/loading.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { ASYNC_PHASES } from 'redux-action-tools' 3 | 4 | const actionTypes = { 5 | ASYNC_STARTED: 'ASYNC_STARTED', 6 | ASYNC_ENDED: 'ASYNC_ENDED', 7 | }; 8 | 9 | export function loadingReducer(state = false, action) { 10 | switch (action.type) { 11 | case actionTypes.ASYNC_STARTED: 12 | return true; 13 | case actionTypes.ASYNC_ENDED: 14 | return false; 15 | default: return state; 16 | } 17 | } 18 | 19 | export default function loadingMiddleWare({ dispatch }) { 20 | return next => (action) => { 21 | const asyncPhase = _.get(action, 'meta.asyncPhase'); 22 | const omitLoading = _.get(action, 'meta.omitLoading'); 23 | 24 | if (asyncPhase && !omitLoading) { 25 | dispatch({ 26 | type: asyncPhase === ASYNC_PHASES.START 27 | ? actionTypes.ASYNC_STARTED 28 | : actionTypes.ASYNC_ENDED, 29 | payload: { 30 | source: 'ACTION', 31 | action, 32 | }, 33 | }); 34 | } 35 | 36 | return next(action); 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /examples/todomvc/src/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes' 2 | import {createAsyncAction} from 'redux-action-tools' 3 | 4 | // return a Promise maybe (70%) fulfilled 1 second later 5 | const maybe = (data)=> (new Promise(function(resolve, reject) { 6 | setTimeout(()=> { 7 | Math.random() > 0.3 ? resolve(data) : reject(new Error('Ops, async operation failed')); 8 | }, 1000) 9 | })) 10 | 11 | export const addTodo = createAsyncAction(types.ADD_TODO, text=> maybe({text})); 12 | 13 | // The metaCreator can be either an object or a function which presents: (payload, defaultMeta) => finalMeta. 14 | // When you provide an object, it will be merged with defaultMeta, equalize to (payload, defaultMeta) => ({...defaultMeta, ...yourObj}) 15 | export const deleteTodo = createAsyncAction(types.DELETE_TODO, id=> maybe({id}), { omitLoading: true }) 16 | 17 | // export const addTodo = text => ({ type: types.ADD_TODO, text }) 18 | // export const deleteTodo = id => ({ type: types.DELETE_TODO, payload: {id} }) 19 | export const editTodo = (id, text) => ({ type: types.EDIT_TODO, payload: {id, text} }) 20 | export const completeTodo = id => ({ type: types.COMPLETE_TODO, payload: {id} }) 21 | export const completeAll = () => ({ type: types.COMPLETE_ALL }) 22 | export const clearCompleted = () => ({ type: types.CLEAR_COMPLETED }) 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-action-tools", 3 | "version": "1.2.2", 4 | "description": "redux action tools with full async support inspired by redux-action", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib", 8 | "src" 9 | ], 10 | "scripts": { 11 | "build": "webpack", 12 | "test": "mocha", 13 | "test:coverage": "rimraf ./coverage && istanbul cover ./node_modules/.bin/_mocha", 14 | "prepublish": "rimraf ./lib && npm run test && npm run build" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/kpaxqin/redux-action-tools" 19 | }, 20 | "keywords": [ 21 | "redux", 22 | "action", 23 | "async", 24 | "promise", 25 | "flux", 26 | "fsa" 27 | ], 28 | "author": "kpaxqin", 29 | "license": "ISC", 30 | "devDependencies": { 31 | "babel-core": "^6.9.0", 32 | "babel-eslint": "^6.1.2", 33 | "babel-loader": "^6.2.4", 34 | "babel-preset-es2015": "^6.9.0", 35 | "babel-preset-stage-1": "^6.5.0", 36 | "chai": "^3.5.0", 37 | "coveralls": "^2.11.12", 38 | "flux-standard-action": "^0.6.1", 39 | "istanbul": "^1.1.0-alpha.1", 40 | "mocha": "^3.0.2", 41 | "rimraf": "^2.5.4", 42 | "sinon": "^1.17.5", 43 | "webpack": "^1.12.6" 44 | }, 45 | "dependencies": { 46 | "camelcase": "^3.0.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/todomvc/src/actions/index.spec.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes' 2 | import * as actions from './index' 3 | 4 | describe('todo actions', () => { 5 | it('addTodo should create ADD_TODO action', () => { 6 | expect(actions.addTodo('Use Redux')).toEqual({ 7 | type: types.ADD_TODO, 8 | text: 'Use Redux' 9 | }) 10 | }) 11 | 12 | it('deleteTodo should create DELETE_TODO action', () => { 13 | expect(actions.deleteTodo(1)).toEqual({ 14 | type: types.DELETE_TODO, 15 | id: 1 16 | }) 17 | }) 18 | 19 | it('editTodo should create EDIT_TODO action', () => { 20 | expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({ 21 | type: types.EDIT_TODO, 22 | id: 1, 23 | text: 'Use Redux everywhere' 24 | }) 25 | }) 26 | 27 | it('completeTodo should create COMPLETE_TODO action', () => { 28 | expect(actions.completeTodo(1)).toEqual({ 29 | type: types.COMPLETE_TODO, 30 | id: 1 31 | }) 32 | }) 33 | 34 | it('completeAll should create COMPLETE_ALL action', () => { 35 | expect(actions.completeAll()).toEqual({ 36 | type: types.COMPLETE_ALL 37 | }) 38 | }) 39 | 40 | it('clearCompleted should create CLEAR_COMPLETED action', () => { 41 | expect(actions.clearCompleted()).toEqual({ 42 | type: types.CLEAR_COMPLETED 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /examples/todomvc/src/reducers/todos.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from 'redux-action-tools' 2 | import { ADD_TODO, DELETE_TODO, EDIT_TODO, COMPLETE_TODO, COMPLETE_ALL, CLEAR_COMPLETED } from '../constants/ActionTypes' 3 | 4 | const initialState = [ 5 | { 6 | text: 'Use Redux', 7 | completed: false, 8 | id: 0 9 | } 10 | ] 11 | 12 | const reducer = createReducer() 13 | .when(ADD_TODO) 14 | .done((state, {payload: {text}}) => [ 15 | { 16 | id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1, 17 | completed: false, 18 | text 19 | }, 20 | ...state 21 | ]) 22 | .when(DELETE_TODO) 23 | .done((state, {payload})=> state.filter(todo => 24 | todo.id !== payload.id 25 | )) 26 | .when(EDIT_TODO, (state, {payload})=> state.map(todo => 27 | todo.id === payload.id ? 28 | { ...todo, text: payload.text } : 29 | todo 30 | )) 31 | .when(COMPLETE_TODO, (state, {payload})=> state.map(todo => 32 | todo.id === payload.id ? 33 | { ...todo, completed: !todo.completed } : 34 | todo 35 | )) 36 | .when(COMPLETE_ALL, (state)=> { 37 | const areAllMarked = state.every(todo => todo.completed) 38 | return state.map(todo => ({ 39 | ...todo, 40 | completed: !areAllMarked 41 | })) 42 | }) 43 | .when(CLEAR_COMPLETED, (state)=> state.filter(todo => todo.completed === false)) 44 | .build(initialState); 45 | 46 | export default reducer; 47 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/TodoTextInput.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | 5 | export default class TodoTextInput extends Component { 6 | static propTypes = { 7 | onSave: PropTypes.func.isRequired, 8 | text: PropTypes.string, 9 | placeholder: PropTypes.string, 10 | editing: PropTypes.bool, 11 | newTodo: PropTypes.bool 12 | } 13 | 14 | state = { 15 | text: this.props.text || '' 16 | } 17 | 18 | handleSubmit = e => { 19 | const text = e.target.value.trim() 20 | if (e.which === 13) { 21 | this.props.onSave(text) 22 | if (this.props.newTodo) { 23 | this.setState({ text: '' }) 24 | } 25 | } 26 | } 27 | 28 | handleChange = e => { 29 | this.setState({ text: e.target.value }) 30 | } 31 | 32 | handleBlur = e => { 33 | if (!this.props.newTodo) { 34 | this.props.onSave(e.target.value) 35 | } 36 | } 37 | 38 | render() { 39 | return ( 40 | 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/Header.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRenderer } from 'react-test-renderer/shallow'; 3 | 4 | import Header from './Header' 5 | import TodoTextInput from './TodoTextInput' 6 | 7 | const setup = () => { 8 | const props = { 9 | addTodo: jest.fn() 10 | } 11 | 12 | const renderer = 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).not.toBeCalled() 46 | input.props.onSave('Use Redux') 47 | expect(props.addTodo).toBeCalled() 48 | }) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /test/createActions.specs.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { createActions } from '../src'; 3 | 4 | describe('createActions', () => { 5 | 6 | it('should return a map of action creators', () => { 7 | const { actionOne, actionTwo } = createActions({ 8 | ACTION_ONE: (foo, bar) => (foo + bar), 9 | ACTION_TWO: (foo, bar) => (foo * bar) 10 | }); 11 | 12 | expect(actionOne(1, 2)).to.deep.equal({ 13 | type: 'ACTION_ONE', 14 | payload: 3 15 | }); 16 | 17 | expect(actionTwo(2, 3)).to.deep.equal({ 18 | type: 'ACTION_TWO', 19 | payload: 6 20 | }); 21 | }); 22 | 23 | it('should use identity when config for action is undefined', () => { 24 | const { actionOne, actionTwo } = createActions({ 25 | ACTION_ONE: undefined, 26 | ACTION_TWO: undefined 27 | }); 28 | 29 | expect(actionOne(1)).to.deep.equal({ 30 | type: 'ACTION_ONE', 31 | payload: 1 32 | }); 33 | 34 | expect(actionTwo(2)).to.deep.equal({ 35 | type: 'ACTION_TWO', 36 | payload: 2 37 | }); 38 | }); 39 | 40 | it('should use the payload and meta creator provided in config', () => { 41 | const { actionOne, actionTwo } = createActions({ 42 | ACTION_ONE: { 43 | payload: (foo, bar) => (foo + bar), 44 | meta: (foo, bar) => foo 45 | }, 46 | ACTION_TWO: { 47 | payload: (foo, bar) => (foo * bar), 48 | meta: (foo, bar) => bar 49 | } 50 | }); 51 | 52 | expect(actionOne(1, 2)).to.deep.equal({ 53 | type: 'ACTION_ONE', 54 | payload: 3, 55 | meta: 1 56 | }); 57 | 58 | expect(actionTwo(2, 3)).to.deep.equal({ 59 | type: 'ACTION_TWO', 60 | payload: 6, 61 | meta: 3 62 | }); 63 | }); 64 | 65 | }); -------------------------------------------------------------------------------- /examples/todomvc/src/components/TodoItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | import TodoTextInput from './TodoTextInput' 5 | 6 | export default class TodoItem extends Component { 7 | static propTypes = { 8 | todo: PropTypes.object.isRequired, 9 | editTodo: PropTypes.func.isRequired, 10 | deleteTodo: PropTypes.func.isRequired, 11 | completeTodo: PropTypes.func.isRequired 12 | } 13 | 14 | state = { 15 | editing: false 16 | } 17 | 18 | handleDoubleClick = () => { 19 | this.setState({ editing: true }) 20 | } 21 | 22 | handleSave = (id, text) => { 23 | if (text.length === 0) { 24 | this.props.deleteTodo(id) 25 | } else { 26 | this.props.editTodo(id, text) 27 | } 28 | this.setState({ editing: false }) 29 | } 30 | 31 | render() { 32 | const { todo, completeTodo, deleteTodo } = this.props 33 | 34 | let element 35 | if (this.state.editing) { 36 | element = ( 37 | this.handleSave(todo.id, text)} /> 40 | ) 41 | } else { 42 | element = ( 43 |
44 | completeTodo(todo.id)} /> 48 | 51 |
54 | ) 55 | } 56 | 57 | return ( 58 |
  • 62 | {element} 63 |
  • 64 | ) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters' 5 | 6 | const FILTER_TITLES = { 7 | [SHOW_ALL]: 'All', 8 | [SHOW_ACTIVE]: 'Active', 9 | [SHOW_COMPLETED]: 'Completed' 10 | } 11 | 12 | export default class Footer extends Component { 13 | static propTypes = { 14 | completedCount: PropTypes.number.isRequired, 15 | activeCount: PropTypes.number.isRequired, 16 | filter: PropTypes.string.isRequired, 17 | onClearCompleted: PropTypes.func.isRequired, 18 | onShow: PropTypes.func.isRequired 19 | } 20 | 21 | renderTodoCount() { 22 | const { activeCount } = this.props 23 | const itemWord = activeCount === 1 ? 'item' : 'items' 24 | 25 | return ( 26 | 27 | {activeCount || 'No'} {itemWord} left 28 | 29 | ) 30 | } 31 | 32 | renderFilterLink(filter) { 33 | const title = FILTER_TITLES[filter] 34 | const { filter: selectedFilter, onShow } = this.props 35 | 36 | return ( 37 | onShow(filter)}> 40 | {title} 41 | 42 | ) 43 | } 44 | 45 | renderClearButton() { 46 | const { completedCount, onClearCompleted } = this.props 47 | if (completedCount > 0) { 48 | return ( 49 | 53 | ) 54 | } 55 | } 56 | 57 | render() { 58 | return ( 59 |
    60 | {this.renderTodoCount()} 61 |
      62 | {[ SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED ].map(filter => 63 |
    • 64 | {this.renderFilterLink(filter)} 65 |
    • 66 | )} 67 |
    68 | {this.renderClearButton()} 69 |
    70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/MainSection.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import TodoItem from './TodoItem' 4 | import Footer from './Footer' 5 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters' 6 | 7 | const TODO_FILTERS = { 8 | [SHOW_ALL]: () => true, 9 | [SHOW_ACTIVE]: todo => !todo.completed, 10 | [SHOW_COMPLETED]: todo => todo.completed 11 | } 12 | 13 | export default class MainSection extends Component { 14 | static propTypes = { 15 | todos: PropTypes.array.isRequired, 16 | actions: PropTypes.object.isRequired 17 | } 18 | 19 | state = { filter: SHOW_ALL } 20 | 21 | handleClearCompleted = () => { 22 | this.props.actions.clearCompleted() 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 |