diff --git a/actions.js b/actions.js
index 7ee5d03..2e238b3 100644
--- a/actions.js
+++ b/actions.js
@@ -4,9 +4,10 @@
export const ADD_TODO = 'ADD_TODO'
export const COMPLETE_TODO = 'COMPLETE_TODO'
-export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'
+export const SET_PANE_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'
export const CHANGE_THEME = 'CHANGE_THEME'
-export const UPDATE_SEARCH = 'UPDATE_SEARCH';
+export const UPDATE_PANE_SEARCH = 'UPDATE_SEARCH';
+export const ADD_PANE = 'ADD_PANE'
/*
* other constants
@@ -22,6 +23,10 @@ export const VisibilityFilters = {
* action creators
*/
+export function addPane() {
+ return { type: ADD_PANE };
+}
+
export function addTodo(text) {
return { type: ADD_TODO, text }
}
@@ -30,17 +35,18 @@ export function completeTodo(index) {
return { type: COMPLETE_TODO, index }
}
-export function setVisibilityFilter(filter) {
- return { type: SET_VISIBILITY_FILTER, filter }
+export function setPaneVisibilityFilter(paneIdx, filter) {
+ return { type: SET_PANE_VISIBILITY_FILTER, index: paneIdx, filter }
}
export function changeTheme() {
return { type: CHANGE_THEME };
}
-export function updateSearch(searchTerm) {
+export function updatePaneSearch(paneIdx, searchTerm) {
return {
- type: UPDATE_SEARCH,
+ type: UPDATE_PANE_SEARCH,
+ index: paneIdx,
searchTerm
};
}
diff --git a/components/Pane.js b/components/Pane.js
new file mode 100644
index 0000000..bfdf23a
--- /dev/null
+++ b/components/Pane.js
@@ -0,0 +1,44 @@
+import React, { PropTypes, Component } from 'react';
+import { addTodo, completeTodo } from '../actions'
+import AddTodo from './AddTodo';
+import TodoList from './TodoList';
+import Footer from './Footer';
+
+class Pane extends Component {
+ render() {
+ const {
+ dispatch,
+ pane,
+ matchingVisibleTodosForPaneFactory,
+ updateSearch,
+ setVisibilityFilter,
+ ...props
+ } = this.props;
+
+ const { visibilityFilter, searchTerm } = pane;
+
+ console.log("Rendering pane: ")
+ console.log(pane);
+
+ const visibleTodos = matchingVisibleTodosForPaneFactory(visibilityFilter, searchTerm);
+
+ return (
+
+ {pane.key}
+ Search:
+
+
dispatch(addTodo(text)) } />
+ dispatch(completeTodo(index)) }
+ />
+
+
+ );
+ }
+}
+
+export default Pane;
diff --git a/containers/App.js b/containers/App.js
index ce71cd8..2007d90 100644
--- a/containers/App.js
+++ b/containers/App.js
@@ -1,96 +1,49 @@
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
-import { addTodo, completeTodo, setVisibilityFilter, changeTheme, VisibilityFilters, updateSearch } from '../actions'
-import AddTodo from '../components/AddTodo'
-import TodoList from '../components/TodoList'
-import Footer from '../components/Footer'
+import { setPaneVisibilityFilter, changeTheme, VisibilityFilters, updatePaneSearch, addPane } from '../actions'
import { memoize, createMemoizedFunction } from '../memoize'
import { createSelector, createStructuredSelector } from 'reselect';
+import { appSelector } from '../selectors/AppSelector';
+import Pane from '../components/Pane';
-class App extends Component {
- updateSearch = function(e) {
- const { dispatch } = this.props;
- dispatch(updateSearch(e.target.value));
- }
+class App extends Component {
render() {
- console.log(this.props);
- // Injected by connect() call:
- const { dispatch, matchingVisibleTodos, currentTheme, searchTerm, visibilityFilter } = this.props
+ const { dispatch, panes, currentTheme, ...props } = this.props
+
+ var createUpdatePaneSearch = function(paneIdx) {
+ return (e) => {
+ dispatch(updatePaneSearch(paneIdx, e.target.value));
+ };
+ }
+
+ var createSetVisibilityFilter = function(paneIdx) {
+ return (filter) => {
+ dispatch(setPaneVisibilityFilter(paneIdx, filter));
+ }
+ }
+
+ let paneComponents = panes.map((pane, idx) =>
+
+ );
+
return (
)
}
}
-App.propTypes = {
- todos: PropTypes.arrayOf(PropTypes.shape({
- text: PropTypes.string.isRequired,
- completed: PropTypes.bool.isRequired
- })),
- visibilityFilter: PropTypes.oneOf([
- 'SHOW_ALL',
- 'SHOW_COMPLETED',
- 'SHOW_ACTIVE'
- ]).isRequired
-}
-
-function selectVisibleTodos(todos, filter) {
- console.log("Recalculating selectTodos");
- switch (filter) {
- case VisibilityFilters.SHOW_ALL:
- return todos
- case VisibilityFilters.SHOW_COMPLETED:
- return todos.filter(todo => todo.completed)
- case VisibilityFilters.SHOW_ACTIVE:
- return todos.filter(todo => !todo.completed)
- }
-}
-
-function selectMatchingTodos(todos, search) {
- console.log("Recalculating matchingTodos");
- return todos.filter((todo) => { return todo.text.search(search) >= 0; });
-}
-
-const todosSelector = state => state.todos;
-const visibilityFilterSelector = state => state.visibilityFilter;
-const currentThemeSelector = state => state.currentTheme;
-const searchTermSelector = state => state.searchTerm;
-
-const visibleTodosSelector = createSelector(
- [todosSelector, visibilityFilterSelector],
- selectVisibleTodos
-);
-
-const matchingVisibleTodosSelector = createSelector(
- [visibleTodosSelector, searchTermSelector],
- selectMatchingTodos
-);
-
-const select = createStructuredSelector({
- matchingVisibleTodos: matchingVisibleTodosSelector,
- visibilityFilter: visibilityFilterSelector,
- currentTheme: currentThemeSelector,
- searchTerm: searchTermSelector
-});
-
// Wrap the component to inject dispatch and state into it
-export default connect(select)(App)
+export default connect(appSelector)(App);
diff --git a/index.js b/index.js
index 9fd15be..1ff0843 100644
--- a/index.js
+++ b/index.js
@@ -3,14 +3,25 @@ import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './containers/App'
-import todoApp from './reducers'
-
-let store = createStore(todoApp)
+import { DevTools, LogMonitor, DebugPanel } from 'redux-devtools/lib/react';
+import configureStore, {USE_DEV_TOOLS} from './store/configureStore'
+let store = configureStore()
let rootElement = document.getElementById('root')
+
+let debugPannel = USE_DEV_TOOLS ? (
+
+
+ ) : null;
+
+
render(
-
-
- ,
+
+
+
+
+ {debugPannel}
+
+
,
rootElement
)
diff --git a/package.json b/package.json
index 64d9ed9..e13f981 100644
--- a/package.json
+++ b/package.json
@@ -18,11 +18,14 @@
"homepage": "http://rackt.github.io/redux",
"dependencies": {
"classnames": "^2.1.2",
+ "lodash": "^3.10.1",
"react": "^0.14.0",
"react-dom": "^0.14.0",
"react-redux": "^4.0.0",
"redux": "^3.0.0",
- "reselect": "^2.0.0"
+ "redux-devtools": "^2.1.5",
+ "reselect": "^2.0.0",
+ "uid": "0.0.2"
},
"devDependencies": {
"babel-core": "^5.6.18",
diff --git a/reducers.js b/reducers.js
deleted file mode 100644
index bc73343..0000000
--- a/reducers.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { combineReducers } from 'redux'
-import { ADD_TODO, COMPLETE_TODO, SET_VISIBILITY_FILTER, CHANGE_THEME, VisibilityFilters, UPDATE_SEARCH } from './actions'
-const { SHOW_ALL } = VisibilityFilters
-
-function visibilityFilter(state = SHOW_ALL, action) {
- switch (action.type) {
- case SET_VISIBILITY_FILTER:
- return action.filter
- default:
- return state
- }
-}
-
-function todos(state = [], action) {
- switch (action.type) {
- case ADD_TODO:
- return [
- ...state,
- {
- text: action.text,
- completed: false
- }
- ]
- case COMPLETE_TODO:
- return [
- ...state.slice(0, action.index),
- Object.assign({}, state[action.index], {
- completed: true
- }),
- ...state.slice(action.index + 1)
- ]
- default:
- return state
- }
-}
-
-function currentTheme(state = 'theme-green', action) {
- switch (action.type) {
- case CHANGE_THEME:
- return state == 'theme-green' ? 'theme-blue' : 'theme-green';
- default:
- return state
- }
-}
-
-function searchTerm(state = '', action) {
- switch (action.type) {
- case UPDATE_SEARCH:
- return action.searchTerm;
- default:
- return state;
- }
-}
-
-
-const todoApp = combineReducers({
- visibilityFilter,
- todos,
- currentTheme,
- searchTerm
-})
-
-export default todoApp
diff --git a/reducers/CurrentThemeReducer.js b/reducers/CurrentThemeReducer.js
new file mode 100644
index 0000000..66f3012
--- /dev/null
+++ b/reducers/CurrentThemeReducer.js
@@ -0,0 +1,10 @@
+import { CHANGE_THEME } from '../actions'
+
+export function currentTheme(state = 'theme-green', action) {
+ switch (action.type) {
+ case CHANGE_THEME:
+ return state == 'theme-green' ? 'theme-blue' : 'theme-green';
+ default:
+ return state
+ }
+}
diff --git a/reducers/PanesReducer.js b/reducers/PanesReducer.js
new file mode 100644
index 0000000..d77cae0
--- /dev/null
+++ b/reducers/PanesReducer.js
@@ -0,0 +1,41 @@
+import { combineReducers } from 'redux'
+import { ADD_PANE, SET_PANE_VISIBILITY_FILTER, VisibilityFilters, UPDATE_PANE_SEARCH } from '../actions'
+import combineCollectionReducers from './combineCollectionReducers'
+import uid from 'uid';
+
+const { SHOW_ALL } = VisibilityFilters
+
+function searchTerm(state = '', action) {
+ switch (action.type) {
+ case UPDATE_PANE_SEARCH:
+ return action.searchTerm;
+ default:
+ return state;
+ }
+}
+
+function visibilityFilter(state = SHOW_ALL, action) {
+ switch (action.type) {
+ case SET_PANE_VISIBILITY_FILTER:
+ return action.filter
+ default:
+ return state
+ }
+}
+
+const paneElementReducer = combineReducers({
+ key: () => { return uid() },
+ visibilityFilter,
+ searchTerm
+});
+
+function paneCollectionReducer(state = [], action) {
+ switch(action.type) {
+ case ADD_PANE:
+ return state.concat(paneElementReducer(undefined, {type: null}));
+ default:
+ return state;
+ }
+}
+
+export const panes = combineCollectionReducers(paneCollectionReducer, paneElementReducer);
diff --git a/reducers/TodosReducer.js b/reducers/TodosReducer.js
new file mode 100644
index 0000000..2bf0b88
--- /dev/null
+++ b/reducers/TodosReducer.js
@@ -0,0 +1,28 @@
+import { ADD_TODO, COMPLETE_TODO } from '../actions'
+import combineCollectionReducers from './combineCollectionReducers'
+
+function todoElementReducer(state, action) {
+ switch(action.type) {
+ case COMPLETE_TODO:
+ return Object.assign({}, state, { completed: true });
+ default:
+ return state;
+ }
+}
+
+function todoCollectionReducer(state = [], action) {
+ switch(action.type) {
+ case ADD_TODO:
+ return [
+ ...state,
+ {
+ text: action.text,
+ completed: false
+ }
+ ]
+ default:
+ return state;
+ }
+}
+
+export const todos = combineCollectionReducers(todoCollectionReducer, todoElementReducer);
diff --git a/reducers/combineCollectionReducers.js b/reducers/combineCollectionReducers.js
new file mode 100644
index 0000000..abc4346
--- /dev/null
+++ b/reducers/combineCollectionReducers.js
@@ -0,0 +1,29 @@
+// There is a common pattern where the state contains
+// a collection of like objects, with some actions
+// needing to affect an element of the collection (i.e. complete a given todo element)
+// while other actions affect the collection as a whole (i.e. add a new todo element).
+//
+// Generally actions, that affect a given todo need to pass in an `index` so we know
+// which todo the action should affect.
+//
+// This function takes two arguments: the first is a collectionReducer that knows how
+// to handle actions against the collection as a whole.
+// The second argument is an elementReducer that knows how to handle
+// actions against a specific element.
+//
+// The goal is to extract the "index-detection" and state reconstruction logic, so
+// that you only have to focus on how to reduce collection actions and element actions.
+
+export default function combineCollectionReducers(collectionReducer, elementReducer) {
+ return function(state = collectionReducer(undefined, { type: null }), action) {
+ if(action.index !== undefined) {
+ let oldElement = state[action.index];
+ let newElement = elementReducer(oldElement, action);
+ if(newElement !== oldElement) {
+ return state.map((element, idx) => idx === action.index ? newElement : element);
+ }
+ return state;
+ }
+ return collectionReducer(state, action);
+ }
+}
diff --git a/reducers/index.js b/reducers/index.js
new file mode 100644
index 0000000..bc3d23f
--- /dev/null
+++ b/reducers/index.js
@@ -0,0 +1,10 @@
+import { combineReducers } from 'redux'
+import { currentTheme } from './CurrentThemeReducer';
+import { todos } from './TodosReducer';
+import { panes } from './PanesReducer';
+
+export default combineReducers({
+ panes,
+ todos,
+ currentTheme,
+});
diff --git a/selectors/AppSelector.js b/selectors/AppSelector.js
new file mode 100644
index 0000000..2de421d
--- /dev/null
+++ b/selectors/AppSelector.js
@@ -0,0 +1,51 @@
+import { createSelector, createStructuredSelector } from 'reselect';
+import { VisibilityFilters } from '../actions';
+import { memoize } from 'lodash';
+
+const resolver = function() {
+ var args = Array.prototype.slice.call(arguments);
+ return args.join();
+}
+
+const todosSelector = state => state.todos;
+const currentThemeSelector = state => state.currentTheme;
+const panesSelector = state => state.panes;
+
+const visibleTodosForFilterFactorySelector = createSelector(
+ todosSelector,
+ (todos) => {
+ console.log("(reselect) Generating a new visibleTodosFilterFactory because todos changed");
+ return _.memoize((visibilityFilter) => {
+ console.log("Lodash calling visible function for new filter " + visibilityFilter);
+ switch (visibilityFilter) {
+ case VisibilityFilters.SHOW_ALL:
+ return todos
+ case VisibilityFilters.SHOW_COMPLETED:
+ return todos.filter(todo => todo.completed)
+ case VisibilityFilters.SHOW_ACTIVE:
+ return todos.filter(todo => !todo.completed)
+ }
+ }, resolver);
+ }
+)
+
+const matchingVisibleTodosForPaneFactorySelector = createSelector(
+ visibleTodosForFilterFactorySelector,
+ (visibleTodosForFilterFactory) => {
+ console.log("(reselect) generating a new matchingVisibleTodosForPaneFactory because visibleTodosFilterFactory changed");
+ return _.memoize((visibilityFilter, searchTerm) => {
+ console.log("Lodash calling matching function for " + visibilityFilter + " and " + searchTerm);
+ const visibleTodos = visibleTodosForFilterFactory(visibilityFilter)
+ console.log("Calculating matchingTodos for " + searchTerm);
+ return visibleTodos.filter((todo) => {
+ return todo.text.search(searchTerm) >= 0;
+ });
+ }, resolver);
+ }
+);
+
+export const appSelector = createStructuredSelector({
+ panes: panesSelector,
+ matchingVisibleTodosForPaneFactory: matchingVisibleTodosForPaneFactorySelector,
+ currentTheme: currentThemeSelector
+});
diff --git a/store/configureStore.js b/store/configureStore.js
index 479c02c..1957bc7 100644
--- a/store/configureStore.js
+++ b/store/configureStore.js
@@ -1,8 +1,14 @@
-import { createStore } from 'redux'
+import { compose, createStore } from 'redux';
import rootReducer from '../reducers'
+import { devTools } from 'redux-devtools';
+
+export const USE_DEV_TOOLS = false;
export default function configureStore(initialState) {
- const store = createStore(rootReducer, initialState)
+
+ const finalCreateStore = USE_DEV_TOOLS ? compose(devTools())(createStore) : createStore;
+
+ const store = finalCreateStore(rootReducer, initialState)
if (module.hot) {
// Enable Webpack hot module replacement for reducers