From d563cb32b6fdb7f4ab49b635b35be50009e501e0 Mon Sep 17 00:00:00 2001 From: Chris Kobrzak Date: Wed, 9 Sep 2015 13:16:15 +0100 Subject: [PATCH] Add Todos widget implemented in Redux --- index.html | 4 +++ src/app.js | 17 +++++++++++ src/flux/actions.js | 24 +++++++++++++++ src/flux/reducers.js | 42 +++++++++++++++++++++++++ src/todos/AddTodo.jsx | 37 ++++++++++++++++++++++ src/todos/App.jsx | 69 ++++++++++++++++++++++++++++++++++++++++++ src/todos/Footer.jsx | 51 +++++++++++++++++++++++++++++++ src/todos/Todo.jsx | 33 ++++++++++++++++++++ src/todos/TodoList.jsx | 24 +++++++++++++++ 9 files changed, 301 insertions(+) create mode 100644 src/flux/actions.js create mode 100644 src/flux/reducers.js create mode 100644 src/todos/AddTodo.jsx create mode 100644 src/todos/App.jsx create mode 100644 src/todos/Footer.jsx create mode 100644 src/todos/Todo.jsx create mode 100644 src/todos/TodoList.jsx diff --git a/index.html b/index.html index df9680c..c45b9bb 100644 --- a/index.html +++ b/index.html @@ -8,9 +8,13 @@
+
+ Page #1 Page #2 + + diff --git a/src/app.js b/src/app.js index e35a639..c134a8e 100644 --- a/src/app.js +++ b/src/app.js @@ -7,5 +7,22 @@ import Book from './book/book.js'; import Section from './book/section.jsx'; React.render(
, document.getElementById( 'heading-container' ) ); +import { createStore } from 'redux'; +import { Provider } from 'react-redux'; +import todoApp from './flux/reducers'; +import Todos from './todos/App.jsx'; // eslint-disable-line no-unused-vars + +let store = createStore(todoApp); + +let rootElement = document.getElementById('notes-container'); +React.render( + // The child must be wrapped in a function + // to work around an issue in React 0.13. + + {() => } + , + rootElement +); + var book = new Book(); book.logSomething(); diff --git a/src/flux/actions.js b/src/flux/actions.js new file mode 100644 index 0000000..e276862 --- /dev/null +++ b/src/flux/actions.js @@ -0,0 +1,24 @@ +// action types +export const ADD_TODO = 'ADD_TODO' +export const COMPLETE_TODO = 'COMPLETE_TODO' +export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER' + +// other constants +export const VisibilityFilters = { + SHOW_ALL: 'SHOW_ALL', + SHOW_COMPLETED: 'SHOW_COMPLETED', + SHOW_ACTIVE: 'SHOW_ACTIVE' +} + +// action creators +export function addTodo(text) { + return { type: ADD_TODO, text } +} + +export function completeTodo(index) { + return { type: COMPLETE_TODO, index } +} + +export function setVisibilityFilter(filter) { + return { type: SET_VISIBILITY_FILTER, filter } +} diff --git a/src/flux/reducers.js b/src/flux/reducers.js new file mode 100644 index 0000000..108b928 --- /dev/null +++ b/src/flux/reducers.js @@ -0,0 +1,42 @@ +import { combineReducers } from 'redux' +import { ADD_TODO, COMPLETE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } + from './actions' +const { SHOW_ALL } = VisibilityFilters + +function visibilityFilter(state = SHOW_ALL, action) { + // TODO Switch statements are a no-no + switch (action.type) { + case SET_VISIBILITY_FILTER: + return action.filter + default: + return state + } +} + +function todos(state = [], action) { + // TODO Switch statements are a no-no + switch (action.type) { + case ADD_TODO: + return [...state, { + text: action.text, + completed: false + }] + case COMPLETE_TODO: + return [ + ...state.slice(0, action.index), + Object.assign( {}, state[action.index], { + completed: true + }), + ...state.slice(action.index + 1) + ] + default: + return state + } +} + +const todoApp = combineReducers({ + visibilityFilter, + todos +}) + +export default todoApp diff --git a/src/todos/AddTodo.jsx b/src/todos/AddTodo.jsx new file mode 100644 index 0000000..7be2af7 --- /dev/null +++ b/src/todos/AddTodo.jsx @@ -0,0 +1,37 @@ +import React from 'react' + +export default class AddTodo extends React.Component { + + constructor(props) { + super(props) + this.bindInstanceMethods( "handleClick" ) + } + + bindInstanceMethods( ...methods ) { + methods.forEach( + ( method ) => this[ method ] = this[ method ].bind( this ) + ) + } + + render() { + return ( +
+ + +
+ ) + } + + handleClick( event ) { + let node = React.findDOMNode( this.refs.input ) + let text = node.value.trim() + this.props.onAddClick( text ) + node.value = '' + } +} + +AddTodo.propTypes = { + onAddClick: React.PropTypes.func.isRequired +} diff --git a/src/todos/App.jsx b/src/todos/App.jsx new file mode 100644 index 0000000..8de9f9b --- /dev/null +++ b/src/todos/App.jsx @@ -0,0 +1,69 @@ +import React from "react" +import { connect } from 'react-redux' +import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } + from '../flux/actions' +import AddTodo from './AddTodo.jsx' +import TodoList from './TodoList.jsx' +import Footer from './Footer.jsx' + +class App extends React.Component { + + render() { + // Injected by connect() call: + const { dispatch, visibleTodos, visibilityFilter } = this.props + return ( +
+ + dispatch( addTodo( text ) ) + } /> + { + dispatch( completeTodo( index ) ) + }} /> +
+ dispatch( setVisibilityFilter( nextFilter ) ) + } /> +
+ ) + } +} + +App.propTypes = { + visibleTodos: React.PropTypes.arrayOf( React.PropTypes.shape({ + text: React.PropTypes.string.isRequired, + completed: React.PropTypes.bool.isRequired + }) ), + visibilityFilter: React.PropTypes.oneOf([ + 'SHOW_ALL', + 'SHOW_COMPLETED', + 'SHOW_ACTIVE' + ]).isRequired +} + +function selectTodos(todos, filter) { + // TODO Switch statements are a no-no + switch ( filter ) { + case VisibilityFilters.SHOW_ALL: + return todos + case VisibilityFilters.SHOW_COMPLETED: + return todos.filter( todo => todo.completed ) + case VisibilityFilters.SHOW_ACTIVE: + return todos.filter( todo => !todo.completed ) + } +} + +// Which props do we want to inject, given the global state? +// Note: use https://github.com/faassen/reselect for better performance. +function select(state) { + return { + visibleTodos: selectTodos( state.todos, state.visibilityFilter ), + visibilityFilter: state.visibilityFilter + } +} + +// Wrap the component to inject dispatch and state into it +export default connect( select )( App ) diff --git a/src/todos/Footer.jsx b/src/todos/Footer.jsx new file mode 100644 index 0000000..fa93511 --- /dev/null +++ b/src/todos/Footer.jsx @@ -0,0 +1,51 @@ +import React from 'react' + +export default class Footer extends React.Component { + + constructor(props) { + super(props) + } + + render() { + return ( +

+ Show: + {' '} + {this.renderFilter('SHOW_ALL', 'All')} + {', '} + {this.renderFilter('SHOW_COMPLETED', 'Completed')} + {', '} + {this.renderFilter('SHOW_ACTIVE', 'Active')} + . +

+ ) + } + + renderFilter(filter, name) { + if (filter === this.props.filter) { + return name + } + + return ( + // Please note the currying technique we use below + + {name} + + ) + } + + handleClick( filter, event ) { + event.preventDefault() + this.props.onFilterChange( filter ) + } + +} + +Footer.propTypes = { + onFilterChange: React.PropTypes.func.isRequired, + filter: React.PropTypes.oneOf([ + 'SHOW_ALL', + 'SHOW_COMPLETED', + 'SHOW_ACTIVE' + ]).isRequired +} diff --git a/src/todos/Todo.jsx b/src/todos/Todo.jsx new file mode 100644 index 0000000..fa6512d --- /dev/null +++ b/src/todos/Todo.jsx @@ -0,0 +1,33 @@ +import React from "react" + +export default class Todo extends React.Component { + + render() { + let textDecoration, cursor + + if ( this.props.completed ) { + textDecoration = 'line-through' + cursor = 'default' + } else { + textDecoration = 'none' + cursor = 'pointer' + } + + return ( +
  • + {this.props.text} +
  • + ) + } +} + +Todo.propTypes = { + onClick: React.PropTypes.func.isRequired, + text: React.PropTypes.string.isRequired, + completed: React.PropTypes.bool.isRequired +} diff --git a/src/todos/TodoList.jsx b/src/todos/TodoList.jsx new file mode 100644 index 0000000..055a622 --- /dev/null +++ b/src/todos/TodoList.jsx @@ -0,0 +1,24 @@ +import React from "react" +import Todo from './Todo.jsx' + +export default class TodoList extends React.Component { + render() { + return ( + + ) + } +} + +TodoList.propTypes = { + onTodoClick: React.PropTypes.func.isRequired, + todos: React.PropTypes.arrayOf( React.PropTypes.shape({ + text: React.PropTypes.string.isRequired, + completed: React.PropTypes.bool.isRequired + }).isRequired ).isRequired +}