├── 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 |
--------------------------------------------------------------------------------