adding a step 9 to call services

This commit is contained in:
Ken
2019-02-18 11:48:34 -08:00
parent 279a43fb35
commit a709417642
39 changed files with 737 additions and 124 deletions

78
package-lock.json generated
View File

@@ -2170,6 +2170,11 @@
"integrity": "sha512-wv7IRqCGsL7WGKB8gPvrl+++HlFM9kxAM6jL1EXNPNTshEJYilMkbfS2SnuHha77uosp/YVK0wAp2jmlBzn1tg==",
"dev": true
},
"curriable": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/curriable/-/curriable-1.2.5.tgz",
"integrity": "sha512-hQwrkCn8DNiCw5CG8OS0td2wfpCDtxo1dmrVYxCDWUBHQPkpAvN9RqBVbmC64oSQaBqPQD2SOCXcTWH1zXe2mA=="
},
"cyclist": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz",
@@ -3122,6 +3127,11 @@
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
"dev": true
},
"fast-equals": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-1.6.2.tgz",
"integrity": "sha512-6FiKwHkrHIqdvo9I92yzCR/zuWE6iy5ldcOszStPbmo7Zzj3OoVeng++GE//JqO4i6JQ4vH/BVAfCmUCki+C3g=="
},
"fast-json-stable-stringify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
@@ -4518,6 +4528,14 @@
"postcss": "^7.0.5"
}
},
"identitate": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/identitate/-/identitate-1.0.1.tgz",
"integrity": "sha512-xnDJ0JYhiZjBDuJRKbHoVzj5yP9FhATxLyUYswQyPdnJrwzGVBqS6DOmvKJi1lk7P+4dkL+hhUhuOZIcOUtG5A==",
"requires": {
"pathington": "^1.0.1"
}
},
"ieee754": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz",
@@ -6376,8 +6394,7 @@
"json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
"dev": true
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
},
"json3": {
"version": "3.3.2",
@@ -7626,6 +7643,11 @@
}
}
},
"pathington": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/pathington/-/pathington-1.1.7.tgz",
"integrity": "sha512-JxzhUzagDfNIOm4qqwQqP3rWeo7rNNOfIahy4n+3GTEdwXLqw5cJHUR0soSopQtNEv763lzxb6eA2xBllpR8zw=="
},
"pbkdf2": {
"version": "3.0.17",
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz",
@@ -8168,6 +8190,33 @@
"symbol-observable": "^1.2.0"
}
},
"redux-devtools-extension": {
"version": "2.13.8",
"resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz",
"integrity": "sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg=="
},
"redux-immutable-state-invariant": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/redux-immutable-state-invariant/-/redux-immutable-state-invariant-2.1.0.tgz",
"integrity": "sha512-3czbDKs35FwiBRsx/3KabUk5zSOoTXC+cgVofGkpBNv3jQcqIe5JrHcF5AmVt7B/4hyJ8MijBIpCJ8cife6yJg==",
"requires": {
"invariant": "^2.1.0",
"json-stringify-safe": "^5.0.1"
}
},
"redux-starter-kit": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/redux-starter-kit/-/redux-starter-kit-0.4.3.tgz",
"integrity": "sha512-T3HXtwEGyFLCy63QlZEM95rDzTQ5BKbBhhTINNVFD1yWgHBKtgN3tXw4/sX9+Ve1Y8mAtXbAhep/E7I0YVvZPg==",
"requires": {
"immer": "^1.10.5",
"redux": "^4.0.0",
"redux-devtools-extension": "^2.13.7",
"redux-immutable-state-invariant": "^2.1.0",
"redux-thunk": "^2.2.0",
"selectorator": "^4.0.1"
}
},
"redux-thunk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
@@ -8386,6 +8435,11 @@
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
"dev": true
},
"reselect": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz",
"integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA=="
},
"resolve": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz",
@@ -8533,6 +8587,17 @@
"integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=",
"dev": true
},
"selectorator": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/selectorator/-/selectorator-4.0.3.tgz",
"integrity": "sha512-A8+paRhzTab4Qm/38RAVnCgEZFbpn5xIWLyTCDqvyU3Obhmo94RS6UK1H00bVH7+U609sOhqbFJha09POsWURA==",
"requires": {
"fast-equals": "^1.2.1",
"identitate": "^1.0.0",
"reselect": "^4.0.0",
"unchanged": "^2.0.1"
}
},
"selfsigned": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.4.tgz",
@@ -9754,6 +9819,15 @@
}
}
},
"unchanged": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/unchanged/-/unchanged-2.1.0.tgz",
"integrity": "sha512-CYVU0Mz35N821Pjxf+iG6JMPx/Yb6wStUq8uuhR52/+AU3vMiueOTrJ6u1vBKYSke7a75FAi2/jCdQkXcHaNGQ==",
"requires": {
"curriable": "^1.2.4",
"pathington": "^1.1.5"
}
},
"undefsafe": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz",

