From f046a07694b5670b477ff412be3dd6ae7414ca7c Mon Sep 17 00:00:00 2001 From: Ken Date: Tue, 19 Feb 2019 23:15:43 -0800 Subject: [PATCH] adding exercise for step 7 --- index.html | 5 +- step2-07/README.md | 12 +++- step2-07/{ => demo}/index.html | 0 step2-07/{ => demo}/src/actions/index.ts | 0 step2-07/demo/src/components/TodoApp.tsx | 26 +++++++ .../{ => demo}/src/components/TodoFooter.tsx | 0 .../{ => demo}/src/components/TodoHeader.tsx | 0 .../{ => demo}/src/components/TodoList.tsx | 0 .../src/components/TodoListItem.tsx | 0 step2-07/{ => demo}/src/index.tsx | 0 step2-07/{ => demo}/src/reducers/index.ts | 0 .../src/reducers/pureFunctions.spec.ts | 0 .../{ => demo}/src/reducers/pureFunctions.ts | 0 step2-07/{ => demo}/src/store/index.ts | 0 step2-07/exercise/index.html | 6 ++ step2-07/exercise/src/actions/index.ts | 8 +++ step2-07/exercise/src/components/TodoApp.tsx | 25 +++++++ .../exercise/src/components/TodoFooter.tsx | 45 ++++++++++++ .../exercise/src/components/TodoHeader.tsx | 70 +++++++++++++++++++ step2-07/exercise/src/components/TodoList.tsx | 37 ++++++++++ .../exercise/src/components/TodoListItem.tsx | 48 +++++++++++++ step2-07/exercise/src/index.tsx | 25 +++++++ step2-07/exercise/src/reducers/index.ts | 27 +++++++ .../src/reducers/pureFunctions.spec.ts | 29 ++++++++ .../exercise/src/reducers/pureFunctions.ts | 36 ++++++++++ step2-07/exercise/src/store/index.ts | 14 ++++ step2-07/src/components/TodoApp.tsx | 36 ---------- step2-08/src/components/TodoApp.tsx | 34 ++++----- 28 files changed, 422 insertions(+), 61 deletions(-) rename step2-07/{ => demo}/index.html (100%) rename step2-07/{ => demo}/src/actions/index.ts (100%) create mode 100644 step2-07/demo/src/components/TodoApp.tsx rename step2-07/{ => demo}/src/components/TodoFooter.tsx (100%) rename step2-07/{ => demo}/src/components/TodoHeader.tsx (100%) rename step2-07/{ => demo}/src/components/TodoList.tsx (100%) rename step2-07/{ => demo}/src/components/TodoListItem.tsx (100%) rename step2-07/{ => demo}/src/index.tsx (100%) rename step2-07/{ => demo}/src/reducers/index.ts (100%) rename step2-07/{ => demo}/src/reducers/pureFunctions.spec.ts (100%) rename step2-07/{ => demo}/src/reducers/pureFunctions.ts (100%) rename step2-07/{ => demo}/src/store/index.ts (100%) create mode 100644 step2-07/exercise/index.html create mode 100644 step2-07/exercise/src/actions/index.ts create mode 100644 step2-07/exercise/src/components/TodoApp.tsx create mode 100644 step2-07/exercise/src/components/TodoFooter.tsx create mode 100644 step2-07/exercise/src/components/TodoHeader.tsx create mode 100644 step2-07/exercise/src/components/TodoList.tsx create mode 100644 step2-07/exercise/src/components/TodoListItem.tsx create mode 100644 step2-07/exercise/src/index.tsx create mode 100644 step2-07/exercise/src/reducers/index.ts create mode 100644 step2-07/exercise/src/reducers/pureFunctions.spec.ts create mode 100644 step2-07/exercise/src/reducers/pureFunctions.ts create mode 100644 step2-07/exercise/src/store/index.ts delete mode 100644 step2-07/src/components/TodoApp.tsx diff --git a/index.html b/index.html index 606889d..72436b4 100644 --- a/index.html +++ b/index.html @@ -115,10 +115,11 @@
  • - +
  • diff --git a/step2-07/README.md b/step2-07/README.md index c21511d..1896379 100644 --- a/step2-07/README.md +++ b/step2-07/README.md @@ -1,3 +1,13 @@ # Step 2.7 -Connect store to view +Connect store to view with `react-redux`. `connect()` is used to turn Redux store and dispatch functions into props inside React components. The state and action dispatchers are passed along with a `` component. + +# Exercise + +1. open up `exercise/src/index.tsx` and wrap `` with `` as instructed in the comment + +2. open up `exercise/src/components/TodoFooter.tsx` and erase the "nullable" type modifier (i.e. the ?) in the interface definition of `TodoFooterProps` + +3. uncomment the bottom bits of code and fill in the implementation for `mapStateToProps()` and `mapDispatchToProps()` - feel free to use `TodoListItem.tsx` as a guide + +4. do steps 2 and 3 for the `TodoHeader.tsx` file diff --git a/step2-07/index.html b/step2-07/demo/index.html similarity index 100% rename from step2-07/index.html rename to step2-07/demo/index.html diff --git a/step2-07/src/actions/index.ts b/step2-07/demo/src/actions/index.ts similarity index 100% rename from step2-07/src/actions/index.ts rename to step2-07/demo/src/actions/index.ts diff --git a/step2-07/demo/src/components/TodoApp.tsx b/step2-07/demo/src/components/TodoApp.tsx new file mode 100644 index 0000000..83464c0 --- /dev/null +++ b/step2-07/demo/src/components/TodoApp.tsx @@ -0,0 +1,26 @@ +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 const TodoApp = () => { + return ( + + + + + + + + + + ); +}; diff --git a/step2-07/src/components/TodoFooter.tsx b/step2-07/demo/src/components/TodoFooter.tsx similarity index 100% rename from step2-07/src/components/TodoFooter.tsx rename to step2-07/demo/src/components/TodoFooter.tsx diff --git a/step2-07/src/components/TodoHeader.tsx b/step2-07/demo/src/components/TodoHeader.tsx similarity index 100% rename from step2-07/src/components/TodoHeader.tsx rename to step2-07/demo/src/components/TodoHeader.tsx diff --git a/step2-07/src/components/TodoList.tsx b/step2-07/demo/src/components/TodoList.tsx similarity index 100% rename from step2-07/src/components/TodoList.tsx rename to step2-07/demo/src/components/TodoList.tsx diff --git a/step2-07/src/components/TodoListItem.tsx b/step2-07/demo/src/components/TodoListItem.tsx similarity index 100% rename from step2-07/src/components/TodoListItem.tsx rename to step2-07/demo/src/components/TodoListItem.tsx diff --git a/step2-07/src/index.tsx b/step2-07/demo/src/index.tsx similarity index 100% rename from step2-07/src/index.tsx rename to step2-07/demo/src/index.tsx diff --git a/step2-07/src/reducers/index.ts b/step2-07/demo/src/reducers/index.ts similarity index 100% rename from step2-07/src/reducers/index.ts rename to step2-07/demo/src/reducers/index.ts diff --git a/step2-07/src/reducers/pureFunctions.spec.ts b/step2-07/demo/src/reducers/pureFunctions.spec.ts similarity index 100% rename from step2-07/src/reducers/pureFunctions.spec.ts rename to step2-07/demo/src/reducers/pureFunctions.spec.ts diff --git a/step2-07/src/reducers/pureFunctions.ts b/step2-07/demo/src/reducers/pureFunctions.ts similarity index 100% rename from step2-07/src/reducers/pureFunctions.ts rename to step2-07/demo/src/reducers/pureFunctions.ts diff --git a/step2-07/src/store/index.ts b/step2-07/demo/src/store/index.ts similarity index 100% rename from step2-07/src/store/index.ts rename to step2-07/demo/src/store/index.ts diff --git a/step2-07/exercise/index.html b/step2-07/exercise/index.html new file mode 100644 index 0000000..454cef5 --- /dev/null +++ b/step2-07/exercise/index.html @@ -0,0 +1,6 @@ + + + +
    + + diff --git a/step2-07/exercise/src/actions/index.ts b/step2-07/exercise/src/actions/index.ts new file mode 100644 index 0000000..4a067b9 --- /dev/null +++ b/step2-07/exercise/src/actions/index.ts @@ -0,0 +1,8 @@ +import uuid from 'uuid/v4'; + +export const actions = { + addTodo: (label: string) => ({ type: 'addTodo', id: uuid(), label }), + remove: (id: string) => ({ type: 'remove', id }), + complete: (id: string) => ({ type: 'complete', id }), + clear: () => ({ type: 'clear' }) +}; diff --git a/step2-07/exercise/src/components/TodoApp.tsx b/step2-07/exercise/src/components/TodoApp.tsx new file mode 100644 index 0000000..44f7452 --- /dev/null +++ b/step2-07/exercise/src/components/TodoApp.tsx @@ -0,0 +1,25 @@ +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 { FluentCustomizations } from '@uifabric/fluent-theme'; + +const className = mergeStyles({ + padding: 25, + ...getTheme().effects.elevation4 +}); + +export const TodoApp = () => { + return ( + + + + + + + + + + ); +}; diff --git a/step2-07/exercise/src/components/TodoFooter.tsx b/step2-07/exercise/src/components/TodoFooter.tsx new file mode 100644 index 0000000..56655f8 --- /dev/null +++ b/step2-07/exercise/src/components/TodoFooter.tsx @@ -0,0 +1,45 @@ +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 { actions } from '../actions'; + +// TODO: after connecting to view, erase the ?'s +interface TodoFooterProps { + clear?: () => void; + todos?: Store['todos']; +} + +export 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 + + ); +}; + +/* +TODO: uncomment this and fill out the below code + +function mapStateToProps(state: Store) { + // TODO: FILL THIS OUT +} + +function mapDispatchToProps(dispatch: any) { + // TODO: FILL THIS OUT +} + +const component = connect( + mapStateToProps, + mapDispatchToProps +)(TodoFooter); + +export { component as TodoFooter }; +*/ diff --git a/step2-07/exercise/src/components/TodoHeader.tsx b/step2-07/exercise/src/components/TodoHeader.tsx new file mode 100644 index 0000000..21a39e5 --- /dev/null +++ b/step2-07/exercise/src/components/TodoHeader.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { Text } from '@uifabric/experiments'; +import { Stack } from 'office-ui-fabric-react'; +import { TextField, PrimaryButton } from 'office-ui-fabric-react'; +import { Store } from '../store'; +import { connect } from 'react-redux'; +import { actions } from '../actions'; + +// TODO: after connecting to view, erase the ?'s +interface TodoHeaderProps { + addTodo?: (label: string) => void; +} + +interface TodoHeaderState { + labelInput: string; +} + +export 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 }); + }; +} + +/* + +TODO: uncomment the following and fill out the TODO's + +function mapStateToProps(state: Store) { + // TODO: fill this out +} + +function mapDispatchToProps(dispatch: any) { + // TODO: fill this out +} + +const component = connect( + mapStateToProps, + mapDispatchToProps +)(TodoHeader); + +export { component as TodoHeader }; + +*/ diff --git a/step2-07/exercise/src/components/TodoList.tsx b/step2-07/exercise/src/components/TodoList.tsx new file mode 100644 index 0000000..f9781ae --- /dev/null +++ b/step2-07/exercise/src/components/TodoList.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { Stack } from 'office-ui-fabric-react'; +import { TodoListItem } from './TodoListItem'; +import { Store } from '../store'; +import { connect } from 'react-redux'; + +interface TodoListProps { + todos: Store['todos']; +} + +const TodoList = (props: TodoListProps) => { + const { todos } = props; + const filteredTodos = Object.keys(todos); + + 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-07/exercise/src/components/TodoListItem.tsx b/step2-07/exercise/src/components/TodoListItem.tsx new file mode 100644 index 0000000..24ab756 --- /dev/null +++ b/step2-07/exercise/src/components/TodoListItem.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { Stack, Checkbox, IconButton } from 'office-ui-fabric-react'; +import { Store } from '../store'; +import { connect } from 'react-redux'; +import { 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-07/exercise/src/index.tsx b/step2-07/exercise/src/index.tsx new file mode 100644 index 0000000..d9f8f08 --- /dev/null +++ b/step2-07/exercise/src/index.tsx @@ -0,0 +1,25 @@ +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 { actions } from './actions'; +import { initializeIcons } from '@uifabric/icons'; + +/* 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) { + return createStore(reducer, initialStore, composeEnhancers()); +} + +const store = createStoreWithDevTool(reducer, {}); + +store.dispatch(actions.addTodo('hello')); +store.dispatch(actions.addTodo('world')); + +initializeIcons(); + +// TODO: wrap with a instance here +ReactDOM.render(, document.getElementById('app')); diff --git a/step2-07/exercise/src/reducers/index.ts b/step2-07/exercise/src/reducers/index.ts new file mode 100644 index 0000000..6eed3c5 --- /dev/null +++ b/step2-07/exercise/src/reducers/index.ts @@ -0,0 +1,27 @@ +import { Store } from '../store'; +import { addTodo, remove, complete, clear } from './pureFunctions'; + +function todoReducer(state: Store['todos'] = {}, action: any): Store['todos'] { + switch (action.type) { + case 'addTodo': + return addTodo(state, action.id, action.label); + + case 'remove': + return remove(state, action.id); + + case 'clear': + return clear(state); + + case 'complete': + return complete(state, action.id); + } + + return state; +} + +export function reducer(state: Store, action: any): Store { + return { + todos: todoReducer(state.todos, action), + filter: 'all' + }; +} diff --git a/step2-07/exercise/src/reducers/pureFunctions.spec.ts b/step2-07/exercise/src/reducers/pureFunctions.spec.ts new file mode 100644 index 0000000..b3815cf --- /dev/null +++ b/step2-07/exercise/src/reducers/pureFunctions.spec.ts @@ -0,0 +1,29 @@ +import { addTodo, complete } from './pureFunctions'; +import { Store } from '../store'; + +describe('TodoApp reducers', () => { + it('can add an item', () => { + const state = {}; + + const newState = addTodo(state, '0', 'item1'); + + const keys = Object.keys(newState); + + expect(newState).not.toBe(state); + expect(keys.length).toBe(1); + expect(newState[keys[0]].label).toBe('item1'); + expect(newState[keys[0]].completed).toBeFalsy(); + }); + + it('can complete an item', () => { + const state = {}; + + let newState = addTodo(state, '0', 'item1'); + + const key = Object.keys(newState)[0]; + + newState = complete(newState, key); + + expect(newState[key].completed).toBeTruthy(); + }); +}); diff --git a/step2-07/exercise/src/reducers/pureFunctions.ts b/step2-07/exercise/src/reducers/pureFunctions.ts new file mode 100644 index 0000000..69e492a --- /dev/null +++ b/step2-07/exercise/src/reducers/pureFunctions.ts @@ -0,0 +1,36 @@ +import { Store, FilterTypes } from '../store'; + +export function addTodo(state: Store['todos'], id: string, label: string): Store['todos'] { + 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-07/exercise/src/store/index.ts b/step2-07/exercise/src/store/index.ts new file mode 100644 index 0000000..221b5f4 --- /dev/null +++ b/step2-07/exercise/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; +} diff --git a/step2-07/src/components/TodoApp.tsx b/step2-07/src/components/TodoApp.tsx deleted file mode 100644 index ef8f05b..0000000 --- a/step2-07/src/components/TodoApp.tsx +++ /dev/null @@ -1,36 +0,0 @@ -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/TodoApp.tsx b/step2-08/src/components/TodoApp.tsx index ef8f05b..83464c0 100644 --- a/step2-08/src/components/TodoApp.tsx +++ b/step2-08/src/components/TodoApp.tsx @@ -11,26 +11,16 @@ const className = mergeStyles({ ...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 ( - - - - - - - +export const TodoApp = () => { + return ( + + + + + + - - ); - } -} + + + ); +};