├── .editorconfig ├── .gitignore ├── .jshintrc ├── LICENSE ├── app.js ├── build ├── development ├── prepare └── views ├── client ├── css │ └── styles.css └── js │ ├── controllers │ └── todos │ │ ├── index.js │ │ └── item.js │ ├── conventions │ └── realtime.js │ └── main.js ├── controllers ├── forms │ ├── redirect.js │ ├── socket-emit.js │ └── todos │ │ ├── clear-completed.js │ │ ├── complete.js │ │ ├── create.js │ │ ├── edit.js │ │ ├── mark-all-completed.js │ │ └── remove.js ├── routes.js └── todos │ └── index.js ├── package.json ├── readme.markdown ├── realtime.js ├── routes.js ├── services └── todos.js ├── shared └── filter-todos.js └── views ├── layout.html └── todos ├── footer.html ├── index.html └── list.html /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Taunus 2 | .bin 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 30 | node_modules 31 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "newcap": true, 5 | "noarg": true, 6 | "noempty": true, 7 | "nonew": true, 8 | "sub": true, 9 | "validthis": true, 10 | "undef": true, 11 | "trailing": true, 12 | "boss": true, 13 | "eqnull": true, 14 | "strict": true, 15 | "immed": true, 16 | "expr": true, 17 | "latedef": "nofunc", 18 | "quotmark": "single", 19 | "indent": 2, 20 | "node": true, 21 | "browser": true 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 JC Ivancevich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var PORT = process.env.PORT || 3000; 4 | var http = require('http'); 5 | var taunus = require('taunus'); 6 | var taunusExpress = require('taunus-express'); 7 | var express = require('express'); 8 | var bodyParser = require('body-parser'); 9 | var serveStatic = require('serve-static'); 10 | var realtime = require('./realtime'); 11 | var routes = require('./routes'); 12 | var app = express(); 13 | var server = http.Server(app); 14 | var options = { 15 | routes: require('./controllers/routes'), 16 | layout: require('./.bin/views/layout') 17 | }; 18 | 19 | app.use(serveStatic('.bin/public')); 20 | app.use(bodyParser.urlencoded({ extended: false })); 21 | app.use(bodyParser.json()); 22 | 23 | realtime(server); 24 | routes(app); 25 | 26 | taunusExpress(taunus, app, options); 27 | 28 | server.listen(PORT, listening); 29 | 30 | function listening () { 31 | console.log('App running on http://localhost:%d', PORT); 32 | } 33 | -------------------------------------------------------------------------------- /build/development: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | build/prepare && ( 3 | nodemon app.js --watch app.js --watch api.js --watch controllers --watch services --watch .bin/views --ignore client & 4 | watchify client/js/main.js -o .bin/public/js/all.js -p errorify --debug --verbose & 5 | chokidar 'views/**/*.html' -c 'build/views' 6 | ) 7 | -------------------------------------------------------------------------------- /build/prepare: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm -rf .bin 3 | build/views 4 | mkdir -p .bin/public/js 5 | taunus -o 6 | browserify client/js/main.js -do .bin/public/js/all.js 7 | mkdir -p .bin/public/css 8 | cp node_modules/todomvc-common/base.css .bin/public/css/base.css 9 | cp node_modules/todomvc-app-css/index.css .bin/public/css/index.css 10 | cp client/css/styles.css .bin/public/css/styles.css 11 | -------------------------------------------------------------------------------- /build/views: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | hoganator --outputdir .bin views/*.html views/**/*.html 3 | -------------------------------------------------------------------------------- /client/css/styles.css: -------------------------------------------------------------------------------- 1 | .todo-list li .toggle.checked:after { 2 | content: url('data:image/svg+xml;utf8,'); 3 | } 4 | -------------------------------------------------------------------------------- /client/js/controllers/todos/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var $ = require('dominus'); 4 | var taunus = require('taunus'); 5 | var skyrocket = require('skyrocket'); 6 | var filterTodos = require('../../../../shared/filter-todos'); 7 | 8 | module.exports = function (viewModel, container, route) { 9 | handleRealtime(container, viewModel, route); 10 | }; 11 | 12 | function handleRealtime (container, viewModel, route) { 13 | var rocket = skyrocket.scope(container, viewModel); 14 | rocket.on('/todos', reaction); 15 | 16 | function reaction (update) { 17 | var list = $.findOne('.todo-list'); 18 | var footer = $.findOne('.footer'); 19 | 20 | filterTodos(viewModel, route.pathname.slice(1)); 21 | 22 | taunus.partial(list, 'todos/list', viewModel); 23 | taunus.partial(footer, 'todos/footer', viewModel); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /client/js/controllers/todos/item.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var $ = require('dominus'); 4 | 5 | $.custom('esc', 'keyup', function (e) { 6 | return e.keyCode === 27; 7 | }); 8 | 9 | module.exports = function (viewModel, container, route) { 10 | var todo = $(container).on('dblclick', onDoubleClick); 11 | $('html').on('click', cancelEditing); 12 | 13 | function onDoubleClick (event) { 14 | event.stopPropagation(); 15 | todo.off('dblclick').addClass('editing'); 16 | var input = $.findOne('input.edit', todo); 17 | $(input).focus().on('esc', cancelEditing).on('click', onClick); 18 | 19 | function onClick (event) { 20 | event.stopPropagation(); 21 | } 22 | } 23 | 24 | function cancelEditing () { 25 | todo.removeClass('editing'); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /client/js/conventions/realtime.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var client = require('socket.io-client'); 4 | var skyrocket = require('skyrocket'); 5 | var taunus = require('taunus'); 6 | var io = client(''); 7 | 8 | io.on('connect', connect); 9 | io.on('disconnect', disconnect); 10 | 11 | function connect () { 12 | setup.id = io.io.engine.id; 13 | } 14 | 15 | function disconnect () { 16 | setup.id = null; 17 | } 18 | 19 | function setup () { 20 | skyrocket.configure({ 21 | taunus: taunus, 22 | revolve: revolve 23 | }); 24 | 25 | skyrocket.op('add', function (target, operation) { 26 | return target + operation.value; 27 | }); 28 | 29 | skyrocket.op('mark-completed', function (target, operation) { 30 | var found = target.some(function (todo, index) { 31 | if (todo.id === operation.model.id) { 32 | target[index] = operation.model; 33 | return true; 34 | } 35 | }); 36 | 37 | if (!found) { 38 | var currentPath = window.location.pathname.slice(1); 39 | var insertTodo = (currentPath === 'active' && operation.model.completed === false) || 40 | (currentPath === 'completed' && operation.model.completed === true); 41 | if (insertTodo) { 42 | target.push(operation.model); 43 | } 44 | } 45 | 46 | return target; 47 | }); 48 | 49 | io.on('/skyrocket/update', skyrocket.react); 50 | 51 | function revolve (type, rooms) { 52 | io.emit('/skyrocket/' + type, { rooms: rooms }); 53 | } 54 | } 55 | 56 | module.exports = setup; 57 | -------------------------------------------------------------------------------- /client/js/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var $ = require('dominus'); 4 | var taunus = require('taunus'); 5 | var actions = require('taunus-actions'); 6 | var wiring = require('../../.bin/wiring'); 7 | var realtime = require('./conventions/realtime'); 8 | var main = $.findOne('main'); 9 | 10 | actions.configure({ 11 | taunus: taunus 12 | }); 13 | 14 | realtime(); 15 | 16 | function generateQueryString () { 17 | return { 18 | current_path: window.location.pathname, 19 | sid: realtime.id ? realtime.id : null 20 | }; 21 | } 22 | 23 | taunus.mount(main, wiring, { 24 | qs: generateQueryString 25 | }); 26 | -------------------------------------------------------------------------------- /controllers/forms/redirect.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var url = require('url'); 4 | var taunus = require('taunus'); 5 | 6 | function redirect (req, res) { 7 | var redirectTo = req.headers.referer ? url.parse(req.headers.referer).path : req.query.current_path; 8 | taunus.redirect(req, res, redirectTo, { 9 | force: true 10 | }); 11 | } 12 | 13 | module.exports = redirect; 14 | -------------------------------------------------------------------------------- /controllers/forms/socket-emit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var socket = require('../../realtime'); 4 | 5 | function emit (req, room, options) { 6 | socket.io.to(room).except(req.query.sid).emit('/skyrocket/update', options); 7 | } 8 | 9 | module.exports = emit; 10 | -------------------------------------------------------------------------------- /controllers/forms/todos/clear-completed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var todosService = require('../../../services/todos'); 4 | var redirect = require('../redirect'); 5 | var emit = require('../socket-emit'); 6 | 7 | function clearCompletedTodos (req, res, next) { 8 | todosService.clearCompleted(handler); 9 | 10 | function handler(err, todos) { 11 | if (err) { 12 | return next(err); 13 | } 14 | 15 | res.viewModel = { 16 | model: todos 17 | }; 18 | 19 | redirect(req, res); 20 | 21 | var room = '/todos'; 22 | emit(req, room, { 23 | updates: [{ 24 | rooms: [room], 25 | model: { 26 | todos: todos, 27 | completedTodosCount: 0 28 | } 29 | }] 30 | }); 31 | } 32 | } 33 | 34 | module.exports = clearCompletedTodos; 35 | -------------------------------------------------------------------------------- /controllers/forms/todos/complete.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var todosService = require('../../../services/todos'); 4 | var redirect = require('../redirect'); 5 | var emit = require('../socket-emit'); 6 | 7 | function completeTodo (req, res, next) { 8 | var completed = req.body.completed === 'true'; 9 | var todo = { 10 | id: req.params.id, 11 | completed: !completed 12 | }; 13 | todosService.complete(todo, handler); 14 | 15 | function handler(err, todo) { 16 | if (err) { 17 | return next(err); 18 | } 19 | 20 | res.viewModel = { 21 | model: todo 22 | }; 23 | 24 | redirect(req, res); 25 | 26 | var room = '/todos'; 27 | emit(req, room, { 28 | updates: [{ 29 | rooms: [room], 30 | operations: [{ 31 | op: 'mark-completed', 32 | model: todo, 33 | concern: 'todos', 34 | query: { id: todo.id } 35 | }, { 36 | op: 'add', 37 | value: todo.completed ? -1 : 1, 38 | concern: 'activeTodosCount' 39 | }, { 40 | op: 'add', 41 | value: todo.completed ? 1 : -1, 42 | concern: 'completedTodosCount' 43 | }] 44 | }] 45 | }); 46 | } 47 | } 48 | 49 | module.exports = completeTodo; 50 | -------------------------------------------------------------------------------- /controllers/forms/todos/create.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var todosService = require('../../../services/todos'); 4 | var redirect = require('../redirect'); 5 | var emit = require('../socket-emit'); 6 | 7 | function createTodo (req, res, next) { 8 | todosService.add(req.body, handler); 9 | 10 | function handler(err, todo) { 11 | if (err) { 12 | return next(err); 13 | } 14 | 15 | res.viewModel = { 16 | model: todo 17 | }; 18 | 19 | redirect(req, res); 20 | 21 | var room = '/todos'; 22 | emit(req, room, { 23 | updates: [{ 24 | rooms: [room], 25 | operations: [{ 26 | op: 'push', 27 | model: todo, 28 | concern: 'todos' 29 | }, { 30 | op: 'add', 31 | value: 1, 32 | concern: 'todosCount' 33 | }, { 34 | op: 'add', 35 | value: 1, 36 | concern: 'activeTodosCount' 37 | }] 38 | }] 39 | }); 40 | } 41 | } 42 | 43 | module.exports = createTodo; 44 | -------------------------------------------------------------------------------- /controllers/forms/todos/edit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var todosService = require('../../../services/todos'); 4 | var redirect = require('../redirect'); 5 | var emit = require('../socket-emit'); 6 | 7 | function editTodo (req, res, next) { 8 | var todo = { 9 | id: req.params.id, 10 | title: req.body.title 11 | }; 12 | todosService.edit(todo, handler); 13 | 14 | function handler(err, todo) { 15 | if (err) { 16 | return next(err); 17 | } 18 | 19 | res.viewModel = { 20 | model: todo 21 | }; 22 | 23 | redirect(req, res); 24 | 25 | var room = '/todos'; 26 | emit(req, room, { 27 | updates: [{ 28 | rooms: [room], 29 | operations: [{ 30 | op: 'edit', 31 | model: todo, 32 | concern: 'todos', 33 | query: { id: todo.id } 34 | }] 35 | }] 36 | }); 37 | } 38 | } 39 | 40 | module.exports = editTodo; 41 | -------------------------------------------------------------------------------- /controllers/forms/todos/mark-all-completed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var todosService = require('../../../services/todos'); 4 | var redirect = require('../redirect'); 5 | var emit = require('../socket-emit'); 6 | 7 | function markAllTodosCompleted (req, res, next) { 8 | todosService.markAllCompleted(handler); 9 | 10 | function handler(err, todos) { 11 | if (err) { 12 | return next(err); 13 | } 14 | 15 | res.viewModel = { 16 | model: todos 17 | }; 18 | 19 | redirect(req, res); 20 | 21 | var room = '/todos'; 22 | emit(req, room, { 23 | updates: [{ 24 | rooms: [room], 25 | model: { 26 | todos: todos, 27 | activeTodosCount: 0, 28 | completedTodosCount: todos.length 29 | } 30 | }] 31 | }); 32 | } 33 | } 34 | 35 | module.exports = markAllTodosCompleted; 36 | -------------------------------------------------------------------------------- /controllers/forms/todos/remove.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var todosService = require('../../../services/todos'); 4 | var redirect = require('../redirect'); 5 | var emit = require('../socket-emit'); 6 | 7 | function removeTodo (req, res, next) { 8 | todosService.remove(req.params.id, handler); 9 | 10 | function handler(err, todo) { 11 | if (err) { 12 | return next(err); 13 | } 14 | 15 | res.viewModel = { 16 | model: todo 17 | }; 18 | 19 | redirect(req, res); 20 | 21 | var room = '/todos'; 22 | emit(req, room, { 23 | updates: [{ 24 | rooms: [room], 25 | operations: [{ 26 | op: 'remove', 27 | concern: 'todos', 28 | query: { id: todo.id } 29 | }, { 30 | op: 'add', 31 | value: -1, 32 | concern: 'todosCount' 33 | }, { 34 | op: 'add', 35 | value: -1, 36 | concern: todo.completed ? 'completedTodosCount' : 'activeTodosCount' 37 | }] 38 | }] 39 | }); 40 | } 41 | } 42 | 43 | module.exports = removeTodo; 44 | -------------------------------------------------------------------------------- /controllers/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = [ 4 | { route: '/', action: 'todos/index' }, 5 | { route: '/active', action: 'todos/index' }, 6 | { route: '/completed', action: 'todos/index' } 7 | ]; 8 | -------------------------------------------------------------------------------- /controllers/todos/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var todosService = require('../../services/todos'); 3 | var filterTodos = require('../../shared/filter-todos'); 4 | 5 | module.exports = function (req, res, next) { 6 | var currentPath = req.route.path.slice(1); 7 | todosService.getAll(getAllHandler); 8 | 9 | function getAllHandler (err, todos) { 10 | if (err) { 11 | return next(err); 12 | } 13 | 14 | var viewModel = { 15 | model: { 16 | all: currentPath === '', 17 | active: currentPath === 'active', 18 | completed: currentPath === 'completed', 19 | todosCount: 0, 20 | activeTodosCount: 0, 21 | completedTodosCount: 0, 22 | todos: todos 23 | } 24 | }; 25 | 26 | filterTodos(viewModel.model, currentPath, { 27 | counts: true 28 | }); 29 | 30 | res.viewModel = viewModel; 31 | 32 | next(); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "taunus-todomvc", 3 | "version": "1.0.0", 4 | "description": "Taunus TodoMVC", 5 | "engines": { 6 | "node": "iojs-v2.3.3" 7 | }, 8 | "main": "app.js", 9 | "scripts": { 10 | "start": "build/prepare && node app.js", 11 | "dev": "build/development", 12 | "test": "echo \"Error: no test specified\" && exit 1", 13 | "preinstall": "npm install node-gyp-install && ./node_modules/node-gyp-install/bin.js" 14 | }, 15 | "author": "JC Ivancevich ", 16 | "license": "MIT", 17 | "dependencies": { 18 | "body-parser": "^1.13.2", 19 | "browserify": "^10.2.4", 20 | "dominus": "^5.0.1", 21 | "express": "^4.13.1", 22 | "hoganator": "^1.1.1", 23 | "serve-static": "^1.10.0", 24 | "skyrocket": "^1.2.1", 25 | "socket.io": "^1.3.5", 26 | "socket.io-client": "^1.3.5", 27 | "superagent": "^1.2.0", 28 | "taunus": "^7.1.0", 29 | "taunus-actions": "^1.0.0", 30 | "taunus-express": "^3.0.0", 31 | "todomvc-app-css": "^2.0.1", 32 | "todomvc-common": "^1.0.2" 33 | }, 34 | "devDependencies": { 35 | "chokidar-cli": "^0.3.0", 36 | "errorify": "^0.2.4", 37 | "nodemon": "^1.3.7", 38 | "watchify": "^3.2.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | # Taunus TodoMVC 2 | 3 | > Micro Isomorphic MVC Engine for Node.js 4 | 5 | ## Install dependencies 6 | `npm install` 7 | 8 | ## Run in production mode 9 | `npm start` 10 | 11 | ## Run in development mode 12 | `npm run dev` 13 | -------------------------------------------------------------------------------- /realtime.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var socket = require('socket.io'); 4 | var io; 5 | 6 | function realtime (server) { 7 | if (io) { return io; } 8 | io = realtime.io = addExceptMethod(socket(server)); 9 | io.on('connection', connected); 10 | return io; 11 | 12 | function connected (socket) { 13 | socket.on('error', console.error.bind(console)); 14 | socket.on('/skyrocket/join', join); 15 | socket.on('/skyrocket/leave', leave); 16 | 17 | function join (data) { 18 | data.rooms.forEach(socket.join, socket); 19 | } 20 | function leave (data) { 21 | data.rooms.forEach(socket.leave, socket); 22 | } 23 | } 24 | } 25 | 26 | function addExceptMethod (io) { 27 | var sockets = io.sockets; 28 | var _broadcast = sockets.adapter.broadcast; 29 | 30 | sockets.constructor.prototype.except = except; 31 | sockets.adapter.broadcast = broadcast; 32 | return io; 33 | 34 | function except (id) { 35 | this.excepts = this.excepts || []; 36 | this.excepts.push(id); 37 | return this; 38 | } 39 | function broadcast (packet, options) { 40 | if (sockets.excepts) { 41 | options.except = sockets.excepts; 42 | } 43 | _broadcast.apply(this, arguments); 44 | delete sockets.excepts; 45 | } 46 | } 47 | 48 | module.exports = realtime; 49 | -------------------------------------------------------------------------------- /routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var url = require('url'); 3 | var taunus = require('taunus'); 4 | var todosService = require('./services/todos'); 5 | 6 | function setup(app) { 7 | var socket = require('./realtime'); 8 | app.post('/api/todo', require('./controllers/forms/todos/create')); 9 | app.post('/api/todo/:id/edit', require('./controllers/forms/todos/edit')); 10 | app.post('/api/todo/:id/remove', require('./controllers/forms/todos/remove')); 11 | app.post('/api/todo/:id/complete', require('./controllers/forms/todos/complete')); 12 | app.post('/api/todo/clear-completed', require('./controllers/forms/todos/clear-completed')); 13 | app.post('/api/todo/mark-all-completed', require('./controllers/forms/todos/mark-all-completed')); 14 | } 15 | 16 | module.exports = setup; 17 | -------------------------------------------------------------------------------- /services/todos.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _todos = { 3 | '1': { 4 | id: 1, 5 | title: 'Taste JavaScript', 6 | completed: true 7 | }, 8 | '2': { 9 | id: 2, 10 | title: 'Buy a Unicorn', 11 | completed: false 12 | } 13 | }; 14 | 15 | function getAll (callback) { 16 | callback(null, toArray(_todos)); 17 | } 18 | 19 | function getOne (id, callback) { 20 | if (!_todos[id]) { return callback(new Error('not_found')); } 21 | callback(null, _todos[id]); 22 | } 23 | 24 | function add (todo, callback) { 25 | todo.id = _todos.length === 0 ? 1 : Object.keys(_todos).reduce(function(a, b) { return Math.max(a, b); }, 0) + 1; 26 | todo.completed = !!todo.completed; 27 | _todos['' + todo.id] = todo; 28 | callback(null, todo); 29 | } 30 | 31 | function edit (todo, callback) { 32 | if (!_todos[todo.id]) { return callback(new Error('not_found')); } 33 | _todos[todo.id].title = todo.title; 34 | callback(null, _todos[todo.id]); 35 | } 36 | 37 | function complete (todo, callback) { 38 | if (!_todos[todo.id]) { return callback(new Error('not_found')); } 39 | _todos[todo.id].completed = todo.completed; 40 | callback(null, _todos[todo.id]); 41 | } 42 | 43 | function remove (id, callback) { 44 | if (!_todos[id]) { return callback(new Error('not_found')); } 45 | var removedTodo = _todos[id]; 46 | delete _todos[id]; 47 | callback(null, removedTodo); 48 | } 49 | 50 | function clearCompleted (callback) { 51 | var todos = {}; 52 | toArray(_todos).filter(function (todo) { 53 | return !todo.completed; 54 | }).forEach(function (todo) { 55 | todos[todo.id] = todo; 56 | }); 57 | _todos = todos; 58 | callback(null, toArray(_todos)); 59 | } 60 | 61 | function markAllCompleted (callback) { 62 | Object.keys(_todos).forEach(function (key) { 63 | _todos[key].completed = true; 64 | }); 65 | callback(null, toArray(_todos)); 66 | } 67 | 68 | function toArray (todos) { 69 | return Object.keys(todos).map(function (key) { 70 | return todos[key]; 71 | }); 72 | } 73 | 74 | module.exports = { 75 | add: add, 76 | edit: edit, 77 | getAll: getAll, 78 | getOne: getOne, 79 | remove: remove, 80 | complete: complete, 81 | clearCompleted: clearCompleted, 82 | markAllCompleted: markAllCompleted 83 | }; 84 | -------------------------------------------------------------------------------- /shared/filter-todos.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function filterTodos (model, currentPath, options) { 4 | options = options || {}; 5 | 6 | var completedTodos = []; 7 | var activeTodos = []; 8 | 9 | model.todos.forEach(function (todo) { 10 | if (todo.completed) { 11 | completedTodos.push(todo); 12 | } else { 13 | activeTodos.push(todo); 14 | } 15 | }); 16 | 17 | if (options.counts) { 18 | model.todosCount = model.todos.length; 19 | model.activeTodosCount = activeTodos.length; 20 | model.completedTodosCount = completedTodos.length; 21 | } 22 | 23 | if (currentPath === 'completed') { 24 | model.todos = completedTodos; 25 | } else if (currentPath === 'active') { 26 | model.todos = activeTodos; 27 | } 28 | } 29 | 30 | module.exports = filterTodos; 31 | -------------------------------------------------------------------------------- /views/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Taunus • TodoMVC 7 | 8 | 9 | 10 | 11 | 12 |
{{{ partial }}}
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /views/todos/footer.html: -------------------------------------------------------------------------------- 1 | {{ activeTodosCount }} item left 2 | 13 | {{#completedTodosCount}} 14 |
15 | 16 |
17 | {{/completedTodosCount}} 18 | -------------------------------------------------------------------------------- /views/todos/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

todos

4 |
5 | 6 |
7 |
8 | {{#todosCount}} 9 |
10 | {{^completed}} 11 |
12 | 13 | 14 |
15 | {{/completed}} 16 |
    17 | {{> list}} 18 |
19 |
20 |
21 | {{> footer}} 22 |
23 | {{/todosCount}} 24 |
25 | 30 | -------------------------------------------------------------------------------- /views/todos/list.html: -------------------------------------------------------------------------------- 1 | {{#todos}} 2 |
  • 3 |
    4 |
    5 | 6 | 7 |
    8 | 9 |
    10 | 11 |
    12 |
    13 |
    14 | 15 |
    16 |
  • 17 | {{/todos}} 18 | --------------------------------------------------------------------------------