├── .babelrc ├── .gitignore ├── actions └── TodoActions.js ├── components ├── Footer.js ├── Header.js ├── MainSection.js ├── TodoItem.js └── TodoTextInput.js ├── constants ├── ActionTypes.js └── TodoFilters.js ├── containers ├── App.js └── TodoApp.js ├── index.html ├── index.js ├── package.json ├── server.js ├── stores ├── index.js ├── todos.js └── user.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /actions/TodoActions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | 3 | export function addTodo(text) { 4 | return { 5 | type: types.ADD_TODO, 6 | text 7 | }; 8 | } 9 | 10 | export function deleteTodo(id) { 11 | return { 12 | type: types.DELETE_TODO, 13 | id 14 | }; 15 | } 16 | 17 | export function startEditingTodo(id) { 18 | return { 19 | type: types.START_EDITING_TODO, 20 | id 21 | }; 22 | } 23 | 24 | export function editTodo(id, text) { 25 | return { 26 | type: types.EDIT_TODO, 27 | id, 28 | text 29 | }; 30 | } 31 | 32 | export function markTodo(id) { 33 | return { 34 | type: types.MARK_TODO, 35 | id 36 | }; 37 | } 38 | 39 | export function markAll() { 40 | return { 41 | type: types.MARK_ALL 42 | }; 43 | } 44 | 45 | export function clearMarked() { 46 | return { 47 | type: types.CLEAR_MARKED 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /components/Footer.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import classnames from 'classnames'; 3 | import { SHOW_ALL, SHOW_MARKED, SHOW_UNMARKED } from '../constants/TodoFilters'; 4 | 5 | const FILTER_TITLES = { 6 | [SHOW_ALL]: 'All', 7 | [SHOW_UNMARKED]: 'Active', 8 | [SHOW_MARKED]: 'Completed' 9 | }; 10 | 11 | export default class Footer extends Component { 12 | static propTypes = { 13 | markedCount: PropTypes.number.isRequired, 14 | unmarkedCount: PropTypes.number.isRequired, 15 | filter: PropTypes.string.isRequired, 16 | onClearMarked: PropTypes.func.isRequired, 17 | onShow: PropTypes.func.isRequired 18 | } 19 | 20 | render() { 21 | return ( 22 | 33 | ); 34 | } 35 | 36 | renderTodoCount() { 37 | const { unmarkedCount } = this.props; 38 | const itemWord = unmarkedCount === 1 ? 'item' : 'items'; 39 | 40 | return ( 41 | 42 | {unmarkedCount || 'No'} {itemWord} left 43 | 44 | ); 45 | } 46 | 47 | renderFilterLink(filter) { 48 | const title = FILTER_TITLES[filter]; 49 | const { filter: selectedFilter, onShow } = this.props; 50 | 51 | return ( 52 | onShow(filter)}> 55 | {title} 56 | 57 | ); 58 | } 59 | 60 | renderClearButton() { 61 | const { markedCount, onClearMarked } = this.props; 62 | if (markedCount > 0) { 63 | return ( 64 | 68 | ); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /components/Header.js: -------------------------------------------------------------------------------- 1 | import {elementOpen, elementClose, text} from 'incremental-dom'; 2 | import TodoTextInput from './TodoTextInput'; 3 | 4 | export default function({addTodo}) { 5 | elementOpen('header', null, null, 'class', 'header'); 6 | elementOpen('h1'); 7 | text('todos'); 8 | elementClose('h1'); 9 | TodoTextInput({newTodo: true}); 10 | elementClose('header'); 11 | }; 12 | -------------------------------------------------------------------------------- /components/MainSection.js: -------------------------------------------------------------------------------- 1 | import {elementOpen, elementClose} from 'incremental-dom'; 2 | 3 | import TodoItem from './TodoItem'; 4 | // import Footer from './Footer'; 5 | import { SHOW_ALL, SHOW_MARKED, SHOW_UNMARKED } from '../constants/TodoFilters'; 6 | 7 | const TODO_FILTERS = { 8 | [SHOW_ALL]: () => true, 9 | [SHOW_UNMARKED]: todo => !todo.marked, 10 | [SHOW_MARKED]: todo => todo.marked 11 | }; 12 | 13 | export default function({todos, user, actions}) { 14 | elementOpen('section', null, ['class', 'main']); 15 | elementOpen('ul', null, ['class', 'todo-list']); 16 | todos.map(todo => { 17 | TodoItem({todo, editing: user.editing === todo.id, ...actions}); 18 | }); 19 | elementClose('ul'); 20 | elementClose('section'); 21 | }; 22 | -------------------------------------------------------------------------------- /components/TodoItem.js: -------------------------------------------------------------------------------- 1 | import {elementOpen, elementClose, elementVoid, text, patch} from 'incremental-dom'; 2 | import classnames from 'classnames'; 3 | import TodoTextInput from './TodoTextInput'; 4 | 5 | export default function TodoItem(props) { 6 | const {todo, editing} = props; 7 | 8 | elementOpen('li', null, null, 'class', classnames({ 9 | completed: todo.marked, 10 | editing 11 | })); 12 | editing ? EditView(props) : ReadView(props); 13 | elementClose('li'); 14 | }; 15 | 16 | function ReadView({todo, startEditingTodo}) { 17 | function onDoubleClick() { 18 | startEditingTodo(todo.id); 19 | 20 | // TODO: Better way of access the real DOM element of a component? 21 | // Here we want to focus the input field and set the curor to the end 22 | const input = document.querySelector('.edit'); 23 | input.focus(); 24 | input.setSelectionRange(input.value.length, input.value.length); 25 | } 26 | 27 | elementOpen('div', null, ['class', 'view']); 28 | elementVoid('input', null, ['class', 'toggle', 'type', 'checkbox']); 29 | elementOpen('label', null, null, 'ondblclick', onDoubleClick); 30 | text(todo.text); 31 | elementClose('label'); 32 | elementVoid('button', null, ['class', 'destroy']); 33 | elementClose('div'); 34 | } 35 | 36 | function EditView({todo, editTodo, deleteTodo}) { 37 | function onSave(text) { 38 | if (text.length === 0) { 39 | deleteTodo(todo.id); 40 | } else { 41 | editTodo(todo.id, text); 42 | } 43 | } 44 | 45 | TodoTextInput({text: todo.text, editing: true, onSave}); 46 | } 47 | -------------------------------------------------------------------------------- /components/TodoTextInput.js: -------------------------------------------------------------------------------- 1 | import {elementOpen, elementClose} from 'incremental-dom'; 2 | import classnames from 'classnames'; 3 | 4 | export default function({editing, newTodo, text, onSave}) { 5 | function handleBlur(e) { 6 | if (!newTodo) { 7 | onSave(e.target.value); 8 | } 9 | } 10 | 11 | elementOpen( 12 | 'input', null, 13 | ['type', 'text'], 14 | 'class', classnames({ 15 | edit: editing, 16 | 'new-todo': newTodo 17 | }), 18 | 'value', text, 19 | 'onblur', handleBlur 20 | ); 21 | elementClose('input'); 22 | }; 23 | -------------------------------------------------------------------------------- /constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export const ADD_TODO = 'ADD_TODO'; 2 | export const DELETE_TODO = 'DELETE_TODO'; 3 | export const START_EDITING_TODO = 'USER_EDIT_TODO'; 4 | export const EDIT_TODO = 'EDIT_TODO'; 5 | export const MARK_TODO = 'MARK_TODO'; 6 | export const MARK_ALL = 'MARK_ALL'; 7 | export const CLEAR_MARKED = 'CLEAR_MARKED'; 8 | -------------------------------------------------------------------------------- /constants/TodoFilters.js: -------------------------------------------------------------------------------- 1 | export const SHOW_ALL = 'show_all'; 2 | export const SHOW_MARKED = 'show_marked'; 3 | export const SHOW_UNMARKED = 'show_unmarked'; 4 | -------------------------------------------------------------------------------- /containers/App.js: -------------------------------------------------------------------------------- 1 | import {elementOpen, elementClose, patch} from 'incremental-dom'; 2 | import { createRedux } from 'redux'; 3 | 4 | import TodoApp from './TodoApp'; 5 | import * as stores from '../stores'; 6 | 7 | const redux = window.redux = createRedux(stores); 8 | 9 | export default function App(root) { 10 | const applyPatch = () => patch(root, render); 11 | applyPatch(); 12 | redux.subscribe(applyPatch); 13 | }; 14 | 15 | function render() { 16 | const {todos, user} = redux.getState(); 17 | TodoApp({ 18 | todos, 19 | user, 20 | dispatch: redux.dispatch 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /containers/TodoApp.js: -------------------------------------------------------------------------------- 1 | import {elementOpen, elementClose} from 'incremental-dom'; 2 | import { bindActionCreators } from 'redux'; 3 | 4 | import Header from '../components/Header'; 5 | import MainSection from '../components/MainSection'; 6 | import * as TodoActions from '../actions/TodoActions'; 7 | 8 | export default function({todos, user, dispatch}) { 9 | const actions = bindActionCreators(TodoActions, dispatch); 10 | elementOpen('div'); 11 | Header({addTodo: actions.addTodo}); 12 | MainSection({todos, user, actions}); 13 | elementClose('div'); 14 | }; 15 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |