├── 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 |
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 |
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 |
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 |
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 | : ''
44 |
45 | return (
46 |
47 |
52 |
53 | {footer}
54 |
55 | )
56 | }
57 |
58 | TodoApp.propTypes = {
59 | newTodoText: React.PropTypes.string,
60 | actions: React.PropTypes.object.isRequired,
61 | todos: React.PropTypes.array.isRequired,
62 | numActiveItem: React.PropTypes.number.isRequired,
63 | numCompletedItem: React.PropTypes.number.isRequired,
64 | filter: React.PropTypes.string.isRequired,
65 | }
66 |
67 | const mapStateToProps = (state) => ({
68 | newTodoText: state.newTodoText,
69 | todos: state.todos,
70 | numActiveItem: state.todos.filter((todo) => todo.completed === false).length,
71 | numCompletedItem: state.todos.filter((todo) => todo.completed === true).length,
72 | filter: state.filter,
73 | })
74 |
75 | const mapDispatchToProps = (dispatch) => ({
76 | actions: bindActionCreators(todoActions, dispatch),
77 | })
78 |
79 | export default connect(mapStateToProps, mapDispatchToProps)(TodoApp)
80 |
--------------------------------------------------------------------------------
/todo-redux/src/containers/TodoList.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux'
2 | import TodoList from '../components/TodoList'
3 | import actions from '../actions/todos'
4 |
5 | const mapStateToProps = (state) => ({
6 | todos: state.todos,
7 | })
8 |
9 | const mapDispatchToProps = (dispatch) => ({
10 | onItemClick: (id) => {
11 | dispatch(actions.toggleTodo(id))
12 | },
13 | })
14 |
15 | export default connect(mapStateToProps, mapDispatchToProps)(TodoList)
16 |
--------------------------------------------------------------------------------
/todo-redux/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Todo App
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/todo-redux/src/index.js:
--------------------------------------------------------------------------------
1 | import 'todomvc-app-css/index.css'
2 | import React from 'react'
3 | import ReactDOM from 'react-dom'
4 | import { Provider } from 'react-redux'
5 | import { createStore } from 'redux'
6 | import reducer from './reducers'
7 | import TodoApp from './containers/TodoApp'
8 |
9 | let store = createStore(
10 | reducer,
11 | window.devToolsExtension && window.devToolsExtension()
12 | )
13 |
14 | // check if HMR is enabled
15 | if (module.hot) {
16 | module.hot.accept('./reducers', () => {
17 | const nextReducer = require('./reducers').default // eslint-disable-line global-require
18 |
19 | store.replaceReducer(nextReducer)
20 | })
21 | }
22 |
23 | ReactDOM.render(
24 |
25 |
26 |
27 | , document.getElementById('root')
28 | )
29 |
--------------------------------------------------------------------------------
/todo-redux/src/reducers/filterTodo.js:
--------------------------------------------------------------------------------
1 | import types from '../actions/types'
2 | import filters from '../constants/filters'
3 |
4 | const initialState = filters.ALL
5 |
6 | const filterTodoReducer = (state = initialState, action) => {
7 | switch (action.type) {
8 | case types.FILTER_TODO: {
9 | return action.payload.filter
10 | }
11 | default: {
12 | return state
13 | }
14 | }
15 | }
16 |
17 | export default filterTodoReducer
18 |
--------------------------------------------------------------------------------
/todo-redux/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import todosReducer from './todos'
3 | import newTodoReducer from './newTodo'
4 | import filterTodoReducer from './filterTodo'
5 |
6 | const rootReducer = combineReducers({
7 | todos: todosReducer,
8 | newTodoText: newTodoReducer,
9 | filter: filterTodoReducer,
10 | })
11 |
12 | export default rootReducer
13 |
--------------------------------------------------------------------------------
/todo-redux/src/reducers/newTodo.js:
--------------------------------------------------------------------------------
1 | import types from '../actions/types'
2 |
3 | const initialState = ''
4 |
5 | const newTodoReducer = (state = initialState, action) => {
6 | switch (action.type) {
7 | case types.CHANGE_NEW_TODO_TEXT: {
8 | return action.payload.text
9 | }
10 | default: {
11 | return state
12 | }
13 | }
14 | }
15 |
16 | export default newTodoReducer
17 |
--------------------------------------------------------------------------------
/todo-redux/src/reducers/todos.js:
--------------------------------------------------------------------------------
1 | import types from '../actions/types'
2 |
3 | const initialState = []
4 |
5 | const todosReducer = (state = initialState, action) => {
6 | switch (action.type) {
7 | case types.ADD_TODO: {
8 | const nextId = state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1
9 | const newTodo = {
10 | id: nextId,
11 | title: action.payload.title,
12 | completed: false,
13 | }
14 | return [...state, newTodo]
15 | }
16 | case types.TOGGLE_TODO: {
17 | return state.map(todo => {
18 | if (todo.id === action.payload.id) {
19 | return { ...todo, completed: !todo.completed }
20 | }
21 | return todo
22 | })
23 | }
24 | case types.TOGGLE_ALL_TODOS: {
25 | const isAllCompleted = state.every(todo => todo.completed)
26 |
27 | return state.map(todo => {
28 | if (isAllCompleted) {
29 | return { ...todo, completed: false }
30 | }
31 | return { ...todo, completed: true }
32 | })
33 | }
34 | case types.DESTROY_TODO: {
35 | return state.filter(todo => todo.id !== action.payload.id)
36 | }
37 | case types.CLEAR_COMPLETED_TODOS: {
38 | return state.filter(todo => todo.completed === false)
39 | }
40 | default:
41 | return state
42 | }
43 | }
44 |
45 | export default todosReducer
46 |
--------------------------------------------------------------------------------
/todo-redux/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true,
4 | },
5 | "rules": {
6 | "no-unused-expressions": "off",
7 | },
8 | }
9 |
--------------------------------------------------------------------------------
/todo-redux/test/actions/todos.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from '../test-helper'
2 | import actions from '../../src/actions/todos'
3 | import types from '../../src/actions/types'
4 | import filters from '../../src/constants/filters'
5 |
6 | describe('Todos Actions', () => {
7 | it('should create action with type CHANGE_NEW_TODO_TEXT', () => {
8 | const action = actions.changeNewTodoText('Feed my cat')
9 | const expectedAction = {
10 | type: types.CHANGE_NEW_TODO_TEXT,
11 | payload: {
12 | text: 'Feed my cat',
13 | },
14 | }
15 | expect(action).to.deep.equal(expectedAction)
16 | })
17 |
18 | it('should create action with type ADD_TODO', () => {
19 | const action = actions.addTodo('Prepare a slide deck')
20 | const expectedAction = {
21 | type: types.ADD_TODO,
22 | payload: {
23 | title: 'Prepare a slide deck',
24 | },
25 | }
26 | expect(action).to.deep.equal(expectedAction)
27 | })
28 |
29 | it('should create action with type TOGGLE_TODO', () => {
30 | const action = actions.toggleTodo(1)
31 | const expectedAction = {
32 | type: types.TOGGLE_TODO,
33 | payload: {
34 | id: 1,
35 | },
36 | }
37 | expect(action).to.deep.equal(expectedAction)
38 | })
39 |
40 | it('should create action with type TOGGLE_ALL_TODOS', () => {
41 | const action = actions.toggleAllTodos()
42 | const expectedAction = {
43 | type: types.TOGGLE_ALL_TODOS,
44 | }
45 | expect(action).to.deep.equal(expectedAction)
46 | })
47 |
48 | it('should create action with type DESTROY_TODO', () => {
49 | const action = actions.destroyTodo(1)
50 | const expectedAction = {
51 | type: types.DESTROY_TODO,
52 | payload: {
53 | id: 1,
54 | },
55 | }
56 | expect(action).to.deep.equal(expectedAction)
57 | })
58 |
59 | it('should create action with type FILTER_TODO', () => {
60 | const action = actions.filterTodo(filters.ALL)
61 | const expectedAction = {
62 | type: types.FILTER_TODO,
63 | payload: {
64 | filter: filters.ALL,
65 | },
66 | }
67 | expect(action).to.deep.equal(expectedAction)
68 | })
69 |
70 | it('should create action with type CLEAR_COMPLETED_TODOS', () => {
71 | const action = actions.clearCompletedTodos()
72 | const expectedAction = {
73 | type: types.CLEAR_COMPLETED_TODOS,
74 | }
75 | expect(action).to.deep.equal(expectedAction)
76 | })
77 | })
78 |
--------------------------------------------------------------------------------
/todo-redux/test/components/ClearCompletedButton.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { expect, shallow, sinon } from '../test-helper'
3 | import ClearCompletedButton from '../../src/components/ClearCompletedButton'
4 |
5 | describe('ClearCompletedButton', () => {
6 | let wrapper
7 | let props
8 |
9 | beforeEach(() => {
10 | props = {
11 | onClearCompleted: sinon.spy(),
12 | }
13 | wrapper = shallow()
14 | })
15 |
16 | it('should render correct structure', () => {
17 | expect(wrapper).to.have.tagName('button')
18 | expect(wrapper).to.have.className('clear-completed')
19 | })
20 |
21 | it('should call onClearCompleted on click', () => {
22 | wrapper.simulate('click')
23 | expect(props.onClearCompleted).to.have.been.called
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/todo-redux/test/components/Filters.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { expect, shallow, sinon } from '../test-helper'
3 | import Filters from '../../src/components/Filters'
4 | import filters from '../../src/constants/filters'
5 |
6 | describe('Filters', () => {
7 | let wrapper
8 | let props
9 |
10 | beforeEach(() => {
11 | props = {
12 | selectedFilter: filters.ALL,
13 | onSelectFilter: sinon.spy(),
14 | }
15 | wrapper = shallow()
16 | })
17 |
18 | it('should render correct structure', () => {
19 | expect(wrapper).to.have.tagName('ul')
20 | expect(wrapper).to.have.className('filters')
21 | expect(wrapper).to.have.exactly(3).descendants('li')
22 |
23 | const lis = wrapper.find('li')
24 | expect(lis.at(0).find('a')).to.present()
25 | expect(lis.at(0).find('a')).to.have.text('All')
26 | expect(lis.at(1).find('a')).to.present()
27 | expect(lis.at(1).find('a')).to.have.text('Active')
28 | expect(lis.at(2).find('a')).to.present()
29 | expect(lis.at(2).find('a')).to.have.text('Completed')
30 | })
31 |
32 | it('should have a selected class on selected filter', () => {
33 | props.selectedFilter = filters.ALL
34 | wrapper.setProps(props)
35 | expect(wrapper.find('li').at(0).find('a')).to.have.className('selected')
36 |
37 | props.selectedFilter = filters.ACTIVE
38 | wrapper.setProps(props)
39 | expect(wrapper.find('li').at(1).find('a')).to.have.className('selected')
40 |
41 | props.selectedFilter = filters.COMPLETED
42 | wrapper.setProps(props)
43 | expect(wrapper.find('li').at(2).find('a')).to.have.className('selected')
44 | })
45 |
46 | it('should call onSelectFilter with a selected filter', () => {
47 | const lis = wrapper.find('li')
48 | lis.at(0).find('a').simulate('click')
49 | expect(props.onSelectFilter).have.been.calledWith(filters.ALL)
50 |
51 | lis.at(1).find('a').simulate('click')
52 | expect(props.onSelectFilter).have.been.calledWith(filters.ACTIVE)
53 |
54 | lis.at(2).find('a').simulate('click')
55 | expect(props.onSelectFilter).have.been.calledWith(filters.COMPLETED)
56 | })
57 | })
58 |
--------------------------------------------------------------------------------
/todo-redux/test/components/Footer.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { expect, shallow, sinon } from '../test-helper'
3 | import Footer from '../../src/components/Footer'
4 | import filters from '../../src/constants/filters'
5 |
6 | describe('Footer', () => {
7 | let wrapper
8 | let props
9 |
10 | beforeEach(() => {
11 | props = {
12 | numActiveItem: 3,
13 | numCompletedItem: 2,
14 | filter: filters.ALL,
15 | onSelectFilter: sinon.stub(),
16 | onClearCompleted: sinon.stub(),
17 | }
18 | wrapper = shallow()
19 | })
20 |
21 | it('should render correct structure', () => {
22 | expect(wrapper).to.have.tagName('footer')
23 | expect(wrapper).to.have.className('footer')
24 | expect(wrapper).to.have.descendants('TodoCount')
25 | expect(wrapper).to.have.descendants('Filters')
26 | expect(wrapper).to.have.descendants('ClearCompletedButton')
27 | })
28 |
29 | it('should hide ClearCompletedButton if there is no completed item', () => {
30 | props.numCompletedItem = 0
31 | wrapper.setProps(props)
32 | expect(wrapper).to.not.have.descendants('ClearCompletedButton')
33 | })
34 |
35 | it('should pass numActiveItem prop to TodoCount', () => {
36 | expect(wrapper.find('TodoCount')).to.have.prop('numActiveItem', 3)
37 | })
38 |
39 | it('should pass selectedFilter to Filters', () => {
40 | expect(wrapper.find('Filters')).to.have.prop('selectedFilter', props.filter)
41 | })
42 |
43 | it('should pass onSelectFilter to Filters', () => {
44 | expect(wrapper.find('Filters')).to.have.prop('onSelectFilter', props.onSelectFilter)
45 | })
46 |
47 | it('should pass onClearCompleted to ClearCompletedButton', () => {
48 | expect(wrapper.find('ClearCompletedButton'))
49 | .to.have.prop('onClearCompleted', props.onClearCompleted)
50 | })
51 | })
52 |
--------------------------------------------------------------------------------
/todo-redux/test/components/Header.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { expect, shallow, sinon } from '../test-helper'
3 | import Header from '../../src/components/Header'
4 |
5 | describe('Header', () => {
6 | let wrapper
7 | let props
8 |
9 | beforeEach(() => {
10 | props = {
11 | newTodoText: '',
12 | onChange: sinon.spy(),
13 | onEnter: sinon.spy(),
14 | }
15 | wrapper = shallow()
16 | })
17 |
18 | it('should render correct structure', () => {
19 | expect(wrapper).to.have.tagName('header')
20 | expect(wrapper).to.have.className('header')
21 | expect(wrapper).to.have.descendants('h1')
22 | expect(wrapper).to.have.descendants('TodoInput')
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/todo-redux/test/components/MainSection.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { expect, reduxMount, sinon } from '../test-helper'
3 | import MainSection from '../../src/components/MainSection'
4 |
5 | describe('MainSection', () => {
6 | let wrapper
7 | let props
8 | let state
9 |
10 | beforeEach(() => {
11 | props = {
12 | onDestroy: sinon.stub(),
13 | onToggleAll: sinon.stub(),
14 | }
15 | state = {
16 | todos: [],
17 | }
18 | wrapper = reduxMount(, state)
19 | })
20 |
21 | it('should render correct structure', () => {
22 | expect(wrapper).to.have.tagName('section')
23 | expect(wrapper).to.have.className('main')
24 | expect(wrapper.find('input')).to.be.present()
25 | expect(wrapper.find('input')).to.have.className('toggle-all')
26 | expect(wrapper.find('TodoList')).to.present()
27 | })
28 |
29 | it('should pass onDestroy prop to TodoList', () => {
30 | expect(wrapper.find('TodoList')).to.have.prop('onDestroy', props.onDestroy)
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/todo-redux/test/components/TodoCount.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { expect, shallow } from '../test-helper'
3 | import TodoCount from '../../src/components/TodoCount'
4 |
5 | describe('TodoCount', () => {
6 | let wrapper
7 | let props
8 |
9 | beforeEach(() => {
10 | props = {
11 | numActiveItem: 3,
12 | }
13 | wrapper = shallow()
14 | })
15 |
16 | it('should render correct structure', () => {
17 | expect(wrapper).to.have.tagName('span')
18 | expect(wrapper).to.have.className('todo-count')
19 | expect(wrapper.childAt(0)).to.have.tagName('strong')
20 | expect(wrapper.childAt(1)).to.have.tagName('span')
21 | expect(wrapper.childAt(2)).to.have.tagName('span')
22 | expect(wrapper.childAt(3)).to.have.tagName('span')
23 | })
24 |
25 | it('should display correct number of items left', () => {
26 | expect(wrapper).to.have.text('3 items left')
27 | })
28 |
29 | it('should display unit in singular term', () => {
30 | props.numActiveItem = 1
31 | wrapper.setProps(props)
32 | expect(wrapper).to.have.text('1 item left')
33 | })
34 |
35 | it('should display no item left if numItem is zero', () => {
36 | props.numActiveItem = 0
37 | wrapper.setProps(props)
38 | expect(wrapper).to.have.text('No item left')
39 | })
40 | })
41 |
--------------------------------------------------------------------------------
/todo-redux/test/components/TodoInput.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { expect, shallow, sinon } from '../test-helper'
3 | import TodoInput from '../../src/components/TodoInput'
4 |
5 | describe('TodoInput', () => {
6 | let wrapper
7 | let props
8 |
9 | beforeEach(() => {
10 | props = {
11 | text: '',
12 | newTodo: false,
13 | placeholder: 'What needs to be done?',
14 | onChange: sinon.spy(),
15 | onEnter: sinon.spy(),
16 | }
17 | wrapper = shallow()
18 | })
19 |
20 | it('should have a correct tag', () => {
21 | expect(wrapper).to.have.tagName('input')
22 | })
23 |
24 | it('should have a correct class', () => {
25 | expect(wrapper).to.have.className('todo-input')
26 | expect(wrapper).to.not.have.className('new-todo')
27 | })
28 |
29 | it('should have new-todo class for newTodo input', () => {
30 | props.newTodo = true
31 | wrapper.setProps(props)
32 |
33 | expect(wrapper).to.have.className('todo-input')
34 | expect(wrapper).to.have.className('new-todo')
35 | })
36 |
37 | it('should have placeholder text', () => {
38 | expect(wrapper).to.have.attr('placeholder', 'What needs to be done?')
39 | })
40 |
41 | it('should display text from props', () => {
42 | expect(wrapper).to.have.value('')
43 |
44 | props.text = 'My new task'
45 | wrapper.setProps(props)
46 |
47 | expect(wrapper).to.have.value('My new task')
48 | })
49 |
50 | it('should call props.onChange on change event', () => {
51 | wrapper.simulate('change', { target: { value: 'My new task' } })
52 |
53 | expect(props.onChange).to.have.been.calledWith('My new task')
54 | })
55 |
56 | it('should not call props.onAddTodo when hitting enter on existing Todo', () => {
57 | const ENTER_KEY = 13
58 |
59 | props.text = 'My new task'
60 | props.newTodo = false
61 | wrapper.setProps(props)
62 | expect(wrapper).to.have.value('My new task')
63 |
64 | wrapper.simulate('keyDown', { which: ENTER_KEY, target: { value: 'My new task' } })
65 | expect(props.onEnter).to.have.been.calledWith('My new task')
66 | expect(props.onChange).to.not.have.been.called
67 | })
68 |
69 | it('should call props.onAddTodo when hitting enter on new Todo', () => {
70 | const ENTER_KEY = 13
71 |
72 | props.text = 'My new task'
73 | props.newTodo = true
74 | wrapper.setProps(props)
75 | expect(wrapper).to.have.value('My new task')
76 |
77 | wrapper.simulate('keyDown', { which: ENTER_KEY, target: { value: 'My new task' } })
78 | expect(props.onEnter).to.have.been.calledWith('My new task')
79 | expect(props.onChange).to.have.been.calledWith('')
80 | })
81 | })
82 |
--------------------------------------------------------------------------------
/todo-redux/test/components/TodoItem.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { expect, shallow, sinon } from '../test-helper'
3 | import TodoItem from '../../src/components/TodoItem'
4 |
5 | describe('TodoItem', () => {
6 | let wrapper
7 | let props
8 |
9 | beforeEach(() => {
10 | props = {
11 | todo: {
12 | id: 1,
13 | title: 'Prepare a slide deck',
14 | completed: false,
15 | },
16 | onClick: sinon.spy(),
17 | onDestroy: sinon.spy(),
18 | }
19 | wrapper = shallow()
20 | })
21 |
22 | it('should have correct tags and class structure', () => {
23 | expect(wrapper).to.have.className('todo-item')
24 | expect(wrapper).to.have.tagName('li')
25 |
26 | const div = wrapper.children()
27 | expect(div).to.have.tagName('div')
28 | expect(div).to.have.className('view')
29 |
30 | const input = div.childAt(0)
31 | expect(input).to.have.tagName('input')
32 | expect(input).to.have.className('toggle')
33 |
34 | const label = div.childAt(1)
35 | expect(label).to.have.tagName('label')
36 |
37 | const button = div.childAt(2)
38 | expect(button).to.have.tagName('button')
39 | expect(button).to.have.className('destroy')
40 | })
41 |
42 | it('should not have a completed class for incomplete item', () => {
43 | expect(wrapper).to.not.have.className('completed')
44 | })
45 |
46 | it('should have a completed class for completed item', () => {
47 | props.todo.completed = true
48 | wrapper.setProps(props)
49 | expect(wrapper).to.have.className('completed')
50 | })
51 |
52 | it('should call onClick when clicking on a toggle checkbox', () => {
53 | wrapper.find('.toggle').simulate('click')
54 | expect(props.onClick).to.have.been.called
55 | })
56 |
57 | it('should call onDestroy when clicking on a delete mark', () => {
58 | wrapper.find('.destroy').simulate('click')
59 | expect(props.onDestroy).to.have.been.calledWith(props.todo.id)
60 | })
61 |
62 | it('should change checked attribute of a toggle checkbox', () => {
63 | props.todo.completed = true
64 | wrapper.setProps(props)
65 | expect(wrapper.find('.toggle')).to.have.attr('checked')
66 |
67 | props.todo.completed = false
68 | wrapper.setProps(props)
69 | expect(wrapper.find('.toggle')).to.not.have.attr('checked')
70 | })
71 | })
72 |
--------------------------------------------------------------------------------
/todo-redux/test/components/TodoList.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { expect, mount, shallow, sinon } from '../test-helper'
3 | import TodoList from '../../src/components/TodoList'
4 | import TodoItem from '../../src/components/TodoItem'
5 |
6 | describe('TodoList', () => {
7 | let wrapper
8 | let props
9 |
10 | beforeEach(() => {
11 | props = {
12 | todos: [
13 | { id: 1, title: 'Prepare a slide deck', completed: false },
14 | { id: 2, title: 'Speak at ReactJS Conference', completed: false },
15 | { id: 3, title: 'Sleep', completed: true },
16 | ],
17 | onItemClick: sinon.spy(),
18 | onDestroy: sinon.spy(),
19 | }
20 | wrapper = shallow()
21 | })
22 |
23 | it('should have a correct element', () => {
24 | expect(wrapper).to.have.tagName('ul')
25 | })
26 |
27 | it('should have a correct class', () => {
28 | expect(wrapper).to.have.className('todo-list')
29 | })
30 |
31 | it('should render TodoItem from props.todo', () => {
32 | expect(wrapper).to.have.exactly(3).descendants(TodoItem)
33 | })
34 |
35 | it('should call onItemClick with clicked item id', () => {
36 | wrapper.find(TodoItem).at(0).simulate('click')
37 | expect(props.onItemClick).have.been.calledWith(1)
38 |
39 | wrapper.find(TodoItem).at(1).simulate('click')
40 | expect(props.onItemClick).have.been.calledWith(2)
41 |
42 | wrapper.find(TodoItem).at(2).simulate('click')
43 | expect(props.onItemClick).have.been.calledWith(3)
44 | })
45 |
46 | it('should call onDestroy with clicked item id', () => {
47 | wrapper = mount()
48 |
49 | wrapper.find(TodoItem).at(0).find('.destroy').simulate('click')
50 | expect(props.onDestroy).to.have.been.calledWith(1)
51 |
52 | wrapper.find(TodoItem).at(1).find('.destroy').simulate('click')
53 | expect(props.onDestroy).to.have.been.calledWith(2)
54 |
55 | wrapper.find(TodoItem).at(2).find('.destroy').simulate('click')
56 | expect(props.onDestroy).to.have.been.calledWith(3)
57 | })
58 | })
59 |
--------------------------------------------------------------------------------
/todo-redux/test/containers/FilteredTodoList.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { expect, reduxMount, sinon } from '../test-helper'
3 | import FilteredTodoList from '../../src/containers/FilteredTodoList'
4 | import filters from '../../src/constants/filters'
5 |
6 | describe('FilteredTodoList', () => {
7 | let wrapper
8 | let props
9 | let state
10 |
11 | beforeEach(() => {
12 | props = {
13 | onItemClick: sinon.spy(),
14 | onDestroy: sinon.spy(),
15 | }
16 | state = {
17 | todos: [
18 | { id: 1, title: 'Prepare a slide deck', completed: false },
19 | { id: 2, title: 'Speak at ReactJS Conference', completed: false },
20 | { id: 3, title: 'Sleep', completed: true },
21 | ],
22 | filter: filters.ALL,
23 | }
24 | })
25 |
26 | it('should display all todos with ALL filter', () => {
27 | state.filter = filters.ALL
28 | wrapper = reduxMount(, state)
29 | expect(wrapper).to.have.exactly(3).descendants('li')
30 | })
31 |
32 | it('should display only active todos with ACTIVE filter', () => {
33 | state.filter = filters.ACTIVE
34 | wrapper = reduxMount(, state)
35 | expect(wrapper).to.have.exactly(2).descendants('li')
36 | })
37 |
38 | it('should display on completed todos with COMPLETED filter', () => {
39 | state.filter = filters.COMPLETED
40 | wrapper = reduxMount(, state)
41 | expect(wrapper).to.have.exactly(1).descendants('li')
42 | })
43 | })
44 |
--------------------------------------------------------------------------------
/todo-redux/test/reducers/filterTodo.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from '../test-helper'
2 | import filterTodoReducer from '../../src/reducers/filterTodo'
3 | import filters from '../../src/constants/filters'
4 | import types from '../../src/actions/types'
5 |
6 | describe('filterTodoReducer', () => {
7 | it('should change to ACTIVE state when receiving ACTIVE filterTodo action', () => {
8 | const curState = filters.ALL
9 | const action = {
10 | type: types.FILTER_TODO,
11 | payload: {
12 | filter: filters.ACTIVE,
13 | },
14 | }
15 | const nextState = filterTodoReducer(curState, action)
16 | const expectedState = filters.ACTIVE
17 |
18 | expect(nextState).to.equal(expectedState)
19 | })
20 |
21 | it('should change to COMPLETED state when receiving COMPLETED filterTodo action', () => {
22 | const curState = filters.ALL
23 | const action = {
24 | type: types.FILTER_TODO,
25 | payload: {
26 | filter: filters.COMPLETED,
27 | },
28 | }
29 | const nextState = filterTodoReducer(curState, action)
30 | const expectedState = filters.COMPLETED
31 |
32 | expect(nextState).to.equal(expectedState)
33 | })
34 |
35 | it('should change to ALL state when receiving ALL filterTodo action', () => {
36 | const curState = filters.ACTIVE
37 | const action = {
38 | type: types.FILTER_TODO,
39 | payload: {
40 | filter: filters.ALL,
41 | },
42 | }
43 | const nextState = filterTodoReducer(curState, action)
44 | const expectedState = filters.ALL
45 |
46 | expect(nextState).to.equal(expectedState)
47 | })
48 | })
49 |
--------------------------------------------------------------------------------
/todo-redux/test/reducers/newTodo.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from '../test-helper'
2 | import types from '../../src/actions/types'
3 | import newTodoReducer from '../../src/reducers/newTodo'
4 |
5 | describe('newTodoReducer', () => {
6 | let curState
7 |
8 | beforeEach(() => {
9 | curState = ''
10 | })
11 |
12 | it('should return correct initial state', () => {
13 | expect(newTodoReducer(undefined, {})).to.equal('')
14 | })
15 |
16 | it('should change state when receiving changeNewTodoText action', () => {
17 | const action = {
18 | type: types.CHANGE_NEW_TODO_TEXT,
19 | payload: {
20 | text: 'My next task',
21 | },
22 | }
23 | const nextState = newTodoReducer(curState, action)
24 | const expectedState = 'My next task'
25 |
26 | expect(nextState).to.equal(expectedState)
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/todo-redux/test/reducers/todos.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from '../test-helper'
2 | import types from '../../src/actions/types'
3 | import reducer from '../../src/reducers/todos'
4 |
5 | describe('Todos Reducer', () => {
6 | let curState
7 |
8 | beforeEach(() => {
9 | curState = [
10 | { id: 1, title: 'Prepare a slide deck', completed: false },
11 | { id: 2, title: 'Speak at ReactJS Conference', completed: false },
12 | { id: 3, title: 'Sleep', completed: true },
13 | ]
14 | })
15 |
16 | it('should return correct initial state', () => {
17 | const expectedState = []
18 |
19 | expect(reducer(undefined, {})).to.deep.equal(expectedState)
20 | })
21 |
22 | describe('add', () => {
23 | it('should add new todo when receiving addTodo action', () => {
24 | const actions = [
25 | {
26 | type: types.ADD_TODO,
27 | payload: {
28 | title: 'Catch a pokemon',
29 | },
30 | },
31 | {
32 | type: types.ADD_TODO,
33 | payload: {
34 | title: 'Evolve a pokemon',
35 | },
36 | },
37 | ]
38 | const nextState = actions.reduce(reducer, curState)
39 |
40 | reducer(curState, actions)
41 | const expectedState = [
42 | { id: 1, title: 'Prepare a slide deck', completed: false },
43 | { id: 2, title: 'Speak at ReactJS Conference', completed: false },
44 | { id: 3, title: 'Sleep', completed: true },
45 | { id: 4, title: 'Catch a pokemon', completed: false },
46 | { id: 5, title: 'Evolve a pokemon', completed: false },
47 | ]
48 |
49 | expect(nextState).to.deep.equal(expectedState)
50 | })
51 |
52 | it('should not return the same reference of state', () => {
53 | const action = {
54 | type: types.ADD_TODO,
55 | payload: {
56 | title: 'Catch a pokemon',
57 | },
58 | }
59 | const nextState = reducer(curState, action)
60 |
61 | expect(nextState).to.not.equal(curState)
62 | })
63 | })
64 |
65 | describe('toggle', () => {
66 | it('should toggle complete status', () => {
67 | const action = {
68 | type: types.TOGGLE_TODO,
69 | payload: {
70 | id: 2,
71 | },
72 | }
73 | const nextState = reducer(curState, action)
74 | const expectedState = [
75 | { id: 1, title: 'Prepare a slide deck', completed: false },
76 | { id: 2, title: 'Speak at ReactJS Conference', completed: true },
77 | { id: 3, title: 'Sleep', completed: true },
78 | ]
79 |
80 | expect(nextState).to.deep.equal(expectedState)
81 | })
82 | })
83 |
84 | describe('toggleAllTodos', () => {
85 | it('should complete all todos if at least one todo is incomplete', () => {
86 | const action = {
87 | type: types.TOGGLE_ALL_TODOS,
88 | }
89 | const nextState = reducer(curState, action)
90 | const expectedState = [
91 | { id: 1, title: 'Prepare a slide deck', completed: true },
92 | { id: 2, title: 'Speak at ReactJS Conference', completed: true },
93 | { id: 3, title: 'Sleep', completed: true },
94 | ]
95 | expect(nextState).to.deep.equal(expectedState)
96 | })
97 |
98 | it('should reset of all complete status if all todos are completed', () => {
99 | curState = [
100 | { id: 1, title: 'Prepare a slide deck', completed: true },
101 | { id: 2, title: 'Speak at ReactJS Conference', completed: true },
102 | { id: 3, title: 'Sleep', completed: true },
103 | ]
104 | const action = {
105 | type: types.TOGGLE_ALL_TODOS,
106 | }
107 | const nextState = reducer(curState, action)
108 | const expectedState = [
109 | { id: 1, title: 'Prepare a slide deck', completed: false },
110 | { id: 2, title: 'Speak at ReactJS Conference', completed: false },
111 | { id: 3, title: 'Sleep', completed: false },
112 | ]
113 | expect(nextState).to.deep.equal(expectedState)
114 | })
115 | })
116 |
117 | describe('destroy', () => {
118 | it('should destroy selected todo', () => {
119 | const action = {
120 | type: types.DESTROY_TODO,
121 | payload: {
122 | id: 2,
123 | },
124 | }
125 | const nextState = reducer(curState, action)
126 | const expectedState = [
127 | { id: 1, title: 'Prepare a slide deck', completed: false },
128 | { id: 3, title: 'Sleep', completed: true },
129 | ]
130 | expect(nextState).to.deep.equal(expectedState)
131 | })
132 | })
133 |
134 | describe('clearCompletedTodos', () => {
135 | it('should clear all completed todos', () => {
136 | const action = {
137 | type: types.CLEAR_COMPLETED_TODOS,
138 | }
139 | const nextState = reducer(curState, action)
140 | const expectedState = [
141 | { id: 1, title: 'Prepare a slide deck', completed: false },
142 | { id: 2, title: 'Speak at ReactJS Conference', completed: false },
143 | ]
144 | expect(nextState).to.deep.equal(expectedState)
145 | })
146 | })
147 | })
148 |
--------------------------------------------------------------------------------
/todo-redux/test/test-helper.js:
--------------------------------------------------------------------------------
1 | import chai, { expect } from 'chai'
2 | import sinon from 'sinon'
3 | import sinonChai from 'sinon-chai'
4 | import { mount, render, shallow } from 'enzyme'
5 | import chaiEnzyme from 'chai-enzyme'
6 | import jsdom from 'jsdom'
7 | import React from 'react'
8 | import { Provider } from 'react-redux'
9 | import configureMockStore from 'redux-mock-store'
10 |
11 | chai.use(sinonChai)
12 | chai.use(chaiEnzyme())
13 |
14 | const doc = jsdom.jsdom('')
15 | global.document = doc
16 | global.window = doc.defaultView
17 |
18 | const reduxMount = (component, state) => { // eslint-disable-line
19 | const mockStore = configureMockStore()
20 | const store = mockStore(state)
21 |
22 | return mount(
23 |
24 | {component}
25 |
26 | )
27 | }
28 |
29 | export {
30 | expect,
31 | sinon,
32 | mount,
33 | render,
34 | shallow,
35 | reduxMount,
36 | }
37 |
--------------------------------------------------------------------------------
/todo-redux/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const DashboardPlugin = require('webpack-dashboard/plugin')
3 | const HtmlWebpackPlugin = require('html-webpack-plugin')
4 |
5 | module.exports = {
6 | devtool: 'eval-source-map',
7 | entry: {
8 | entry: path.join(__dirname, 'src', 'index.js'),
9 | vendor: ['react', 'react-dom'],
10 | },
11 | output: {
12 | path: path.join(__dirname, 'dist'),
13 | publicPath: '/dist/',
14 | filename: '[name].[hash].js',
15 | },
16 | module: {
17 | loaders: [
18 | {
19 | test: /\.jsx?$/,
20 | exclude: /node_modules/,
21 | loaders: ['react-hot', 'babel'],
22 | },
23 | {
24 | test: /\.css?$/,
25 | loaders: ['style', 'css'],
26 | },
27 | ],
28 | },
29 | plugins: [
30 | new DashboardPlugin(),
31 | new HtmlWebpackPlugin({
32 | template: 'src/index.html',
33 | }),
34 | ],
35 | }
36 |
--------------------------------------------------------------------------------