From 279a43fb350fb499edd6e135cf049766dbce72e6 Mon Sep 17 00:00:00 2001 From: Ken Date: Sun, 17 Feb 2019 18:12:26 -0800 Subject: [PATCH] Step 8: combine reducers --- index.html | 6 ++ step2-03/src/components/TodoHeader.tsx | 2 +- step2-06/src/reducers/index.ts | 13 ++-- step2-07/src/reducers/index.ts | 10 +-- step2-08/README.md | 3 + step2-08/index.html | 6 ++ step2-08/src/actions/index.ts | 5 ++ step2-08/src/components/TodoApp.tsx | 36 +++++++++++ step2-08/src/components/TodoFooter.tsx | 42 +++++++++++++ step2-08/src/components/TodoHeader.tsx | 78 ++++++++++++++++++++++++ step2-08/src/components/TodoList.tsx | 41 +++++++++++++ step2-08/src/components/TodoListItem.tsx | 48 +++++++++++++++ step2-08/src/index.tsx | 30 +++++++++ step2-08/src/reducers/index.ts | 35 +++++++++++ step2-08/src/reducers/pureFunctions.ts | 39 ++++++++++++ step2-08/src/store/index.ts | 14 +++++ 16 files changed, 394 insertions(+), 14 deletions(-) create mode 100644 step2-08/README.md create mode 100644 step2-08/index.html create mode 100644 step2-08/src/actions/index.ts create mode 100644 step2-08/src/components/TodoApp.tsx create mode 100644 step2-08/src/components/TodoFooter.tsx create mode 100644 step2-08/src/components/TodoHeader.tsx create mode 100644 step2-08/src/components/TodoList.tsx create mode 100644 step2-08/src/components/TodoListItem.tsx create mode 100644 step2-08/src/index.tsx create mode 100644 step2-08/src/reducers/index.ts create mode 100644 step2-08/src/reducers/pureFunctions.ts create mode 100644 step2-08/src/store/index.ts diff --git a/index.html b/index.html index bc48954..7da3798 100644 --- a/index.html +++ b/index.html @@ -110,6 +110,12 @@ Redux 3: Connect to UI +
  • + + Step 8
    + Redux 4: Combine Reducers +
    +
  • diff --git a/step2-03/src/components/TodoHeader.tsx b/step2-03/src/components/TodoHeader.tsx index 0015717..7a9cae2 100644 --- a/step2-03/src/components/TodoHeader.tsx +++ b/step2-03/src/components/TodoHeader.tsx @@ -7,7 +7,7 @@ import { FilterTypes } from '../store'; interface TodoHeaderProps { addTodo: (label: string) => void; setFilter: (filter: FilterTypes) => void; - filter: string; + filter: FilterTypes; } interface TodoHeaderState { diff --git a/step2-06/src/reducers/index.ts b/step2-06/src/reducers/index.ts index 6a8562a..946dbb1 100644 --- a/step2-06/src/reducers/index.ts +++ b/step2-06/src/reducers/index.ts @@ -1,19 +1,16 @@ import { Store } from '../store'; import { addTodo, remove, complete } from './pureFunctions'; -import { clear } from '../../../step2-07/src/reducers/pureFunctions'; -let index = 0; - -export function reducer(state: Store, payload: any): Store { - switch (payload.type) { +export function reducer(state: Store, action: any): Store { + switch (action.type) { case 'addTodo': - return addTodo(state, payload.label); + return addTodo(state, action.label); case 'remove': - return remove(state, payload.id); + return remove(state, action.id); case 'complete': - return complete(state, payload.id); + return complete(state, action.id); } return state; diff --git a/step2-07/src/reducers/index.ts b/step2-07/src/reducers/index.ts index 92a2976..0054c0e 100644 --- a/step2-07/src/reducers/index.ts +++ b/step2-07/src/reducers/index.ts @@ -1,19 +1,19 @@ import { Store } from '../store'; import { addTodo, remove, complete, clear } from './pureFunctions'; -export function reducer(state: Store, payload: any): Store { - switch (payload.type) { +export function reducer(state: Store, action: any): Store { + switch (action.type) { case 'addTodo': - return addTodo(state, payload.label); + return addTodo(state, action.label); case 'remove': - return remove(state, payload.id); + return remove(state, action.id); case 'clear': return clear(state); case 'complete': - return complete(state, payload.id); + return complete(state, action.id); } return state; diff --git a/step2-08/README.md b/step2-08/README.md new file mode 100644 index 0000000..ce86500 --- /dev/null +++ b/step2-08/README.md @@ -0,0 +1,3 @@ +# Step 2.8 + +Combine Reducers diff --git a/step2-08/index.html b/step2-08/index.html new file mode 100644 index 0000000..454cef5 --- /dev/null +++ b/step2-08/index.html @@ -0,0 +1,6 @@ + + + +
    + + diff --git a/step2-08/src/actions/index.ts b/step2-08/src/actions/index.ts new file mode 100644 index 0000000..05959f8 --- /dev/null +++ b/step2-08/src/actions/index.ts @@ -0,0 +1,5 @@ +export const addTodo = (label: string) => ({ type: 'addTodo', label }); +export const remove = (id: string) => ({ type: 'remove', id }); +export const complete = (id: string) => ({ type: 'complete', id }); +export const clear = () => ({ type: 'clear' }); +export const setFilter = (filter: string) => ({ type: 'setFilter', filter }); diff --git a/step2-08/src/components/TodoApp.tsx b/step2-08/src/components/TodoApp.tsx new file mode 100644 index 0000000..ef8f05b --- /dev/null +++ b/step2-08/src/components/TodoApp.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Stack, Customizer, mergeStyles, getTheme } from 'office-ui-fabric-react'; +import { TodoFooter } from './TodoFooter'; +import { TodoHeader } from './TodoHeader'; +import { TodoList } from './TodoList'; +import { Store } from '../store'; +import { FluentCustomizations } from '@uifabric/fluent-theme'; + +const className = mergeStyles({ + padding: 25, + ...getTheme().effects.elevation4 +}); + +export class TodoApp extends React.Component { + constructor(props) { + super(props); + this.state = { + todos: {}, + filter: 'all' + }; + } + render() { + const { filter, todos } = this.state; + return ( + + + + + + + + + + ); + } +} diff --git a/step2-08/src/components/TodoFooter.tsx b/step2-08/src/components/TodoFooter.tsx new file mode 100644 index 0000000..7c23c44 --- /dev/null +++ b/step2-08/src/components/TodoFooter.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Text } from '@uifabric/experiments'; +import { Stack } from 'office-ui-fabric-react'; +import { Store } from '../store'; +import { DefaultButton } from 'office-ui-fabric-react'; +import { connect } from 'react-redux'; +import * as actions from '../actions'; + +interface TodoFooterProps { + clear: () => void; + todos: Store['todos']; +} + +const TodoFooter = (props: TodoFooterProps) => { + const itemCount = Object.keys(props.todos).filter(id => !props.todos[id].completed).length; + + return ( + + + {itemCount} item{itemCount > 1 ? 's' : ''} left + + props.clear()}>Clear Completed + + ); +}; + +function mapStateToProps(state: Store) { + return { ...state }; +} + +function mapDispatchToProps(dispatch: any) { + return { + clear: () => dispatch(actions.clear()) + }; +} + +const component = connect( + mapStateToProps, + mapDispatchToProps +)(TodoFooter); + +export { component as TodoFooter }; diff --git a/step2-08/src/components/TodoHeader.tsx b/step2-08/src/components/TodoHeader.tsx new file mode 100644 index 0000000..4859517 --- /dev/null +++ b/step2-08/src/components/TodoHeader.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { Text } from '@uifabric/experiments'; +import { Stack } from 'office-ui-fabric-react'; +import { Pivot, PivotItem, TextField, PrimaryButton } from 'office-ui-fabric-react'; +import { FilterTypes, Store } from '../store'; +import * as actions from '../actions'; +import { connect } from 'react-redux'; + +interface TodoHeaderProps { + addTodo: (label: string) => void; + setFilter: (filter: FilterTypes) => void; + filter: FilterTypes; +} + +interface TodoHeaderState { + labelInput: string; +} + +class TodoHeader extends React.Component { + constructor(props: TodoHeaderProps) { + super(props); + this.state = { labelInput: undefined }; + } + + render() { + return ( + + + todos + + + + + + + Add + + + + + + + + + ); + } + + private onAdd = () => { + this.props.addTodo(this.state.labelInput); + this.setState({ labelInput: undefined }); + }; + + private onChange = (evt: React.FormEvent, newValue: string) => { + this.setState({ labelInput: newValue }); + }; + + private onFilter = (item: PivotItem) => { + this.props.setFilter(item.props.headerText as FilterTypes); + }; +} + +function mapStateToProps(state: Store) { + return { ...state }; +} + +function mapDispatchToProps(dispatch: any) { + return { + addTodo: (label: string) => dispatch(actions.addTodo(label)), + setFilter: (filter: FilterTypes) => dispatch(actions.setFilter(filter)) + }; +} + +const component = connect( + mapStateToProps, + mapDispatchToProps +)(TodoHeader); + +export { component as TodoHeader }; diff --git a/step2-08/src/components/TodoList.tsx b/step2-08/src/components/TodoList.tsx new file mode 100644 index 0000000..cd0962f --- /dev/null +++ b/step2-08/src/components/TodoList.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Stack } from 'office-ui-fabric-react'; +import { TodoListItem } from './TodoListItem'; +import { Store, FilterTypes } from '../store'; +import { connect } from 'react-redux'; +import * as actions from '../actions'; + +interface TodoListProps { + todos: Store['todos']; + filter: FilterTypes; +} + +const TodoList = (props: TodoListProps) => { + const { filter, todos } = props; + const filteredTodos = Object.keys(todos).filter(id => { + return filter === 'all' || (filter === 'completed' && todos[id].completed) || (filter === 'active' && !todos[id].completed); + }); + + return ( + + {filteredTodos.map(id => ( + + ))} + + ); +}; + +function mapStateToProps(state: Store) { + return { ...state }; +} + +function mapDispatchToProps(dispatch: any) { + return {}; +} + +const component = connect( + mapStateToProps, + mapDispatchToProps +)(TodoList); + +export { component as TodoList }; diff --git a/step2-08/src/components/TodoListItem.tsx b/step2-08/src/components/TodoListItem.tsx new file mode 100644 index 0000000..482b472 --- /dev/null +++ b/step2-08/src/components/TodoListItem.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { Stack, Checkbox, IconButton, TextField, DefaultButton } from 'office-ui-fabric-react'; +import { Store } from '../store'; +import { connect } from 'react-redux'; +import * as actions from '../actions'; + +interface TodoListItemProps { + id: string; + todos: Store['todos']; + remove: (id: string) => void; + complete: (id: string) => void; +} + +class TodoListItem extends React.Component { + render() { + const { todos, id, complete, remove } = this.props; + const item = todos[id]; + + return ( + + complete(id)} /> +
    + remove(id)} /> +
    +
    + ); + } +} + +function mapStateToProps({ todos }: Store) { + return { + todos + }; +} + +function mapDispatchToProps(dispatch: any) { + return { + remove: (id: string) => dispatch(actions.remove(id)), + complete: (id: string) => dispatch(actions.complete(id)) + }; +} + +const component = connect( + mapStateToProps, + mapDispatchToProps +)(TodoListItem); + +export { component as TodoListItem }; diff --git a/step2-08/src/index.tsx b/step2-08/src/index.tsx new file mode 100644 index 0000000..9767dfd --- /dev/null +++ b/step2-08/src/index.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { reducer } from './reducers'; +import { createStore, compose } from 'redux'; +import { Provider } from 'react-redux'; +import { TodoApp } from './components/TodoApp'; +import { addTodo } from './actions'; +import { initializeIcons } from '@uifabric/icons'; +import { Store } from './store'; + +/* Goop for making the Redux dev tool to work */ +declare var window: any; +const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; +function createStoreWithDevTool(reducer, initialStore?: Store) { + return createStore(reducer, initialStore, composeEnhancers()); +} + +const store = createStoreWithDevTool(reducer); + +store.dispatch(addTodo('hello')); +store.dispatch(addTodo('world')); + +initializeIcons(); + +ReactDOM.render( + + + , + document.getElementById('app') +); diff --git a/step2-08/src/reducers/index.ts b/step2-08/src/reducers/index.ts new file mode 100644 index 0000000..4ec7c95 --- /dev/null +++ b/step2-08/src/reducers/index.ts @@ -0,0 +1,35 @@ +import { Store } from '../store'; +import { addTodo, remove, complete, clear, setFilter } from './pureFunctions'; +import { combineReducers } from 'redux'; + +function todoReducer(state: Store['todos'] = {}, action: any): Store['todos'] { + switch (action.type) { + case 'addTodo': + return addTodo(state, action.label); + + case 'remove': + return remove(state, action.id); + + case 'clear': + return clear(state); + + case 'complete': + return complete(state, action.id); + } + + return state; +} + +function filterReducer(state: Store['filter'] = 'all', action: any): Store['filter'] { + switch (action.type) { + case 'setFilter': + return setFilter(state, action.filter); + } + + return state; +} + +export const reducer = combineReducers({ + todos: todoReducer, + filter: filterReducer +}); diff --git a/step2-08/src/reducers/pureFunctions.ts b/step2-08/src/reducers/pureFunctions.ts new file mode 100644 index 0000000..0d6765f --- /dev/null +++ b/step2-08/src/reducers/pureFunctions.ts @@ -0,0 +1,39 @@ +import { Store, FilterTypes } from '../store'; + +let index = 0; + +export function addTodo(state: Store['todos'], label: string): Store['todos'] { + const id = index++; + return { ...state, [id]: { label, completed: false } }; +} + +export function remove(state: Store['todos'], id: string) { + const newTodos = { ...state }; + + delete newTodos[id]; + + return newTodos; +} + +export function complete(state: Store['todos'], id: string) { + const newTodos = { ...state }; + newTodos[id].completed = !newTodos[id].completed; + + return newTodos; +} + +export function clear(state: Store['todos']) { + const newTodos = { ...state }; + + Object.keys(state.todos).forEach(key => { + if (state.todos[key].completed) { + delete newTodos[key]; + } + }); + + return newTodos; +} + +export function setFilter(state: Store['filter'], filter: FilterTypes) { + return filter; +} diff --git a/step2-08/src/store/index.ts b/step2-08/src/store/index.ts new file mode 100644 index 0000000..221b5f4 --- /dev/null +++ b/step2-08/src/store/index.ts @@ -0,0 +1,14 @@ +export type FilterTypes = 'all' | 'active' | 'completed'; + +export interface TodoItem { + label: string; + completed: boolean; +} + +export interface Store { + todos: { + [id: string]: TodoItem; + }; + + filter: FilterTypes; +}