├── Procfile ├── index.html ├── js ├── app.js ├── controllers │ └── todoCtrl.js ├── directives │ ├── todoEscape.js │ └── todoFocus.js └── services │ └── todoStorage.js ├── package.json ├── readme.md └── server.js /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularJS • TodoMVC 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /js/app.js: -------------------------------------------------------------------------------- 1 | /*global angular */ 2 | 3 | /** 4 | * The main TodoMVC app module 5 | * 6 | * @type {angular.Module} 7 | */ 8 | angular.module('todomvc', ['ngRoute']) 9 | .config(function ($routeProvider) { 10 | 'use strict'; 11 | 12 | var routeConfig = { 13 | controller: 'TodoCtrl', 14 | templateUrl: 'todomvc-index.html', 15 | resolve: { 16 | store: function (todoStorage) { 17 | // Get the correct module (API or localStorage). 18 | return todoStorage.then(function (module) { 19 | module.get(); // Fetch the todo records in the background. 20 | return module; 21 | }); 22 | } 23 | } 24 | }; 25 | 26 | $routeProvider 27 | .when('/', routeConfig) 28 | .when('/:status', routeConfig) 29 | .otherwise({ 30 | redirectTo: '/' 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /js/controllers/todoCtrl.js: -------------------------------------------------------------------------------- 1 | /*global angular */ 2 | 3 | /** 4 | * The main controller for the app. The controller: 5 | * - retrieves and persists the model via the todoStorage service 6 | * - exposes the model to the template and provides event handlers 7 | */ 8 | angular.module('todomvc') 9 | .controller('TodoCtrl', function TodoCtrl($scope, $routeParams, $filter, store) { 10 | 'use strict'; 11 | 12 | var todos = $scope.todos = store.todos; 13 | 14 | $scope.newTodo = ''; 15 | $scope.editedTodo = null; 16 | 17 | $scope.$watch('todos', function () { 18 | $scope.remainingCount = $filter('filter')(todos, { completed: false }).length; 19 | $scope.completedCount = todos.length - $scope.remainingCount; 20 | $scope.allChecked = !$scope.remainingCount; 21 | }, true); 22 | 23 | // Monitor the current route for changes and adjust the filter accordingly. 24 | $scope.$on('$routeChangeSuccess', function () { 25 | var status = $scope.status = $routeParams.status || ''; 26 | 27 | $scope.statusFilter = (status === 'active') ? 28 | { completed: false } : (status === 'completed') ? 29 | { completed: true } : ''; 30 | }); 31 | 32 | $scope.addTodo = function () { 33 | var newTodo = { 34 | title: $scope.newTodo.trim(), 35 | completed: false 36 | }; 37 | 38 | if (!newTodo.title) { 39 | return; 40 | } 41 | 42 | $scope.saving = true; 43 | store.insert(newTodo) 44 | .then(function success() { 45 | $scope.newTodo = ''; 46 | }) 47 | .finally(function () { 48 | $scope.saving = false; 49 | }); 50 | }; 51 | 52 | $scope.editTodo = function (todo) { 53 | $scope.editedTodo = todo; 54 | // Clone the original todo to restore it on demand. 55 | $scope.originalTodo = angular.extend({}, todo); 56 | }; 57 | 58 | $scope.saveEdits = function (todo, event) { 59 | // Blur events are automatically triggered after the form submit event. 60 | // This does some unfortunate logic handling to prevent saving twice. 61 | if (event === 'blur' && $scope.saveEvent === 'submit') { 62 | $scope.saveEvent = null; 63 | return; 64 | } 65 | 66 | $scope.saveEvent = event; 67 | 68 | if ($scope.reverted) { 69 | // Todo edits were reverted-- don't save. 70 | $scope.reverted = null; 71 | return; 72 | } 73 | 74 | todo.title = todo.title.trim(); 75 | 76 | if (todo.title === $scope.originalTodo.title) { 77 | $scope.editedTodo = null; 78 | return; 79 | } 80 | 81 | store[todo.title ? 'put' : 'delete'](todo) 82 | .then(function success() {}, function error() { 83 | todo.title = $scope.originalTodo.title; 84 | }) 85 | .finally(function () { 86 | $scope.editedTodo = null; 87 | }); 88 | }; 89 | 90 | $scope.revertEdits = function (todo) { 91 | todos[todos.indexOf(todo)] = $scope.originalTodo; 92 | $scope.editedTodo = null; 93 | $scope.originalTodo = null; 94 | $scope.reverted = true; 95 | }; 96 | 97 | $scope.removeTodo = function (todo) { 98 | store.delete(todo); 99 | }; 100 | 101 | $scope.saveTodo = function (todo) { 102 | store.put(todo); 103 | }; 104 | 105 | $scope.toggleCompleted = function (todo, completed) { 106 | if (angular.isDefined(completed)) { 107 | todo.completed = completed; 108 | } 109 | store.put(todo, todos.indexOf(todo)) 110 | .then(function success() {}, function error() { 111 | todo.completed = !todo.completed; 112 | }); 113 | }; 114 | 115 | $scope.clearCompletedTodos = function () { 116 | store.clearCompleted(); 117 | }; 118 | 119 | $scope.markAll = function (completed) { 120 | todos.forEach(function (todo) { 121 | if (todo.completed !== completed) { 122 | $scope.toggleCompleted(todo, completed); 123 | } 124 | }); 125 | }; 126 | }); 127 | -------------------------------------------------------------------------------- /js/directives/todoEscape.js: -------------------------------------------------------------------------------- 1 | /*global angular */ 2 | 3 | /** 4 | * Directive that executes an expression when the element it is applied to gets 5 | * an `escape` keydown event. 6 | */ 7 | angular.module('todomvc') 8 | .directive('todoEscape', function () { 9 | 'use strict'; 10 | 11 | var ESCAPE_KEY = 27; 12 | 13 | return function (scope, elem, attrs) { 14 | elem.bind('keydown', function (event) { 15 | if (event.keyCode === ESCAPE_KEY) { 16 | scope.$apply(attrs.todoEscape); 17 | } 18 | }); 19 | 20 | scope.$on('$destroy', function () { 21 | elem.unbind('keydown'); 22 | }); 23 | }; 24 | }); 25 | -------------------------------------------------------------------------------- /js/directives/todoFocus.js: -------------------------------------------------------------------------------- 1 | /*global angular */ 2 | 3 | /** 4 | * Directive that places focus on the element it is applied to when the 5 | * expression it binds to evaluates to true 6 | */ 7 | angular.module('todomvc') 8 | .directive('todoFocus', function todoFocus($timeout) { 9 | 'use strict'; 10 | 11 | return function (scope, elem, attrs) { 12 | scope.$watch(attrs.todoFocus, function (newVal) { 13 | if (newVal) { 14 | $timeout(function () { 15 | elem[0].focus(); 16 | }, 0, false); 17 | } 18 | }); 19 | }; 20 | }); 21 | -------------------------------------------------------------------------------- /js/services/todoStorage.js: -------------------------------------------------------------------------------- 1 | /*global angular */ 2 | 3 | /** 4 | * Services that persists and retrieves todos from localStorage or a backend API 5 | * if available. 6 | * 7 | * They both follow the same API, returning promises for all changes to the 8 | * model. 9 | */ 10 | angular.module('todomvc') 11 | .factory('todoStorage', function ($http, $injector) { 12 | 'use strict'; 13 | 14 | // Detect if an API backend is present. If so, return the API module, else 15 | // hand off the localStorage adapter 16 | return $http.get('/api') 17 | .then(function () { 18 | return $injector.get('api'); 19 | }, function () { 20 | return $injector.get('localStorage'); 21 | }); 22 | }) 23 | 24 | .factory('api', function ($http) { 25 | 'use strict'; 26 | 27 | var store = { 28 | todos: [], 29 | 30 | clearCompleted: function () { 31 | var originalTodos = store.todos.slice(0); 32 | 33 | var completeTodos = []; 34 | var incompleteTodos = []; 35 | store.todos.forEach(function (todo) { 36 | if (todo.completed) { 37 | completeTodos.push(todo); 38 | } else { 39 | incompleteTodos.push(todo); 40 | } 41 | }); 42 | 43 | angular.copy(incompleteTodos, store.todos); 44 | 45 | return $http.delete('/api/todos') 46 | .then(function success() { 47 | return store.todos; 48 | }, function error() { 49 | angular.copy(originalTodos, store.todos); 50 | return originalTodos; 51 | }); 52 | }, 53 | 54 | delete: function (todo) { 55 | var originalTodos = store.todos.slice(0); 56 | 57 | store.todos.splice(store.todos.indexOf(todo), 1); 58 | 59 | return $http.delete('/api/todos/' + todo.id) 60 | .then(function success() { 61 | return store.todos; 62 | }, function error() { 63 | angular.copy(originalTodos, store.todos); 64 | return originalTodos; 65 | }); 66 | }, 67 | 68 | get: function () { 69 | return $http.get('/api/todos') 70 | .then(function (resp) { 71 | angular.copy(resp.data, store.todos); 72 | return store.todos; 73 | }); 74 | }, 75 | 76 | insert: function (todo) { 77 | var originalTodos = store.todos.slice(0); 78 | 79 | return $http.post('/api/todos', todo) 80 | .then(function success(resp) { 81 | todo.id = resp.data.id; 82 | store.todos.push(todo); 83 | return store.todos; 84 | }, function error() { 85 | angular.copy(originalTodos, store.todos); 86 | return store.todos; 87 | }); 88 | }, 89 | 90 | put: function (todo) { 91 | var originalTodos = store.todos.slice(0); 92 | 93 | return $http.put('/api/todos/' + todo.id, todo) 94 | .then(function success() { 95 | return store.todos; 96 | }, function error() { 97 | angular.copy(originalTodos, store.todos); 98 | return originalTodos; 99 | }); 100 | } 101 | }; 102 | 103 | return store; 104 | }) 105 | 106 | .factory('localStorage', function ($q) { 107 | 'use strict'; 108 | 109 | var STORAGE_ID = 'todos-angularjs'; 110 | 111 | var store = { 112 | todos: [], 113 | 114 | _getFromLocalStorage: function () { 115 | return JSON.parse(localStorage.getItem(STORAGE_ID) || '[]'); 116 | }, 117 | 118 | _saveToLocalStorage: function (todos) { 119 | localStorage.setItem(STORAGE_ID, JSON.stringify(todos)); 120 | }, 121 | 122 | clearCompleted: function () { 123 | var deferred = $q.defer(); 124 | 125 | var completeTodos = []; 126 | var incompleteTodos = []; 127 | store.todos.forEach(function (todo) { 128 | if (todo.completed) { 129 | completeTodos.push(todo); 130 | } else { 131 | incompleteTodos.push(todo); 132 | } 133 | }); 134 | 135 | angular.copy(incompleteTodos, store.todos); 136 | 137 | store._saveToLocalStorage(store.todos); 138 | deferred.resolve(store.todos); 139 | 140 | return deferred.promise; 141 | }, 142 | 143 | delete: function (todo) { 144 | var deferred = $q.defer(); 145 | 146 | store.todos.splice(store.todos.indexOf(todo), 1); 147 | 148 | store._saveToLocalStorage(store.todos); 149 | deferred.resolve(store.todos); 150 | 151 | return deferred.promise; 152 | }, 153 | 154 | get: function () { 155 | var deferred = $q.defer(); 156 | 157 | angular.copy(store._getFromLocalStorage(), store.todos); 158 | deferred.resolve(store.todos); 159 | 160 | return deferred.promise; 161 | }, 162 | 163 | insert: function (todo) { 164 | var deferred = $q.defer(); 165 | 166 | store.todos.push(todo); 167 | 168 | store._saveToLocalStorage(store.todos); 169 | deferred.resolve(store.todos); 170 | 171 | return deferred.promise; 172 | }, 173 | 174 | put: function (todo, index) { 175 | var deferred = $q.defer(); 176 | 177 | store.todos[index] = todo; 178 | 179 | store._saveToLocalStorage(store.todos); 180 | deferred.resolve(store.todos); 181 | 182 | return deferred.promise; 183 | } 184 | }; 185 | 186 | return store; 187 | }); 188 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "angular": "^1.3.12", 5 | "todomvc-common": "^1.0.0", 6 | "todomvc-app-css": "^1.0.1", 7 | "angular-route": "^1.3.12", 8 | "express": "~3.4.7", 9 | "body-parser": "~1.13.1", 10 | "mongoose": "~4.0.6" 11 | }, 12 | "engines": { 13 | "node": ">=0.10.25" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # AngularJS TodoMVC Example 2 | 3 | > It's based on [this MVC AngularJS ToDo List](http://todomvc.com/examples/angularjs/#/), I created a new github repository to be easier to deploy it on Heroku. 4 | 5 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 2 | // https://devcenter.heroku.com/articles/mongolab 3 | // http://todomvc.com/examples/angularjs/#/ 4 | var express = require('express'), 5 | mongoose = require('mongoose'), 6 | bodyParser = require('body-parser'), 7 | 8 | // Mongoose Schema definition 9 | Schema = new mongoose.Schema({ 10 | id : String, 11 | title : String, 12 | completed: Boolean 13 | }), 14 | 15 | Todo = mongoose.model('Todo', Schema); 16 | 17 | /* 18 | * I’m sharing my credential here. 19 | * Feel free to use it while you’re learning. 20 | * After that, create and use your own credential. 21 | * Thanks. 22 | * 23 | * MONGOLAB_URI=mongodb://example:example@ds053312.mongolab.com:53312/todolist 24 | * 'mongodb://example:example@ds053312.mongolab.com:53312/todolist' 25 | */ 26 | mongoose.connect(process.env.MONGOLAB_URI, function (error) { 27 | if (error) console.error(error); 28 | else console.log('mongo connected'); 29 | }); 30 | 31 | express() 32 | // https://scotch.io/tutorials/use-expressjs-to-get-url-and-post-parameters 33 | .use(bodyParser.json()) // support json encoded bodies 34 | .use(bodyParser.urlencoded({ extended: true })) // support encoded bodies 35 | 36 | .get('/api', function (req, res) { 37 | res.json(200, {msg: 'OK' }); 38 | }) 39 | 40 | .get('/api/todos', function (req, res) { 41 | // http://mongoosejs.com/docs/api.html#query_Query-find 42 | Todo.find( function ( err, todos ){ 43 | res.json(200, todos); 44 | }); 45 | }) 46 | 47 | .post('/api/todos', function (req, res) { 48 | var todo = new Todo( req.body ); 49 | todo.id = todo._id; 50 | // http://mongoosejs.com/docs/api.html#model_Model-save 51 | todo.save(function (err) { 52 | res.json(200, todo); 53 | }); 54 | }) 55 | 56 | .del('/api/todos', function (req, res) { 57 | // http://mongoosejs.com/docs/api.html#query_Query-remove 58 | Todo.remove({ completed: true }, function ( err ) { 59 | res.json(200, {msg: 'OK'}); 60 | }); 61 | }) 62 | 63 | .get('/api/todos/:id', function (req, res) { 64 | // http://mongoosejs.com/docs/api.html#model_Model.findById 65 | Todo.findById( req.params.id, function ( err, todo ) { 66 | res.json(200, todo); 67 | }); 68 | }) 69 | 70 | .put('/api/todos/:id', function (req, res) { 71 | // http://mongoosejs.com/docs/api.html#model_Model.findById 72 | Todo.findById( req.params.id, function ( err, todo ) { 73 | todo.title = req.body.title; 74 | todo.completed = req.body.completed; 75 | // http://mongoosejs.com/docs/api.html#model_Model-save 76 | todo.save( function ( err, todo ){ 77 | res.json(200, todo); 78 | }); 79 | }); 80 | }) 81 | 82 | .del('/api/todos/:id', function (req, res) { 83 | // http://mongoosejs.com/docs/api.html#model_Model.findById 84 | Todo.findById( req.params.id, function ( err, todo ) { 85 | // http://mongoosejs.com/docs/api.html#model_Model.remove 86 | todo.remove( function ( err, todo ){ 87 | res.json(200, {msg: 'OK'}); 88 | }); 89 | }); 90 | }) 91 | 92 | .use(express.static(__dirname + '/')) 93 | .listen(process.env.PORT || 5000); 94 | --------------------------------------------------------------------------------