├── .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 | Redux TodoMVC 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import 'todomvc-app-css/index.css'; 2 | import App from './containers/App'; 3 | 4 | App(document.getElementById('root')); 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "idom-redux-todomvc-app", 3 | "version": "0.0.0", 4 | "description": "TodoMVC example for incremental-dom & redux", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/joshthecoder/idom-redux-todomvc-app.git" 12 | }, 13 | "keywords": [ 14 | "incremental-dom", 15 | "redux", 16 | "webpack", 17 | "flux", 18 | "todomvc" 19 | ], 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/joshthecoder/idom-redux-todomvc-app/issues" 23 | }, 24 | "dependencies": { 25 | "classnames": "^2.1.2", 26 | "incremental-dom": "^0.1.0", 27 | "redux": "^0.12.0" 28 | }, 29 | "devDependencies": { 30 | "babel-core": "^5.5.8", 31 | "babel-loader": "^5.1.4", 32 | "node-libs-browser": "^0.5.2", 33 | "raw-loader": "^0.5.1", 34 | "style-loader": "^0.12.3", 35 | "todomvc-app-css": "^2.0.1", 36 | "webpack": "^1.9.11", 37 | "webpack-dev-server": "^1.9.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var WebpackDevServer = require('webpack-dev-server'); 3 | var config = require('./webpack.config'); 4 | 5 | new WebpackDevServer(webpack(config), { 6 | publicPath: config.output.publicPath, 7 | // hot: true, 8 | historyApiFallback: true, 9 | stats: { 10 | colors: true 11 | } 12 | }).listen(3000, 'localhost', function (err) { 13 | if (err) { 14 | console.log(err); 15 | } 16 | 17 | console.log('Listening at localhost:3000'); 18 | }); 19 | -------------------------------------------------------------------------------- /stores/index.js: -------------------------------------------------------------------------------- 1 | export { default as todos } from './todos'; 2 | export { default as user } from './user'; 3 | -------------------------------------------------------------------------------- /stores/todos.js: -------------------------------------------------------------------------------- 1 | import { ADD_TODO, DELETE_TODO, EDIT_TODO, MARK_TODO, MARK_ALL, CLEAR_MARKED } from '../constants/ActionTypes'; 2 | 3 | const initialState = [{ 4 | text: 'Use Redux', 5 | marked: false, 6 | id: 0 7 | }, 8 | { 9 | text: 'Use Incremental DOM', 10 | marked: false, 11 | id: 1 12 | }]; 13 | 14 | export default function todos(state = initialState, action) { 15 | switch (action.type) { 16 | case ADD_TODO: 17 | return [{ 18 | id: (state.length === 0) ? 0 : state[0].id + 1, 19 | marked: false, 20 | text: action.text 21 | }, ...state]; 22 | 23 | case DELETE_TODO: 24 | return state.filter(todo => 25 | todo.id !== action.id 26 | ); 27 | 28 | case EDIT_TODO: 29 | return state.map(todo => 30 | todo.id === action.id ? 31 | { ...todo, text: action.text } : 32 | todo 33 | ); 34 | 35 | case MARK_TODO: 36 | return state.map(todo => 37 | todo.id === action.id ? 38 | { ...todo, marked: !todo.marked } : 39 | todo 40 | ); 41 | 42 | case MARK_ALL: 43 | const areAllMarked = state.every(todo => todo.marked); 44 | return state.map(todo => ({ 45 | ...todo, 46 | marked: !areAllMarked 47 | })); 48 | 49 | case CLEAR_MARKED: 50 | return state.filter(todo => todo.marked === false); 51 | 52 | default: 53 | return state; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /stores/user.js: -------------------------------------------------------------------------------- 1 | import {START_EDITING_TODO, EDIT_TODO} from '../constants/ActionTypes'; 2 | 3 | export default function user(state = {editing: null}, action) { 4 | switch (action.type) { 5 | case START_EDITING_TODO: 6 | return {editing: action.id}; 7 | 8 | case EDIT_TODO: 9 | return {editing: null}; 10 | 11 | default: 12 | return state; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | entry: [ 7 | 'webpack-dev-server/client?http://localhost:3000', 8 | 'webpack/hot/only-dev-server', 9 | './index' 10 | ], 11 | output: { 12 | path: path.join(__dirname, 'dist'), 13 | filename: 'bundle.js', 14 | publicPath: '/static/' 15 | }, 16 | plugins: [ 17 | new webpack.HotModuleReplacementPlugin(), 18 | new webpack.NoErrorsPlugin() 19 | ], 20 | resolve: { 21 | extensions: ['', '.js'] 22 | }, 23 | module: { 24 | loaders: [{ 25 | test: /\.js$/, 26 | loaders: ['babel'], 27 | exclude: /node_modules/ 28 | }, { 29 | test: /\.css?$/, 30 | loaders: ['style', 'raw'] 31 | }] 32 | } 33 | }; 34 | --------------------------------------------------------------------------------