├── .gitignore ├── assets ├── Source_maps.gif └── Redux_with_Monkberry.gif ├── constants ├── TodoFilters.js └── ActionTypes.js ├── components ├── TodoTextInput.monk ├── App.monk ├── Header.js ├── Header.monk ├── TodoItem.monk ├── MainSection.monk ├── Footer.js ├── Footer.monk ├── TodoTextInput.js ├── TodoItem.js └── MainSection.js ├── reducers ├── index.js └── todos.js ├── index.html ├── README.md ├── store └── configureStore.js ├── actions └── todos.js ├── server.js ├── index.js ├── webpack.config.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | -------------------------------------------------------------------------------- /assets/Source_maps.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monkberry/todomvc/HEAD/assets/Source_maps.gif -------------------------------------------------------------------------------- /assets/Redux_with_Monkberry.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monkberry/todomvc/HEAD/assets/Redux_with_Monkberry.gif -------------------------------------------------------------------------------- /constants/TodoFilters.js: -------------------------------------------------------------------------------- 1 | export const SHOW_ALL = 'show_all' 2 | export const SHOW_COMPLETED = 'show_completed' 3 | export const SHOW_ACTIVE = 'show_active' 4 | -------------------------------------------------------------------------------- /components/TodoTextInput.monk: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import todos from './todos' 3 | 4 | const rootReducer = combineReducers({ 5 | todos 6 | }) 7 | 8 | export default rootReducer 9 | -------------------------------------------------------------------------------- /components/App.monk: -------------------------------------------------------------------------------- 1 | {% import Header from './Header' %} 2 | {% import MainSection from './MainSection' %} 3 |
4 |
5 | 6 |
7 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Monkberry TodoMVC 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /components/Header.js: -------------------------------------------------------------------------------- 1 | import Header from './Header.monk'; 2 | 3 | export default class extends Header { 4 | handleSave(text) { 5 | if (text.length !== 0) { 6 | this.context.addTodo(text); 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /components/Header.monk: -------------------------------------------------------------------------------- 1 | {% import TodoTextInput from './TodoTextInput' %} 2 |
3 |

todos

4 | 5 |
-------------------------------------------------------------------------------- /constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export const ADD_TODO = 'ADD_TODO' 2 | export const DELETE_TODO = 'DELETE_TODO' 3 | export const EDIT_TODO = 'EDIT_TODO' 4 | export const COMPLETE_TODO = 'COMPLETE_TODO' 5 | export const COMPLETE_ALL = 'COMPLETE_ALL' 6 | export const CLEAR_COMPLETED = 'CLEAR_COMPLETED' 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TodoMVC — Redux with Monkberry 2 | 3 | Original example with Redux + React = 91.67 kB (minified & gzipped), **Redux + Monkberry = 10.77 kB** (minified & gzipped). 4 | 5 | ``` 6 | npm start 7 | ``` 8 | 9 | 10 | ## Hot module replacement 11 | 12 | ![Redux with Monkberry](assets/Redux_with_Monkberry.gif) 13 | 14 | ## Source maps 15 | ![Source maps with Monkberry](assets/Source_maps.gif) 16 | -------------------------------------------------------------------------------- /store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | import rootReducer from '../reducers'; 3 | 4 | export default function configureStore(initialState) { 5 | const store = createStore(rootReducer, initialState); 6 | 7 | if (module.hot) { 8 | // Enable Webpack hot module replacement for reducers 9 | module.hot.accept('../reducers', () => { 10 | const nextReducer = require('../reducers'); 11 | store.replaceReducer(nextReducer); 12 | }); 13 | } 14 | 15 | return store; 16 | } 17 | -------------------------------------------------------------------------------- /actions/todos.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes' 2 | 3 | export function addTodo(text) { 4 | return { type: types.ADD_TODO, text } 5 | } 6 | 7 | export function deleteTodo(id) { 8 | return { type: types.DELETE_TODO, id } 9 | } 10 | 11 | export function editTodo(id, text) { 12 | return { type: types.EDIT_TODO, id, text } 13 | } 14 | 15 | export function completeTodo(id) { 16 | return { type: types.COMPLETE_TODO, id } 17 | } 18 | 19 | export function completeAll() { 20 | return { type: types.COMPLETE_ALL } 21 | } 22 | 23 | export function clearCompleted() { 24 | return { type: types.CLEAR_COMPLETED } 25 | } 26 | -------------------------------------------------------------------------------- /components/TodoItem.monk: -------------------------------------------------------------------------------- 1 | {% import TodoTextInput from './TodoTextInput' %} 2 |
  • 3 | {% if editing %} 4 |
    5 | 8 |
    9 | {% else %} 10 |
    11 | 12 | 15 |
    17 | {% endif %} 18 |
  • -------------------------------------------------------------------------------- /components/MainSection.monk: -------------------------------------------------------------------------------- 1 | {% import TodoItem from './TodoItem' %} 2 | {% import Footer from './Footer' %} 3 |
    4 | {% if filteredTodos.length > 0 %} 5 | 6 | {% endif %} 7 | 12 | {% if todos.length > 0 %} 13 |
    -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var webpackDevMiddleware = require('webpack-dev-middleware'); 3 | var webpackHotMiddleware = require('webpack-hot-middleware'); 4 | var config = require('./webpack.config'); 5 | 6 | var app = new (require('express'))(); 7 | var port = 3000; 8 | 9 | var compiler = webpack(config); 10 | app.use(webpackDevMiddleware(compiler, {noInfo: true, publicPath: config.output.publicPath})); 11 | app.use(webpackHotMiddleware(compiler)); 12 | 13 | app.get("/", function (req, res) { 14 | res.sendFile(__dirname + '/index.html') 15 | }); 16 | 17 | app.listen(port, function (error) { 18 | if (error) { 19 | console.error(error) 20 | } else { 21 | console.info("Open up http://localhost:%s/ in your browser.", port) 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /components/Footer.js: -------------------------------------------------------------------------------- 1 | import Footer from './Footer.monk'; 2 | import { SHOW_ALL } from '../constants/TodoFilters' 3 | 4 | export default class extends Footer { 5 | constructor() { 6 | super(); 7 | this.state = { 8 | completedCount: 0, 9 | activeCount: 0, 10 | filter: SHOW_ALL, 11 | onShow: null 12 | }; 13 | this.on('click', '.clear-completed', this.onClearCompleted.bind(this)); 14 | this.on('click', '[data-filter]', this.onFilter.bind(this)); 15 | } 16 | 17 | update(state) { 18 | Object.assign(this.state, state); 19 | super.update(state); 20 | } 21 | 22 | onClearCompleted(event) { 23 | this.context.clearCompleted(); 24 | } 25 | 26 | onFilter(event) { 27 | this.state.onShow(event.target.dataset.filter); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /components/Footer.monk: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import Monkberry from 'monkberry'; 2 | import 'monkberry-events'; 3 | import App from './components/App.monk'; 4 | import * as TodoActions from './actions/todos' 5 | import { bindActionCreators } from 'redux' 6 | import configureStore from './store/configureStore' 7 | import 'todomvc-app-css/index.css' 8 | 9 | const store = configureStore(); 10 | const context = bindActionCreators(TodoActions, store.dispatch); 11 | 12 | let view = Monkberry.render(App, document.body, {context}); 13 | 14 | const listener = () => view.update(store.getState()); 15 | store.subscribe(listener); 16 | listener(); 17 | 18 | if (module.hot) { 19 | module.hot.accept('./components/App.monk', () => { 20 | view.remove(); 21 | const App = require('./components/App.monk'); 22 | view = Monkberry.render(App, document.body, {context}); 23 | view.update(store.getState()); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /components/TodoTextInput.js: -------------------------------------------------------------------------------- 1 | import TodoTextInput from './TodoTextInput.monk'; 2 | 3 | export default class extends TodoTextInput { 4 | constructor() { 5 | super(); 6 | this.state = { 7 | editing: false, 8 | noBlur: false, 9 | onSave: null 10 | }; 11 | this.on('keydown', 'input', this.onKeyDown.bind(this)); 12 | this.on('blur', 'input', this.onBlur.bind(this)); 13 | } 14 | 15 | update(state) { 16 | Object.assign(this.state, state); 17 | super.update(state); 18 | } 19 | 20 | onKeyDown(event) { 21 | if (event.which === 13) { 22 | const text = event.target.value.trim(); 23 | 24 | if (!this.state.editing) { 25 | super.update({text: ''}); 26 | } 27 | 28 | this.state.noBlur = true; 29 | this.state.onSave(text); 30 | this.state.noBlur = false; 31 | } 32 | } 33 | 34 | onBlur(event) { 35 | if (this.state.editing && !this.state.noBlur) { 36 | const text = event.target.value.trim(); 37 | this.state.onSave(text); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /components/TodoItem.js: -------------------------------------------------------------------------------- 1 | import TodoItem from './TodoItem.monk'; 2 | 3 | export default class extends TodoItem { 4 | constructor() { 5 | super(); 6 | this.state = { 7 | todo: null, 8 | editing: false 9 | }; 10 | 11 | this.on('click', '.toggle', this.onToggle.bind(this)); 12 | this.on('click', '.destroy', this.onDestroy.bind(this)); 13 | this.on('dblclick', 'label', this.onDblClick.bind(this)); 14 | } 15 | 16 | update(state) { 17 | Object.assign(this.state, state); 18 | super.update(this.state); 19 | } 20 | 21 | onToggle(event) { 22 | this.context.completeTodo(this.state.todo.id); 23 | } 24 | 25 | onDestroy(event) { 26 | this.context.deleteTodo(this.state.todo.id); 27 | } 28 | 29 | onDblClick(event) { 30 | this.update({editing: true}); 31 | this.querySelector('input').focus(); 32 | } 33 | 34 | handleSave(text) { 35 | if (text.length === 0) { 36 | this.context.deleteTodo(this.state.todo.id) 37 | } else { 38 | this.context.editTodo(this.state.todo.id, text) 39 | } 40 | 41 | this.update({editing: false}); 42 | } 43 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'cheap-module-eval-source-map', 6 | entry: [ 7 | 'webpack-hot-middleware/client', 8 | './index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/dist/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.HotModuleReplacementPlugin(), 18 | new webpack.NoErrorsPlugin() 19 | ], 20 | module: { 21 | loaders: [ 22 | { 23 | test: /\.js$/, 24 | loader: 'babel', 25 | exclude: /node_modules/, 26 | include: __dirname 27 | }, 28 | { 29 | test: /\.monk$/, 30 | loader: 'monkberry-loader?hot' 31 | }, 32 | { 33 | test: /\.css?$/, 34 | loaders: ['style', 'raw'], 35 | include: __dirname 36 | } 37 | ] 38 | } 39 | }; 40 | 41 | // You can safely delete next lines. 42 | // This is only for local development of monkberry-loader. 43 | var fs = require('fs'); 44 | var src = path.join(__dirname, '../monkberry'); 45 | if (fs.existsSync(src)) { 46 | module.exports.resolve = { 47 | alias: { 48 | monkberry: src 49 | } 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monkberry-todomvc", 3 | "version": "1.0.0", 4 | "description": "Redux with Monkberry TodoMVC", 5 | "scripts": { 6 | "start": "node server.js" 7 | }, 8 | "babel": { 9 | "presets": [ 10 | "es2015" 11 | ], 12 | "plugins": ["transform-object-rest-spread"] 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/monkberry/todomvc.git" 17 | }, 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/monkberry/todomvc/issues" 21 | }, 22 | "homepage": "https://github.com/monkberry/todomvc", 23 | "dependencies": { 24 | "monkberry": "^4.0.0", 25 | "monkberry-events": "^4.0.0", 26 | "redux": "^3.5.2" 27 | }, 28 | "devDependencies": { 29 | "babel-core": "^6.9.1", 30 | "babel-loader": "^6.2.4", 31 | "babel-plugin-transform-object-rest-spread": "^6.8.0", 32 | "babel-preset-es2015": "^6.9.0", 33 | "expect": "^1.20.1", 34 | "express": "^4.13.4", 35 | "monkberry-loader": "^4.0.0", 36 | "node-libs-browser": "^1.0.0", 37 | "raw-loader": "^0.5.1", 38 | "style-loader": "^0.13.1", 39 | "todomvc-app-css": "^2.0.6", 40 | "webpack": "^1.13.1", 41 | "webpack-dev-middleware": "^1.6.1", 42 | "webpack-hot-middleware": "^2.10.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /components/MainSection.js: -------------------------------------------------------------------------------- 1 | import MainSection from './MainSection.monk'; 2 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters'; 3 | 4 | const TODO_FILTERS = { 5 | [SHOW_ALL]: () => true, 6 | [SHOW_ACTIVE]: todo => !todo.completed, 7 | [SHOW_COMPLETED]: todo => todo.completed 8 | }; 9 | 10 | export default class extends MainSection { 11 | constructor() { 12 | super(); 13 | this.state = { 14 | filter: SHOW_ALL, 15 | completedCount: 0, 16 | activeCount: 0, 17 | filteredTodos: [], 18 | todos: [] 19 | }; 20 | 21 | this.on('click', '.toggle-all', this.onToggleAll.bind(this)); 22 | } 23 | 24 | update({filter, todos}) { 25 | if (filter) { 26 | this.state.filter = filter; 27 | } 28 | 29 | if (todos) { 30 | this.state.todos = todos; 31 | this.state.completedCount = todos.reduce((count, todo) => todo.completed ? count + 1 : count, 0); 32 | this.state.activeCount = todos.length - this.state.completedCount; 33 | this.state.filteredTodos = todos.filter(TODO_FILTERS[this.state.filter]); 34 | } 35 | 36 | super.update(this.state); 37 | } 38 | 39 | handleShow(filter) { 40 | this.update({...this.state, filter}); 41 | } 42 | 43 | onToggleAll(event) { 44 | this.context.completeAll(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /reducers/todos.js: -------------------------------------------------------------------------------- 1 | import { ADD_TODO, DELETE_TODO, EDIT_TODO, COMPLETE_TODO, COMPLETE_ALL, CLEAR_COMPLETED } from '../constants/ActionTypes' 2 | 3 | const initialState = [ 4 | { 5 | text: 'Use Monkberry', 6 | completed: false, 7 | id: 0 8 | }, 9 | { 10 | text: 'With Redux', 11 | completed: true, 12 | id: 1 13 | } 14 | ] 15 | 16 | export default function todos(state = initialState, action) { 17 | switch (action.type) { 18 | case ADD_TODO: 19 | return [ 20 | { 21 | id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1, 22 | completed: false, 23 | text: action.text 24 | }, 25 | ...state 26 | ] 27 | 28 | case DELETE_TODO: 29 | return state.filter(todo => 30 | todo.id !== action.id 31 | ) 32 | 33 | case EDIT_TODO: 34 | return state.map(todo => 35 | todo.id === action.id ? 36 | Object.assign({}, todo, { text: action.text }) : 37 | todo 38 | ) 39 | 40 | case COMPLETE_TODO: 41 | return state.map(todo => 42 | todo.id === action.id ? 43 | Object.assign({}, todo, { completed: !todo.completed }) : 44 | todo 45 | ) 46 | 47 | case COMPLETE_ALL: 48 | const areAllMarked = state.every(todo => todo.completed) 49 | return state.map(todo => Object.assign({}, todo, { 50 | completed: !areAllMarked 51 | })) 52 | 53 | case CLEAR_COMPLETED: 54 | return state.filter(todo => todo.completed === false) 55 | 56 | default: 57 | return state 58 | } 59 | } 60 | --------------------------------------------------------------------------------