├── .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 |
--------------------------------------------------------------------------------
/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 | 
13 |
14 | ## Source maps
15 | 
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 |
16 |
17 | {% endif %}
18 |
--------------------------------------------------------------------------------
/components/MainSection.monk:
--------------------------------------------------------------------------------
1 | {% import TodoItem from './TodoItem' %}
2 | {% import Footer from './Footer' %}
3 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------