From 578ad8221dbde14408bcc821cb87da741cbf3e56 Mon Sep 17 00:00:00 2001 From: Ken Date: Thu, 31 Jan 2019 00:03:02 -0800 Subject: [PATCH] editing begin --- playground/src/actions/index.ts | 4 +- playground/src/components/TodoApp.tsx | 12 ++- .../src/components/TodoAppContainer.tsx | 1 + playground/src/components/TodoFooter.tsx | 16 +++- playground/src/components/TodoHeader.tsx | 8 +- playground/src/components/TodoList.tsx | 36 ++++++- playground/src/components/TodoListItem.tsx | 93 +++++++++++++++---- playground/src/reducers/createReducer.ts | 22 +++-- playground/src/reducers/index.ts | 16 +++- 9 files changed, 164 insertions(+), 44 deletions(-) diff --git a/playground/src/actions/index.ts b/playground/src/actions/index.ts index 08da3c5..3fa900d 100644 --- a/playground/src/actions/index.ts +++ b/playground/src/actions/index.ts @@ -1,7 +1,7 @@ import { Action, ActionCreator } from 'redux'; export type ActionTypes = 'add' | 'remove' | 'edit' | 'complete' | 'completeAll' | 'clear' | 'filter'; -type TodoActionCreator = ActionCreator>; + export interface TodoAction extends Action { [extraProps: string]: any; } @@ -12,4 +12,4 @@ export const edit = (id: string, label: string): TodoAction => ({ type: 'edit', export const complete = (id: string): TodoAction => ({ type: 'complete', id }); export const completeAll = (): TodoAction => ({ type: 'completeAll' }); export const clear = (): TodoAction => ({ type: 'clear' }); -export const filter = (filterTypes: string): TodoAction => ({ type: 'filter', filterTypes }); +export const filter = (filterTypes: string): TodoAction => ({ type: 'filter', filter: filterTypes }); diff --git a/playground/src/components/TodoApp.tsx b/playground/src/components/TodoApp.tsx index adc0cbd..6cb2e06 100644 --- a/playground/src/components/TodoApp.tsx +++ b/playground/src/components/TodoApp.tsx @@ -10,20 +10,22 @@ export interface TodoAppProps { filter: FilterTypes; add: (label: string) => void; remove: (id: string) => void; + edit: (id: string, label: string) => void; complete: (id: string) => void; + clear: () => void; setFilter: (filter: FilterTypes) => void; } export class TodoApp extends React.Component { render() { - const { todos, filter, add, remove, setFilter, complete } = this.props; + const { todos, filter, add, remove, setFilter, complete, clear, edit } = this.props; return ( - - - - + + + + ); diff --git a/playground/src/components/TodoAppContainer.tsx b/playground/src/components/TodoAppContainer.tsx index fb07157..58b12c0 100644 --- a/playground/src/components/TodoAppContainer.tsx +++ b/playground/src/components/TodoAppContainer.tsx @@ -17,6 +17,7 @@ export function mapDispatchToProps(dispatch: Dispatch) { remove: (id: string) => dispatch(actions.remove(id)), complete: (id: string) => dispatch(actions.complete(id)), completeAll: () => dispatch(actions.completeAll()), + edit: (id: string, label: string) => dispatch(actions.edit(id, label)), clear: () => dispatch(actions.clear()), setFilter: (filter: FilterTypes) => dispatch(actions.filter(filter)) }; diff --git a/playground/src/components/TodoFooter.tsx b/playground/src/components/TodoFooter.tsx index 3dc012f..1c4b066 100644 --- a/playground/src/components/TodoFooter.tsx +++ b/playground/src/components/TodoFooter.tsx @@ -1,12 +1,22 @@ import React from 'react'; import { Text, Stack } from '@uifabric/experiments'; +import { TodoItem } from '../store'; +import { DefaultButton } from 'office-ui-fabric-react'; -export interface TodoFooterProps {} +export interface TodoFooterProps { + todos: { [id: string]: TodoItem }; + clear: () => void; +} export const TodoFooter = (props: TodoFooterProps) => { + const itemCount = Object.keys(props.todos).filter(id => !props.todos[id].completed).length; + return ( - - 1 item left + + + {itemCount} item{itemCount > 1 ? 's' : ''} left + + props.clear()}>Clear Completed ); }; diff --git a/playground/src/components/TodoHeader.tsx b/playground/src/components/TodoHeader.tsx index 550bb22..22713a8 100644 --- a/playground/src/components/TodoHeader.tsx +++ b/playground/src/components/TodoHeader.tsx @@ -2,10 +2,12 @@ import React from 'react'; import { Text, Stack } from '@uifabric/experiments'; import { Pivot, PivotItem, TextField } from 'office-ui-fabric-react'; import { add } from '../actions'; +import { FilterTypes } from '../store'; export interface TodoHeaderProps { add: (label: string) => void; remove: (id: string) => void; + filter: (filter: FilterTypes) => void; } export interface TodoHeaderState { @@ -29,6 +31,10 @@ export class TodoHeader extends React.Component { + this.props.filter(item.props.headerText as FilterTypes); + }; + render() { return ( @@ -43,7 +49,7 @@ export class TodoHeader extends React.Component - + diff --git a/playground/src/components/TodoList.tsx b/playground/src/components/TodoList.tsx index 367b1af..357ba2d 100644 --- a/playground/src/components/TodoList.tsx +++ b/playground/src/components/TodoList.tsx @@ -6,17 +6,47 @@ import { TodoItem, FilterTypes } from '../store'; export interface TodoListProps { todos: { [id: string]: TodoItem }; filter: FilterTypes; + edit: (id: string, label: string) => void; complete: (id: string) => void; + remove: (id: string) => void; } export class TodoList extends React.Component { render() { const { filter, todos } = this.props; + let filteredTodos = todos; + + switch (filter) { + case 'completed': + filteredTodos = Object.keys(todos).reduce( + (collection, id) => (todos[id].completed ? { ...collection, id: todos[id] } : collection), + {} + ); + break; + + case 'active': + filteredTodos = Object.keys(todos).reduce( + (collection, id) => (!todos[id].completed ? { ...collection, id: todos[id] } : collection), + {} + ); + break; + } + return ( - {Object.keys(todos).map(id => { - const todo = todos[id]; - return ; + {Object.keys(filteredTodos).map(id => { + const todo = filteredTodos[id]; + return ( + + ); })} ); diff --git a/playground/src/components/TodoListItem.tsx b/playground/src/components/TodoListItem.tsx index 340776c..b4d9932 100644 --- a/playground/src/components/TodoListItem.tsx +++ b/playground/src/components/TodoListItem.tsx @@ -1,31 +1,84 @@ import React from 'react'; -import { Stack } from '@uifabric/experiments'; -import { Checkbox, IconButton } from 'office-ui-fabric-react'; +import { Stack, Text } from '@uifabric/experiments'; +import { Checkbox, IconButton, TextField } from 'office-ui-fabric-react'; import { mergeStyles } from '@uifabric/styling'; export interface TodoListItemProps { id: string; checked: boolean; label: string; + edit: (id: string, label: string) => void; complete: (id: string) => void; + remove: (id: string) => void; } -export const TodoListItem = (props: TodoListItemProps) => { - const className = mergeStyles({ - selectors: { - ':global(.clearButton)': { - display: 'none' - }, - '&:hover :global(.clearButton)': { - display: 'block' - } - } - }); +export interface TodoListItemState { + editing: boolean; + editLabel: string; +} - return ( - - props.complete(props.id)} /> - - - ); -}; +const className = mergeStyles({ + selectors: { + '.clearButton': { + visibility: 'hidden' + }, + '&:hover .clearButton': { + visibility: 'visible' + } + } +}); + +export class TodoListItem extends React.Component { + /** + * + */ + constructor(props: TodoListItemProps) { + super(props); + this.state = { editing: false, editLabel: undefined }; + } + + onEdit = () => { + this.setState(prevState => ({ + editing: true, + editLabel: prevState.editLabel || this.props.label + })); + }; + + onDoneEdit = () => { + this.props.edit(this.props.id, this.state.editLabel); + this.setState(prevState => ({ + editing: false, + editLabel: undefined + })); + }; + + onKeyDown = (evt: React.KeyboardEvent) => { + if (evt.which === 13) { + this.onDoneEdit(); + } + }; + + onChange = (evt: React.FormEvent, newValue: string) => { + this.setState({ editLabel: newValue }); + }; + + render() { + const { label, checked, complete, remove, id } = this.props; + + return ( + + {!this.state.editing && ( + <> + complete(id)} /> +
+ + remove(id)} /> +
+ + )} + + {this.state.editing && } +
+ ); + } +} diff --git a/playground/src/reducers/createReducer.ts b/playground/src/reducers/createReducer.ts index 0131893..2225be6 100644 --- a/playground/src/reducers/createReducer.ts +++ b/playground/src/reducers/createReducer.ts @@ -2,13 +2,23 @@ import { Reducer } from 'redux'; import { ActionTypes, TodoAction } from '../actions'; import { Draft, produce } from 'immer'; -export function createReducer( - initialState: T, - handlers: { [actionType in ActionTypes]?: (state: Draft, action: TodoAction) => T } -): Reducer { +export type ImmerReducer = (state: Draft, action: TodoAction) => T; +export type HandlerMap = { [actionType in ActionTypes]?: ImmerReducer }; + +function isHandlerFunction(handlerOrMap: HandlerMap | ImmerReducer): handlerOrMap is ImmerReducer { + if (typeof handlerOrMap === 'function') { + return true; + } + + return false; +} + +export function createReducer(initialState: T, handlerOrMap: HandlerMap | ImmerReducer): Reducer { return function reducer(state = initialState, action: TodoAction): T { - if (handlers.hasOwnProperty(action.type)) { - return produce(state, draft => handlers[action.type](draft, action)); + if (isHandlerFunction(handlerOrMap)) { + return produce(state, draft => handlerOrMap(draft, action)); + } else if (handlerOrMap.hasOwnProperty(action.type)) { + return produce(state, draft => handlerOrMap[action.type](draft, action)); } else { return state; } diff --git a/playground/src/reducers/index.ts b/playground/src/reducers/index.ts index 0749c38..13b4fff 100644 --- a/playground/src/reducers/index.ts +++ b/playground/src/reducers/index.ts @@ -1,6 +1,7 @@ import { createReducer } from './createReducer'; import { Store, FilterTypes } from '../store'; import { combineReducers } from 'redux'; +import produce from 'immer'; let counter = 0; @@ -22,12 +23,19 @@ export const reducer = combineReducers({ complete(draft, action) { draft[action.id].completed = !draft[action.id].completed; return draft; + }, + + clear(draft, action) { + Object.keys(draft).forEach(id => { + if (draft[id].completed) { + delete draft[id]; + } + }); + return draft; } } ), - filter: createReducer('all', { - filter(draft, action) { - return action.filter; - } + filter: createReducer('all', (draft, action) => { + return action.filter; }) });