View File

@@ -51,6 +51,7 @@
"react-dom": "^16.7.0",
"react-redux": "^6.0.0",
"redux-thunk": "^2.3.0",
"redux-starter-kit": "^0.4.3",
"redux": "^4.0.1"
}
}

View File

@@ -1,3 +1,8 @@
export const addTodo = (label: string) => ({ type: 'addTodo', label });
export const remove = (id: string) => ({ type: 'remove', id });
export const complete = (id: string) => ({ type: 'complete', id });
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' })
};

View File

@@ -1,6 +1,6 @@
import { reducer } from './reducers';
import { createStore, compose } from 'redux';
import { addTodo } from './actions';
import { actions } from './actions';
/* Goop for making the Redux dev tool to work */
declare var window: any;
@@ -13,7 +13,7 @@ const store = createStoreWithDevTool(reducer, {});
console.log(store.getState());
store.dispatch(addTodo('hello'));
store.dispatch(addTodo('world'));
store.dispatch(actions.addTodo('hello'));
store.dispatch(actions.addTodo('world'));
console.log(store.getState());

View File

@@ -1,17 +1,27 @@
import { Store } from '../store';
import { addTodo, remove, complete } from './pureFunctions';
import { addTodo, remove, complete, clear } from './pureFunctions';
export function reducer(state: Store, action: any): Store {
function todoReducer(state: Store['todos'] = {}, action: any): Store['todos'] {
switch (action.type) {
case 'addTodo':
return addTodo(state, action.label);
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'
};
}

View File

@@ -1,20 +1,29 @@
import { addTodo } from './pureFunctions';
import { addTodo, complete } from './pureFunctions';
import { Store } from '../store';
describe('TodoApp reducers', () => {
it('can add an item', () => {
const state = <Store>{
todos: {},
filter: 'all'
};
const state = <Store['todos']>{};
const newState = addTodo(state, 'item1');
const newState = addTodo(state, '0', 'item1');
const keys = Object.keys(newState.todos);
const keys = Object.keys(newState);
expect(newState).not.toBe(state);
expect(keys.length).toBe(1);
expect(newState.todos[keys[0]].label).toBe('item1');
expect(newState.todos[keys[0]].completed).toBeFalsy();
expect(newState[keys[0]].label).toBe('item1');
expect(newState[keys[0]].completed).toBeFalsy();
});
it('can complete an item', () => {
const state = <Store['todos']>{};
let newState = addTodo(state, '0', 'item1');
const key = Object.keys(newState)[0];
newState = complete(newState, key);
expect(newState[key].completed).toBeTruthy();
});
});

View File

@@ -1,34 +1,36 @@
import { Store } from '../store';
import { Store, FilterTypes } from '../store';
let index = 0;
export function addTodo(state: Store, label: string): Store {
const { todos } = state;
const id = index++;
return {
...state,
todos: { ...todos, [id]: { label, completed: false } }
};
export function addTodo(state: Store['todos'], id: string, label: string): Store['todos'] {
return { ...state, [id]: { label, completed: false } };
}
export function remove(state: Store, id: string) {
const newTodos = { ...state.todos };
export function remove(state: Store['todos'], id: string) {
const newTodos = { ...state };
delete newTodos[id];
return {
...state,
todos: newTodos
};
return newTodos;
}
export function complete(state: Store, id: string) {
const newTodos = { ...state.todos };
export function complete(state: Store['todos'], id: string) {
const newTodos = { ...state };
newTodos[id].completed = !newTodos[id].completed;
return {
...state,
todos: newTodos
};
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;
}

View File

@@ -1,4 +1,8 @@
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' });
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' })
};

View File

@@ -4,7 +4,7 @@ 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';
import { actions } from '../actions';
interface TodoFooterProps {
clear: () => void;

View File

@@ -1,10 +1,10 @@
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 { TextField, PrimaryButton } from 'office-ui-fabric-react';
import { Store } from '../store';
import { connect } from 'react-redux';
import { actions } from '../actions';
interface TodoHeaderProps {
addTodo: (label: string) => void;

View File

@@ -1,9 +1,8 @@
import React from 'react';
import { Stack } from 'office-ui-fabric-react';
import { TodoListItem } from './TodoListItem';
import { Store, FilterTypes } from '../store';
import { Store } from '../store';
import { connect } from 'react-redux';
import * as actions from '../actions';
interface TodoListProps {
todos: Store['todos'];

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { Stack, Checkbox, IconButton, TextField, DefaultButton } from 'office-ui-fabric-react';
import { Stack, Checkbox, IconButton } from 'office-ui-fabric-react';
import { Store } from '../store';
import { connect } from 'react-redux';
import * as actions from '../actions';
import { actions } from '../actions';
interface TodoListItemProps {
id: string;

View File

@@ -4,7 +4,7 @@ import { reducer } from './reducers';
import { createStore, compose } from 'redux';
import { Provider } from 'react-redux';
import { TodoApp } from './components/TodoApp';
import { addTodo } from './actions';
import { actions } from './actions';
import { initializeIcons } from '@uifabric/icons';
/* Goop for making the Redux dev tool to work */
@@ -16,8 +16,8 @@ function createStoreWithDevTool(reducer, initialStore) {
const store = createStoreWithDevTool(reducer, {});
store.dispatch(addTodo('hello'));
store.dispatch(addTodo('world'));
store.dispatch(actions.addTodo('hello'));
store.dispatch(actions.addTodo('world'));
initializeIcons();

View File

@@ -1,10 +1,10 @@
import { Store } from '../store';
import { addTodo, remove, complete, clear } from './pureFunctions';
export function reducer(state: Store, action: any): Store {
function todoReducer(state: Store['todos'] = {}, action: any): Store['todos'] {
switch (action.type) {
case 'addTodo':
return addTodo(state, action.label);
return addTodo(state, action.id, action.label);
case 'remove':
return remove(state, action.id);
@@ -18,3 +18,10 @@ export function reducer(state: Store, action: any): Store {
return state;
}
export function reducer(state: Store, action: any): Store {
return {
todos: todoReducer(state.todos, action),
filter: 'all'
};
}

View File

@@ -3,33 +3,27 @@ import { Store } from '../store';
describe('TodoApp reducers', () => {
it('can add an item', () => {
const state = <Store>{
todos: {},
filter: 'all'
};
const state = <Store['todos']>{};
const newState = addTodo(state, 'item1');
const newState = addTodo(state, '0', 'item1');
const keys = Object.keys(newState.todos);
const keys = Object.keys(newState);
expect(newState).not.toBe(state);
expect(keys.length).toBe(1);
expect(newState.todos[keys[0]].label).toBe('item1');
expect(newState.todos[keys[0]].completed).toBeFalsy();
expect(newState[keys[0]].label).toBe('item1');
expect(newState[keys[0]].completed).toBeFalsy();
});
it('can complete an item', () => {
const state = <Store>{
todos: {},
filter: 'all'
};
const state = <Store['todos']>{};
let newState = addTodo(state, 'item1');
let newState = addTodo(state, '0', 'item1');
const key = Object.keys(newState.todos)[0];
const key = Object.keys(newState)[0];
newState = complete(newState, key);
expect(newState.todos[key].completed).toBeTruthy();
expect(newState[key].completed).toBeTruthy();
});
});

View File

@@ -1,49 +1,36 @@
import { Store } from '../store';
import { Store, FilterTypes } from '../store';
let index = 0;
export function addTodo(state: Store, label: string): Store {
const { todos } = state;
const id = index++;
return {
...state,
todos: { ...todos, [id]: { label, completed: false } }
};
export function addTodo(state: Store['todos'], id: string, label: string): Store['todos'] {
return { ...state, [id]: { label, completed: false } };
}
export function remove(state: Store, id: string) {
const newTodos = { ...state.todos };
export function remove(state: Store['todos'], id: string) {
const newTodos = { ...state };
delete newTodos[id];
return {
...state,
todos: newTodos
};
return newTodos;
}
export function complete(state: Store, id: string) {
const newTodos = { ...state.todos };
export function complete(state: Store['todos'], id: string) {
const newTodos = { ...state };
newTodos[id].completed = !newTodos[id].completed;
return {
...state,
todos: newTodos
};
return newTodos;
}
export function clear(state: Store) {
const newTodos = {};
export function clear(state: Store['todos']) {
const newTodos = { ...state };
Object.keys(state.todos).forEach(key => {
if (!state.todos[key].completed) {
newTodos[key] = state.todos[key];
if (state.todos[key].completed) {
delete newTodos[key];
}
});
return {
...state,
todos: newTodos
};
return newTodos;
}
export function setFilter(state: Store['filter'], filter: FilterTypes) {
return filter;
}

View File

@@ -1,5 +1,9 @@
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 });
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' }),
setFilter: (filter: string) => ({ type: 'setFilter', filter })
};

View File

@@ -4,7 +4,7 @@ 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';
import { actions } from '../actions';
interface TodoFooterProps {
clear: () => void;

View File

@@ -3,7 +3,7 @@ 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 { actions } from '../actions';
import { connect } from 'react-redux';
interface TodoHeaderProps {

View File

@@ -3,7 +3,6 @@ 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'];

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { Stack, Checkbox, IconButton, TextField, DefaultButton } from 'office-ui-fabric-react';
import { Stack, Checkbox, IconButton } from 'office-ui-fabric-react';
import { Store } from '../store';
import { connect } from 'react-redux';
import * as actions from '../actions';
import { actions } from '../actions';
interface TodoListItemProps {
id: string;

View File

@@ -4,7 +4,7 @@ import { reducer } from './reducers';
import { createStore, compose } from 'redux';
import { Provider } from 'react-redux';
import { TodoApp } from './components/TodoApp';
import { addTodo } from './actions';
import { actions } from './actions';
import { initializeIcons } from '@uifabric/icons';
import { Store } from './store';
@@ -17,8 +17,8 @@ function createStoreWithDevTool(reducer, initialStore?: Store) {
const store = createStoreWithDevTool(reducer);
store.dispatch(addTodo('hello'));
store.dispatch(addTodo('world'));
store.dispatch(actions.addTodo('hello'));
store.dispatch(actions.addTodo('world'));
initializeIcons();

View File

@@ -5,7 +5,7 @@ import { combineReducers } from 'redux';
function todoReducer(state: Store['todos'] = {}, action: any): Store['todos'] {
switch (action.type) {
case 'addTodo':
return addTodo(state, action.label);
return addTodo(state, action.id, action.label);
case 'remove':
return remove(state, action.id);

View File

@@ -0,0 +1,29 @@
import { addTodo, complete } from './pureFunctions';
import { Store } from '../store';
describe('TodoApp reducers', () => {
it('can add an item', () => {
const state = <Store['todos']>{};
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 = <Store['todos']>{};
let newState = addTodo(state, '0', 'item1');
const key = Object.keys(newState)[0];
newState = complete(newState, key);
expect(newState[key].completed).toBeTruthy();
});
});

View File

@@ -1,9 +1,6 @@
import { Store, FilterTypes } from '../store';
let index = 0;
export function addTodo(state: Store['todos'], label: string): Store['todos'] {
const id = index++;
export function addTodo(state: Store['todos'], id: string, label: string): Store['todos'] {
return { ...state, [id]: { label, completed: false } };
}

3
step2-09/README.md Normal file
View File

@@ -0,0 +1,3 @@
# Step 2.9
actions with service calls

6
step2-09/index.html Normal file
View File

@@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<div id="app"></div>
</body>
</html>

View File

@@ -0,0 +1,43 @@
import uuid from 'uuid/v4';
import { Store } from '../store';
import * as service from '../service';
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' }),
setFilter: (filter: string) => ({ type: 'setFilter', filter })
};
export const actionsWithService = {
addTodo: (label: string) => {
return async (dispatch: any, getState: () => Store) => {
const addAction = actions.addTodo(label);
const id = addAction.id;
dispatch(addAction);
await service.add(id, getState().todos[id]);
};
},
remove: (id: string) => {
return async (dispatch: any, getState: () => Store) => {
dispatch(actions.remove(id));
await service.remove(id);
};
},
complete: (id: string) => {
return async (dispatch: any, getState: () => Store) => {
dispatch(actions.complete(id));
await service.edit(id, getState().todos[id]);
};
},
clear: () => {
return async (dispatch: any, getState: () => Store) => {
dispatch(actions.clear());
await service.editBulk(getState().todos);
};
}
};

View File

@@ -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<any, Store> {
constructor(props) {
super(props);
this.state = {
todos: {},
filter: 'all'
};
}
render() {
const { filter, todos } = this.state;
return (
<Customizer {...FluentCustomizations}>
<Stack horizontalAlign="center">
<Stack style={{ width: 400 }} gap={25} className={className}>
<TodoHeader />
<TodoList />
<TodoFooter />
</Stack>
</Stack>
</Customizer>
);
}
}

View File

@@ -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 { actionsWithService } 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 (
<Stack horizontal horizontalAlign="space-between">
<Text>
{itemCount} item{itemCount > 1 ? 's' : ''} left
</Text>
<DefaultButton onClick={() => props.clear()}>Clear Completed</DefaultButton>
</Stack>
);
};
function mapStateToProps(state: Store) {
return { ...state };
}
function mapDispatchToProps(dispatch: any) {
return {
clear: () => dispatch(actionsWithService.clear())
};
}
const component = connect(
mapStateToProps,
mapDispatchToProps
)(TodoFooter);
export { component as TodoFooter };

View File

@@ -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 { actionsWithService, 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<TodoHeaderProps, TodoHeaderState> {
constructor(props: TodoHeaderProps) {
super(props);
this.state = { labelInput: undefined };
}
render() {
return (
<Stack gap={10}>
<Stack horizontal horizontalAlign="center">
<Text variant="xxLarge">todos</Text>
</Stack>
<Stack horizontal gap={10}>
<Stack.Item grow>
<TextField placeholder="What needs to be done?" value={this.state.labelInput} onChange={this.onChange} />
</Stack.Item>
<PrimaryButton onClick={this.onAdd}>Add</PrimaryButton>
</Stack>
<Pivot onLinkClick={this.onFilter}>
<PivotItem headerText="all" />
<PivotItem headerText="active" />
<PivotItem headerText="completed" />
</Pivot>
</Stack>
);
}
private onAdd = () => {
this.props.addTodo(this.state.labelInput);
this.setState({ labelInput: undefined });
};
private onChange = (evt: React.FormEvent<HTMLInputElement>, 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(actionsWithService.addTodo(label)),
setFilter: (filter: FilterTypes) => dispatch(actions.setFilter(filter))
};
}
const component = connect(
mapStateToProps,
mapDispatchToProps
)(TodoHeader);
export { component as TodoHeader };

View File

@@ -0,0 +1,40 @@
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';
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 (
<Stack gap={10}>
{filteredTodos.map(id => (
<TodoListItem key={id} id={id} />
))}
</Stack>
);
};
function mapStateToProps(state: Store) {
return { ...state };
}
function mapDispatchToProps(dispatch: any) {
return {};
}
const component = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList);
export { component as TodoList };

View File

@@ -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 { actionsWithService } from '../actions';
interface TodoListItemProps {
id: string;
todos: Store['todos'];
remove: (id: string) => void;
complete: (id: string) => void;
}
class TodoListItem extends React.Component<TodoListItemProps, {}> {
render() {
const { todos, id, complete, remove } = this.props;
const item = todos[id];
return (
<Stack horizontal verticalAlign="center" horizontalAlign="space-between">
<Checkbox label={item.label} checked={item.completed} onChange={() => complete(id)} />
<div>
<IconButton iconProps={{ iconName: 'Cancel' }} onClick={() => remove(id)} />
</div>
</Stack>
);
}
}
function mapStateToProps({ todos }: Store) {
return {
todos
};
}
function mapDispatchToProps(dispatch: any) {
return {
remove: (id: string) => dispatch(actionsWithService.remove(id)),
complete: (id: string) => dispatch(actionsWithService.complete(id))
};
}
const component = connect(
mapStateToProps,
mapDispatchToProps
)(TodoListItem);
export { component as TodoListItem };

30
step2-09/src/index.tsx Normal file
View File

@@ -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 { actions } 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(actions.addTodo('hello'));
store.dispatch(actions.addTodo('world'));
initializeIcons();
ReactDOM.render(
<Provider store={store}>
<TodoApp />
</Provider>,
document.getElementById('app')
);

View File

@@ -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.id, 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
});

View File

@@ -0,0 +1,29 @@
import { addTodo, complete } from './pureFunctions';
import { Store } from '../store';
describe('TodoApp reducers', () => {
it('can add an item', () => {
const state = <Store['todos']>{};
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 = <Store['todos']>{};
let newState = addTodo(state, '0', 'item1');
const key = Object.keys(newState)[0];
newState = complete(newState, key);
expect(newState[key].completed).toBeTruthy();
});
});

View File

@@ -0,0 +1,40 @@
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 edit(state: Store['todos'], id: string, label: string): Store['todos'] {
return { ...state, [id]: { ...state[id], label } };
}
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;
}

View File

@@ -0,0 +1,48 @@
import { TodoItem, Store } from '../store';
const HOST = 'http://localhost:3000';
export async function add(id: string, todo: TodoItem) {
const response = await fetch(`${HOST}/todos/${id}`, {
method: 'post',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(todo)
});
return await response.json();
}
export async function edit(id: string, todo: TodoItem) {
const response = await fetch(`${HOST}/todos/${id}`, {
method: 'put',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(todo)
});
return await response.json();
}
export async function remove(id: string) {
const response = await fetch(`${HOST}/todos/${id}`, {
method: 'delete'
});
return await response.json();
}
export async function getAll() {
const response = await fetch(`${HOST}/todos`, {
method: 'get'
});
return await response.json();
}
export async function editBulk(todos: Store['todos']) {
const response = await fetch(`${HOST}/todos`, {
method: 'post',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(todos)
});
return await response.json();
}

View File

@@ -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;
}