A Pen by subpopular on CodePen.
Created
January 27, 2016 18:52
-
-
Save darekrossman/61eb1e01fd507d75851b to your computer and use it in GitHub Desktop.
Redux Todo App
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div id="root"></div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { Map, List, fromJS } = Immutable; | |
const { Component } = React | |
const { createStore, combineReducers } = Redux; | |
const initialState = { | |
todos: [], | |
visibilityFilter: 'SHOW_ALL' | |
} | |
/** | |
* | |
* REDUCERS | |
* | |
*/ | |
const addTodo = (state, action) => { | |
const todos = state.get('todos'); | |
const newTodo = fromJS({ | |
id: getTodoId(todos), | |
text: action.text, | |
completed: false | |
}) | |
return state.set('todos', todos.push(newTodo)) | |
} | |
const deleteTodo = (state, action) => { | |
const todoIndex = state.get('todos').findIndex(t => t.get('id') === action.id) | |
return state.deleteIn(['todos', todoIndex]) | |
} | |
const editTodo = (state, action) => { | |
const todos = state.get('todos') | |
const todoIndex = todos.findIndex(t => t.get('id') === action.id) | |
return state.setIn(['todos', todoIndex, 'text'], action.text) | |
} | |
const toggleTodo = (state, action) => { | |
const todos = state.get('todos') | |
const [todoIndex, todo] = todos.findEntry(t => t.get('id') === action.id) | |
return state.setIn( | |
['todos', todoIndex], | |
todo.set('completed', !todo.get('completed')) | |
) | |
} | |
const setFilter = (state, action) => { | |
return action.filter ? | |
state.set('visibilityFilter', action.filter) : | |
state | |
} | |
const todoList = (state = initialState, action) => { | |
if (!Map.isMap(state) && !List.isList(state)) | |
state = Immutable.fromJS(state); | |
const handlers = { | |
ADD_TODO: addTodo, | |
DELETE_TODO: deleteTodo, | |
EDIT_TODO: editTodo, | |
TOGGLE_TODO: toggleTodo, | |
SET_FILTER: setFilter | |
}; | |
return handlers[action.type] ? | |
handlers[action.type](state, action) : | |
state; | |
} | |
/** | |
* | |
* COMPONENTS | |
* | |
*/ | |
const Todo = ({ | |
todo, | |
editing, | |
todoEditInputRef, | |
toggleTodo, | |
beginEditing, | |
endEditing, | |
deleteTodo | |
}) => { | |
return ( | |
<div | |
className={(!todo.completed ? 'TodoApp__todo' : 'TodoApp__todo--completed') + (editing ? ' editing' : '')} | |
onDoubleClick={e => beginEditing(todo.id)}> | |
<div className='TodoApp__todo__primary'> | |
<button className="material-icons TodoApp__todo__complete-btn" onClick={() => toggleTodo(todo.id)}>done</button> | |
{editing ? | |
<input | |
className='TodoApp__todo__edit-input' | |
ref={todoEditInputRef} | |
onKeyUp={e => { if (e.keyCode === 13) endEditing(todo.id) }} | |
onBlur={e => endEditing(todo.id)} | |
defaultValue={todo.text}/> | |
: | |
<div className='TodoApp__todo__label'>{todo.text}</div> | |
} | |
</div> | |
<button className="material-icons TodoApp__todo__delete-btn" onClick={() => deleteTodo(todo.id)}>close</button> | |
</div> | |
) | |
} | |
class TodoApp extends Component { | |
constructor(props) { | |
super(props) | |
this.state = { | |
editing: null | |
} | |
} | |
render() { | |
const { todos, visibilityFilter } = this.props.store.getState().todoList.toJS() | |
const filteredTodos = todos.filter(t => { | |
if (visibilityFilter === 'SHOW_ACTIVE') | |
return !t.completed | |
if (visibilityFilter === 'SHOW_COMPLETED') | |
return t.completed | |
return true | |
}) | |
return ( | |
<div className='TodoApp'> | |
<div className='TodoApp__filters'> | |
<button className={visibilityFilter === 'SHOW_ALL' ? 'TodoApp__filter__btn--active' : 'TodoApp__filter__btn'} onClick={() => this.setFilter('SHOW_ALL')}>All</button> | |
<button className={visibilityFilter === 'SHOW_ACTIVE' ? 'TodoApp__filter__btn--active' : 'TodoApp__filter__btn'} onClick={() => this.setFilter('SHOW_ACTIVE')}>Active</button> | |
<button className={visibilityFilter === 'SHOW_COMPLETED' ? 'TodoApp__filter__btn--active' : 'TodoApp__filter__btn'} onClick={() => this.setFilter('SHOW_COMPLETED')}>Complete</button> | |
</div> | |
<div className='TodoApp__form'> | |
<input | |
className='TodoApp__form__input' | |
ref={node => this.input = node} | |
placeholder='Add a todo...' | |
onKeyUp={e => this.handleKeyUp(e)}/> | |
</div> | |
<div className='TodoApp__todo-list'> | |
{filteredTodos.map(todo => | |
<Todo | |
todo={todo} | |
editing={this.state.editing === todo.id} | |
todoEditInputRef={node => this.todoEditInput = node} | |
toggleTodo={id => this.toggleTodo(id)} | |
beginEditing={id => this.beginEditing(id)} | |
endEditing={id => this.endEditing(id)} | |
deleteTodo={id => this.deleteTodo(id)} />)} | |
</div> | |
</div> | |
) | |
} | |
addTodo() { | |
if (!this.input.value) | |
return | |
this.props.store.dispatch({ | |
type: 'ADD_TODO', | |
text: this.input.value | |
}) | |
this.input.value = '' | |
} | |
toggleTodo(id) { | |
this.props.store.dispatch({ | |
type: 'TOGGLE_TODO', | |
id | |
}) | |
} | |
deleteTodo(id) { | |
this.props.store.dispatch({ | |
type: 'DELETE_TODO', | |
id | |
}) | |
} | |
setFilter(filter) { | |
this.props.store.dispatch({ | |
type: 'SET_FILTER', | |
filter | |
}) | |
} | |
handleKeyUp(e) { | |
if (e.keyCode === 13) | |
this.addTodo() | |
} | |
beginEditing(id) { | |
this.setState({editing: id}, () => { | |
let node = this.todoEditInput | |
node.focus() | |
node.setSelectionRange(node.value.length, node.value.length) | |
}) | |
} | |
endEditing(id) { | |
this.setState({editing: null}) | |
if (!this.todoEditInput.value) | |
return | |
this.props.store.dispatch({ | |
type: 'EDIT_TODO', | |
text: this.todoEditInput.value, | |
id | |
}) | |
} | |
} | |
/** | |
* | |
* INIT APP | |
* | |
*/ | |
const todoApp = combineReducers({ todoList }) | |
const store = createStore(todoApp) | |
const render = () => { | |
ReactDOM.render(<TodoApp store={store}/>, document.getElementById('root')) | |
} | |
render() | |
store.subscribe(render) | |
/** | |
* | |
* HELPERS | |
* | |
*/ | |
function getTodoId(todos) { | |
return todos.size ? | |
Math.max(...todos.map(t => t.get('id')).toJS()) + 1 : | |
0 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="http://codepen.io/chriscoyier/pen/yIgqi.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.6/react.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.6/react-dom.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.0.6/redux.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/4.0.6/react-redux.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.7.6/immutable.min.js"></script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
* { | |
box-sizing: border-box; | |
} | |
body { | |
font-family: sans-serif; | |
font-size: 16px; | |
line-height: 24px; | |
background: #EEE; | |
minHeight: 100vh; | |
display: flex; | |
justify-content: center; | |
} | |
button { | |
display: inline-block; | |
border: 0; | |
background: #333; | |
margin: 0; | |
color: #FFF; | |
height: 36px; | |
padding: 0 12px; | |
text-transform: uppercase; | |
font-size: 16px; | |
line-height: 24px; | |
border-radius: 3px; | |
outline: none; | |
} | |
.material-icons { | |
background: transparent; | |
color: #000; | |
padding: 8px; | |
height: auto; | |
border-radius: 50%; | |
&:hover { | |
background: rgba(0,0,0,0.1); | |
} | |
} | |
.TodoApp { | |
width: 600px; | |
margin: 60px; | |
display: flex; | |
flex-direction: column; | |
} | |
.TodoApp__filters { | |
display: flex; | |
justify-content: center; | |
padding: 16px; | |
background: transparent; | |
} | |
.TodoApp__filter__btn { | |
background: transparent; | |
color: #999; | |
text-transform: none; | |
font-size: 14px; | |
padding: 0; | |
width: 90px; | |
height: auto; | |
border: 1px solid #999; | |
&:nth-child(1) { | |
border-radius: 3px 0 0 3px; | |
border-right: 0; | |
} | |
&:nth-child(2) { | |
border-radius: 0; | |
border-right: 0; | |
} | |
&:nth-child(3) { | |
border-radius: 0 3px 3px 0; | |
} | |
} | |
.TodoApp__filter__btn--active { | |
@extend .TodoApp__filter__btn; | |
background: #999; | |
color: #FFF; | |
} | |
.TodoApp__form { | |
display: flex; | |
margin-bottom: 0px; | |
z-index: 9; | |
position: relative; | |
} | |
.TodoApp__form__input { | |
border: 0; | |
// border-top: 3px solid #444; | |
flex: 1; | |
padding: 24px 16px; | |
display: block; | |
font-size: 24px; | |
outline: none; | |
background: #FFF; | |
box-shadow: 0px 2px 4px rgba(0,0,0,.1) | |
} | |
.TodoApp__form__add-btn { | |
height: auto; | |
padding: 0 24px; | |
margin-left: 8px; | |
} | |
.TodoApp__todo-list { | |
} | |
.TodoApp__todo { | |
background: #fdfdfd; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
padding: 16px 12px; | |
box-shadow: 0px 2px 4px rgba(0,0,0,0.15); | |
& + .TodoApp__todo { | |
border-top: 1px solid #EEE; | |
} | |
&.editing { | |
padding: 0; | |
} | |
&:hover { | |
.TodoApp__todo__delete-btn { | |
opacity: 1; | |
} | |
} | |
} | |
.TodoApp__todo--completed { | |
@extend .TodoApp__todo; | |
.TodoApp__todo__complete-btn { | |
color: green; | |
} | |
} | |
.TodoApp__todo__edit-input { | |
padding: 20px 24px 22px 24px; | |
font-size: 20px; | |
line-height: 24px; | |
margin: 0 0 0 0px; | |
width: 100%; | |
font-weight: 300; | |
background: #FFF9C4; | |
border: 0; | |
outline: none; | |
} | |
.TodoApp__todo__label { | |
font-size: 20px; | |
margin: 0 12px; | |
color: #333; | |
font-weight: 300; | |
.TodoApp__todo--completed & { | |
color: #CCC; | |
text-decoration: line-through; | |
} | |
} | |
.TodoApp__todo__primary { | |
display: flex; | |
flex: 1; | |
align-items: center; | |
} | |
.TodoApp__todo__complete-btn { | |
border: 1px solid #CCC; | |
padding: 4px; | |
color: transparent; | |
&:hover { | |
background: transparent; | |
} | |
.editing & { | |
display: none; | |
} | |
} | |
.TodoApp__todo__delete-btn { | |
transition: all .1s ease-out; | |
padding: 4px; | |
color: #777; | |
opacity: 0; | |
&:hover { | |
background: transparent; | |
color: red; | |
} | |
.editing & { | |
display: none | |
} | |
} | |
::-webkit-input-placeholder { | |
font-weight: 100; | |
color: #BBB; | |
} | |
::-moz-placeholder { /* Firefox 19+ */ | |
font-weight: 100; | |
color: #BBB; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<link href="//fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" /> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment