Skip to content

Instantly share code, notes, and snippets.

@darekrossman
Created January 27, 2016 18:52
Show Gist options
  • Save darekrossman/61eb1e01fd507d75851b to your computer and use it in GitHub Desktop.
Save darekrossman/61eb1e01fd507d75851b to your computer and use it in GitHub Desktop.
Redux Todo App
<div id="root"></div>
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
}
<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>
* {
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;
}
<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