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