diff --git a/step2-06/demo/index.html b/step2-06/demo/index.html new file mode 100644 index 0000000..ee4b5cb --- /dev/null +++ b/step2-06/demo/index.html @@ -0,0 +1,6 @@ + + + +
+ + diff --git a/step2-06/demo/src/actions/index.ts b/step2-06/demo/src/actions/index.ts new file mode 100644 index 0000000..4a067b9 --- /dev/null +++ b/step2-06/demo/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-06/demo/src/index.tsx b/step2-06/demo/src/index.tsx new file mode 100644 index 0000000..e4ca608 --- /dev/null +++ b/step2-06/demo/src/index.tsx @@ -0,0 +1,19 @@ +import { reducer } from './reducers'; +import { createStore, compose } from 'redux'; +import { actions } from './actions'; + +/* 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, {}); + +console.log(store.getState()); + +store.dispatch(actions.addTodo('hello')); +store.dispatch(actions.addTodo('world')); + +console.log(store.getState()); diff --git a/step2-06/demo/src/reducers/index.ts b/step2-06/demo/src/reducers/index.ts new file mode 100644 index 0000000..6eed3c5 --- /dev/null +++ b/step2-06/demo/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-06/demo/src/reducers/pureFunctions.spec.ts b/step2-06/demo/src/reducers/pureFunctions.spec.ts new file mode 100644 index 0000000..b3815cf --- /dev/null +++ b/step2-06/demo/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-06/demo/src/reducers/pureFunctions.ts b/step2-06/demo/src/reducers/pureFunctions.ts new file mode 100644 index 0000000..69e492a --- /dev/null +++ b/step2-06/demo/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-06/demo/src/store/index.ts b/step2-06/demo/src/store/index.ts new file mode 100644 index 0000000..221b5f4 --- /dev/null +++ b/step2-06/demo/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-06/exercise/index.html b/step2-06/exercise/index.html new file mode 100644 index 0000000..af40d4d --- /dev/null +++ b/step2-06/exercise/index.html @@ -0,0 +1,6 @@ + + + +
+ + diff --git a/step2-06/exercise/src/actions/index.ts b/step2-06/exercise/src/actions/index.ts new file mode 100644 index 0000000..d75c2d9 --- /dev/null +++ b/step2-06/exercise/src/actions/index.ts @@ -0,0 +1,5 @@ +import uuid from 'uuid/v4'; + +export const actions = { + addTodo: (label: string) => ({ type: 'addTodo', id: uuid(), label }) +}; diff --git a/step2-06/exercise/src/index.tsx b/step2-06/exercise/src/index.tsx new file mode 100644 index 0000000..e4ca608 --- /dev/null +++ b/step2-06/exercise/src/index.tsx @@ -0,0 +1,19 @@ +import { reducer } from './reducers'; +import { createStore, compose } from 'redux'; +import { actions } from './actions'; + +/* 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, {}); + +console.log(store.getState()); + +store.dispatch(actions.addTodo('hello')); +store.dispatch(actions.addTodo('world')); + +console.log(store.getState()); diff --git a/step2-06/exercise/src/reducers/index.ts b/step2-06/exercise/src/reducers/index.ts new file mode 100644 index 0000000..6eed3c5 --- /dev/null +++ b/step2-06/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-06/exercise/src/reducers/pureFunctions.spec.ts b/step2-06/exercise/src/reducers/pureFunctions.spec.ts new file mode 100644 index 0000000..b3815cf --- /dev/null +++ b/step2-06/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-06/exercise/src/reducers/pureFunctions.ts b/step2-06/exercise/src/reducers/pureFunctions.ts new file mode 100644 index 0000000..69e492a --- /dev/null +++ b/step2-06/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-06/exercise/src/reducers/reducer.spec.ts b/step2-06/exercise/src/reducers/reducer.spec.ts new file mode 100644 index 0000000..e97c35e --- /dev/null +++ b/step2-06/exercise/src/reducers/reducer.spec.ts @@ -0,0 +1,10 @@ +describe('reducers', () => { + it('should listen to addTodo message', () => { + const store = createStoreWithDevTool(reducer, {}); + + console.log(store.getState()); + + store.dispatch(actions.addTodo('hello')); + store.dispatch(actions.addTodo('world')); + }); +}); diff --git a/step2-06/exercise/src/store/index.ts b/step2-06/exercise/src/store/index.ts new file mode 100644 index 0000000..221b5f4 --- /dev/null +++ b/step2-06/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; +}