diff --git a/index.html b/index.html
index 606889d..72436b4 100644
--- a/index.html
+++ b/index.html
@@ -115,10 +115,11 @@
-
+
Step 7
Redux: Connect to UI
-
+
+
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 (
+
+
+
+
+
+
-
- );
- }
-}
+
+
+ );
+};