Commit f94b1125 authored by Stephen Sawchuk's avatar Stephen Sawchuk

Implement backend API to Angular.

parent 258e1720
......@@ -3,7 +3,7 @@
"version": "0.0.0",
"dependencies": {
"angular": "1.3.0",
"todomvc-common": "~0.1.4"
"todomvc-common": "~0.3.0"
},
"devDependencies": {
"angular-mocks": "1.3.0",
......
......@@ -171,25 +171,42 @@
framework = document.querySelector('[data-framework]').dataset.framework;
}
this.template = template;
if (template && learnJSON[framework]) {
if (learnJSON.backend) {
this.frameworkJSON = learnJSON.backend;
this.append({
backend: true
});
} else if (learnJSON[framework]) {
this.frameworkJSON = learnJSON[framework];
this.template = template;
this.append();
}
}
Learn.prototype.append = function () {
Learn.prototype.append = function (opts) {
var aside = document.createElement('aside');
aside.innerHTML = _.template(this.template, this.frameworkJSON);
aside.className = 'learn';
// Localize demo links
var demoLinks = aside.querySelectorAll('.demo-link');
Array.prototype.forEach.call(demoLinks, function (demoLink) {
demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href'));
});
if (opts && opts.backend) {
// Remove demo link
var sourceLinks = aside.querySelector('.source-links');
var heading = sourceLinks.firstElementChild;
var sourceLink = sourceLinks.lastElementChild;
// Correct link path
var href = sourceLink.getAttribute('href');
sourceLink.setAttribute('href', href.substr(href.lastIndexOf('http')));
sourceLinks.innerHTML = heading.outerHTML + sourceLink.outerHTML;
} else {
// Localize demo links
var demoLinks = aside.querySelectorAll('.demo-link');
Array.prototype.forEach.call(demoLinks, function (demoLink) {
if (demoLink.getAttribute('href').substr(0, 4) !== 'http') {
demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href'));
}
});
}
document.body.className = (document.body.className + ' learn-bar').trim();
document.body.insertAdjacentHTML('afterBegin', aside.outerHTML);
......
......@@ -14,7 +14,7 @@
<header id="header">
<h1>todos</h1>
<form id="todo-form" ng-submit="addTodo()">
<input id="new-todo" placeholder="What needs to be done?" ng-model="newTodo" autofocus>
<input id="new-todo" placeholder="What needs to be done?" ng-model="newTodo" ng-disabled="saving" autofocus>
</form>
</header>
<section id="main" ng-show="todos.length" ng-cloak>
......@@ -23,12 +23,12 @@
<ul id="todo-list">
<li ng-repeat="todo in todos | filter:statusFilter track by $index" ng-class="{completed: todo.completed, editing: todo == editedTodo}">
<div class="view">
<input class="toggle" type="checkbox" ng-model="todo.completed">
<input class="toggle" type="checkbox" ng-model="todo.completed" ng-change="toggleCompleted(todo)">
<label ng-dblclick="editTodo(todo)">{{todo.title}}</label>
<button class="destroy" ng-click="removeTodo(todo)"></button>
</div>
<form ng-submit="doneEditing(todo)">
<input class="edit" ng-trim="false" ng-model="todo.title" todo-escape="revertEditing(todo)" ng-blur="doneEditing(todo)" todo-focus="todo == editedTodo">
<form ng-submit="saveEdits(todo, 'submit')">
<input class="edit" ng-trim="false" ng-model="todo.title" todo-escape="revertEdits(todo)" ng-blur="saveEdits(todo, 'blur')" todo-focus="todo == editedTodo">
</form>
</li>
</ul>
......
......@@ -9,13 +9,24 @@ angular.module('todomvc', ['ngRoute'])
.config(function ($routeProvider) {
'use strict';
$routeProvider.when('/', {
var routeConfig = {
controller: 'TodoCtrl',
templateUrl: 'todomvc-index.html'
}).when('/:status', {
controller: 'TodoCtrl',
templateUrl: 'todomvc-index.html'
}).otherwise({
redirectTo: '/'
});
templateUrl: 'todomvc-index.html',
resolve: {
store: function (todoStorage) {
// Get the correct module (API or localStorage).
return todoStorage.then(function (module) {
module.get(); // Fetch the todo records in the background.
return module;
});
}
}
};
$routeProvider
.when('/', routeConfig)
.when('/:status', routeConfig)
.otherwise({
redirectTo: '/'
});
});
......@@ -6,21 +6,18 @@
* - exposes the model to the template and provides event handlers
*/
angular.module('todomvc')
.controller('TodoCtrl', function TodoCtrl($scope, $routeParams, $filter, todoStorage) {
.controller('TodoCtrl', function TodoCtrl($scope, $routeParams, $filter, store) {
'use strict';
var todos = $scope.todos = todoStorage.get();
var todos = $scope.todos = store.todos;
$scope.newTodo = '';
$scope.editedTodo = null;
$scope.$watch('todos', function (newValue, oldValue) {
$scope.$watch('todos', function () {
$scope.remainingCount = $filter('filter')(todos, { completed: false }).length;
$scope.completedCount = todos.length - $scope.remainingCount;
$scope.allChecked = !$scope.remainingCount;
if (newValue !== oldValue) { // This prevents unneeded calls to the local storage
todoStorage.put(todos);
}
}, true);
// Monitor the current route for changes and adjust the filter accordingly.
......@@ -33,17 +30,23 @@ angular.module('todomvc')
});
$scope.addTodo = function () {
var newTodo = $scope.newTodo.trim();
if (!newTodo.length) {
var newTodo = {
title: $scope.newTodo.trim(),
completed: false
};
if (!newTodo.title) {
return;
}
todos.push({
title: newTodo,
completed: false
});
$scope.newTodo = '';
$scope.saving = true;
store.insert(newTodo)
.then(function success() {
$scope.newTodo = '';
})
.finally(function () {
$scope.saving = false;
});
};
$scope.editTodo = function (todo) {
......@@ -52,33 +55,71 @@ angular.module('todomvc')
$scope.originalTodo = angular.extend({}, todo);
};
$scope.doneEditing = function (todo) {
$scope.editedTodo = null;
$scope.saveEdits = function (todo, event) {
// Blur events are automatically triggered after the form submit event.
// This does some unfortunate logic handling to prevent saving twice.
if (event === 'blur' && $scope.saveEvent === 'submit') {
$scope.saveEvent = null;
return;
}
$scope.saveEvent = event;
if ($scope.reverted) {
// Todo edits were reverted-- don't save.
$scope.reverted = null;
return;
}
todo.title = todo.title.trim();
if (!todo.title) {
$scope.removeTodo(todo);
if (todo.title === $scope.originalTodo.title) {
return;
}
store[todo.title ? 'put' : 'delete'](todo)
.then(function success() {}, function error() {
todo.title = $scope.originalTodo.title;
})
.finally(function () {
$scope.editedTodo = null;
});
};
$scope.revertEditing = function (todo) {
$scope.revertEdits = function (todo) {
todos[todos.indexOf(todo)] = $scope.originalTodo;
$scope.doneEditing($scope.originalTodo);
$scope.editedTodo = null;
$scope.originalTodo = null;
$scope.reverted = true;
};
$scope.removeTodo = function (todo) {
todos.splice(todos.indexOf(todo), 1);
store.delete(todo);
};
$scope.saveTodo = function (todo) {
store.put(todo);
};
$scope.toggleCompleted = function (todo, completed) {
if (angular.isDefined(completed)) {
todo.completed = completed;
}
store.put(todo, todos.indexOf(todo))
.then(function success() {}, function error() {
todo.completed = !todo.completed;
});
};
$scope.clearCompletedTodos = function () {
$scope.todos = todos = todos.filter(function (val) {
return !val.completed;
});
store.clearCompleted();
};
$scope.markAll = function (completed) {
todos.forEach(function (todo) {
todo.completed = !completed;
if (todo.completed !== completed) {
$scope.toggleCompleted(todo, completed);
}
});
};
});
/*global angular */
/**
* Services that persists and retrieves TODOs from localStorage
* Services that persists and retrieves todos from localStorage or a backend API
* if available.
*
* They both follow the same API, returning promises for all changes to the
* model.
*/
angular.module('todomvc')
.factory('todoStorage', function () {
.factory('todoStorage', function ($http, $injector) {
'use strict';
var STORAGE_ID = 'todos-angularjs';
// Detect if an API backend is present. If so, return the API module, else
// hand off the localStorage adapter
return $http.head('/api')
.then(function () {
return $injector.get('api');
}, function () {
return $injector.get('localStorage');
});
})
.factory('api', function ($http) {
'use strict';
var store = {
todos: [],
clearCompleted: function () {
var originalTodos = store.todos.slice(0);
var completeTodos = [], incompleteTodos = [];
store.todos.forEach(function (todo) {
if (todo.completed) {
completeTodos.push(todo);
} else {
incompleteTodos.push(todo);
}
});
angular.copy(incompleteTodos, store.todos);
return $http.delete('/api/todos')
.then(function success() {
return store.todos;
}, function error() {
angular.copy(originalTodos, store.todos);
return originalTodos;
});
},
delete: function (todo) {
var originalTodos = store.todos.slice(0);
store.todos.splice(store.todos.indexOf(todo), 1);
return $http.delete('/api/todos/' + todo.id)
.then(function success() {
return store.todos;
}, function error() {
angular.copy(originalTodos, store.todos);
return originalTodos;
});
},
return {
get: function () {
return $http.get('/api/todos')
.then(function (resp) {
angular.copy(resp.data, store.todos);
return store.todos;
});
},
insert: function (todo) {
var originalTodos = store.todos.slice(0);
store.todos.push(todo);
return $http.post('/api/todos', todo)
.then(function success(resp) {
todo.id = resp.data.id;
return store.todos;
}, function error() {
angular.copy(originalTodos, store.todos);
return store.todos;
});
},
put: function (todo) {
var originalTodos = store.todos.slice(0);
return $http.put('/api/todos/' + todo.id, todo)
.then(function success() {
return store.todos;
}, function error() {
angular.copy(originalTodos, store.todos);
return originalTodos;
});
}
};
return store;
})
.factory('localStorage', function ($q) {
'use strict';
var STORAGE_ID = 'todos-angularjs';
var store = {
todos: [],
_getFromLocalStorage: function () {
return JSON.parse(localStorage.getItem(STORAGE_ID) || '[]');
},
put: function (todos) {
_saveToLocalStorage: function (todos) {
localStorage.setItem(STORAGE_ID, JSON.stringify(todos));
},
clearCompleted: function () {
var deferred = $q.defer();
var completeTodos = [], incompleteTodos = [];
store.todos.forEach(function (todo) {
if (todo.completed) {
completeTodos.push(todo);
} else {
incompleteTodos.push(todo);
}
});
angular.copy(incompleteTodos, store.todos);
store._saveToLocalStorage(store.todos);
deferred.resolve(store.todos);
return deferred.promise;
},
delete: function (todo) {
var deferred = $q.defer();
store.todos.splice(store.todos.indexOf(todo), 1);
store._saveToLocalStorage(store.todos);
deferred.resolve(store.todos);
return deferred.promise;
},
get: function () {
var deferred = $q.defer();
angular.copy(store._getFromLocalStorage(), store.todos);
deferred.resolve(store.todos);
return deferred.promise;
},
insert: function (todo) {
var deferred = $q.defer();
store.todos.push(todo);
store._saveToLocalStorage(store.todos);
deferred.resolve(store.todos);
return deferred.promise;
},
put: function (todo, index) {
var deferred = $q.defer();
store.todos[index] = todo;
store._saveToLocalStorage(store.todos);
deferred.resolve(store.todos);
return deferred.promise;
}
};
return store;
});
......@@ -3,24 +3,28 @@
'use strict';
describe('Todo Controller', function () {
var ctrl, scope;
var todoList;
var todoStorage = {
storage: {},
get: function () {
return this.storage;
},
put: function (value) {
this.storage = value;
}
};
// Load the module containing the app, only 'ng' is loaded by default.
var ctrl, scope, store;
// Load the module containing the app, only 'ng' is loaded by default.
beforeEach(module('todomvc'));
beforeEach(inject(function ($controller, $rootScope) {
beforeEach(inject(function ($controller, $rootScope, localStorage) {
scope = $rootScope.$new();
ctrl = $controller('TodoCtrl', { $scope: scope });
store = localStorage;
localStorage.todos = [];
localStorage._getFromLocalStorage = function () {
return [];
};
localStorage._saveToLocalStorage = function (todos) {
localStorage.todos = todos;
};
ctrl = $controller('TodoCtrl', {
$scope: scope,
store: store
});
}));
it('should not have an edited Todo on start', function () {
......@@ -48,6 +52,7 @@
it('should filter non-completed', inject(function ($controller) {
ctrl = $controller('TodoCtrl', {
$scope: scope,
store: store,
$routeParams: {
status: 'active'
}
......@@ -64,7 +69,8 @@
$scope: scope,
$routeParams: {
status: 'completed'
}
},
store: store
});
scope.$emit('$routeChangeSuccess');
......@@ -77,10 +83,9 @@
var ctrl;
beforeEach(inject(function ($controller) {
todoStorage.storage = [];
ctrl = $controller('TodoCtrl', {
$scope: scope,
todoStorage: todoStorage
store: store
});
scope.$digest();
}));
......@@ -113,28 +118,16 @@
var ctrl;
beforeEach(inject(function ($controller) {
todoList = [{
'title': 'Uncompleted Item 0',
'completed': false
}, {
'title': 'Uncompleted Item 1',
'completed': false
}, {
'title': 'Uncompleted Item 2',
'completed': false
}, {
'title': 'Completed Item 0',
'completed': true
}, {
'title': 'Completed Item 1',
'completed': true
}];
todoStorage.storage = todoList;
ctrl = $controller('TodoCtrl', {
$scope: scope,
todoStorage: todoStorage
store: store
});
store.insert({ title: 'Uncompleted Item 0', completed: false });
store.insert({ title: 'Uncompleted Item 1', completed: false });
store.insert({ title: 'Uncompleted Item 2', completed: false });
store.insert({ title: 'Completed Item 0', completed: true })
store.insert({ title: 'Completed Item 1', completed: true })
scope.$digest();
}));
......@@ -146,22 +139,22 @@
});
it('should save Todos to local storage', function () {
expect(todoStorage.storage.length).toBe(5);
expect(scope.todos.length).toBe(5);
});
it('should remove Todos w/o title on saving', function () {
var todo = todoList[2];
var todo = store.todos[2];
scope.editTodo(todo);
todo.title = '';
scope.doneEditing(todo);
scope.saveEdits(todo);
expect(scope.todos.length).toBe(4);
});
it('should trim Todos on saving', function () {
var todo = todoList[0];
var todo = store.todos[0];
scope.editTodo(todo);
todo.title = ' buy moar unicorns ';
scope.doneEditing(todo);
scope.saveEdits(todo);
expect(scope.todos[0].title).toBe('buy moar unicorns');
});
......@@ -171,16 +164,16 @@
});
it('markAll() should mark all Todos completed', function () {
scope.markAll();
scope.markAll(true);
scope.$digest();
expect(scope.completedCount).toBe(5);
});
it('revertTodo() get a Todo to its previous state', function () {
var todo = todoList[0];
var todo = store.todos[0];
scope.editTodo(todo);
todo.title = 'Unicorn sparkly skypuffles.';
scope.revertEditing(todo);
scope.revertEdits(todo);
scope.$digest();
expect(scope.todos[0].title).toBe('Uncompleted Item 0');
});
......
......@@ -2265,6 +2265,6 @@
}]
},
"templates": {
"todomvc": "<header> <h3><%= name %></h3> <span class=\"source-links\"> <% if (typeof examples !== 'undefined') { %> <% examples.forEach(function (example) { %> <h5><%= example.name %></h5> <% if (!location.href.match(example.url + '/')) { %> <a class=\"demo-link\" href=\"<%= example.url %>\">Demo</a>, <% } %> <a href=\"https://github.com/tastejs/todomvc/tree/gh-pages/<%= example.source_url ? example.source_url : example.url %>\">Source</a> <% }); %> <% } %> </span> </header> <hr> <blockquote class=\"quote speech-bubble\"> <p><%= description %></p> <footer> <a href=\"http://<%= homepage %>\"><%= name %></a> </footer> </blockquote> <% if (typeof link_groups !== 'undefined') { %> <hr> <% link_groups.forEach(function (link_group) { %> <h4><%= link_group.heading %></h4> <ul> <% link_group.links.forEach(function (link) { %> <li> <a href=\"<%= link.url %>\"><%= link.name %></a> </li> <% }); %> </ul> <% }); %> <% } %> <footer> <hr> <em>If you have other helpful links to share, or find any of the links above no longer work, please <a href=\"https://github.com/tastejs/todomvc/issues\">let us know</a>.</em> </footer>"
"todomvc": "<header> <h3><%= name %></h3> <span class=\"source-links\"> <% if (typeof examples !== 'undefined') { %> <% examples.forEach(function (example) { %> <h5><%= example.name %></h5> <% if (!location.href.match(example.url + '/')) { %> <a class=\"demo-link\" data-type=\"<%= example.type === 'backend' ? 'external' : 'local' %>\" href=\"<%= example.url %>\">Demo</a>, <% } if (example.type === 'backend') { %><a href=\"<%= example.source_url %>\"><% } else { %><a href=\"https://github.com/tastejs/todomvc/tree/gh-pages/<%= example.source_url ? example.source_url : example.url %>\"><% } %>Source</a> <% }); %> <% } %> </span> </header> <hr> <blockquote class=\"quote speech-bubble\"> <p><%= description %></p> <footer> <a href=\"http://<%= homepage %>\"><%= name %></a> </footer> </blockquote> <% if (typeof link_groups !== 'undefined') { %> <hr> <% link_groups.forEach(function (link_group) { %> <h4><%= link_group.heading %></h4> <ul> <% link_group.links.forEach(function (link) { %> <li> <a href=\"<%= link.url %>\"><%= link.name %></a> </li> <% }); %> </ul> <% }); %> <% } %> <footer> <hr> <em>If you have other helpful links to share, or find any of the links above no longer work, please <a href=\"https://github.com/tastejs/todomvc/issues\">let us know</a>.</em> </footer>"
}
}
{
"name": "todomvc",
"version": "0.0.0",
"main": "server.js",
"files": [
"license.md",
"server.js",
"examples",
"media",
"site-assets",
"index.html",
"learn.json"
],
"dependencies": {
"express": "^4.10.0"
},
"devDependencies": {
"del": "^0.1.1",
"gulp": "^3.8.5",
......@@ -19,9 +34,5 @@
"jshint-stylish": "^1.0.0",
"psi": "^0.1.1",
"run-sequence": "^1.0.1"
},
"engines": {
"node": ">=0.10.0"
},
"private": true
}
}
'use strict';
var express = require('express');
var fs = require('fs');
var learnJson = require('./learn.json');
var app = module.exports = express();
app.use(express.static(__dirname));
Object.defineProperty(module.exports, 'learnJson', {
set: function (backend) {
learnJson.backend = backend;
fs.writeFile(require.resolve('./learn.json'), JSON.stringify(learnJson, null, 2), function (err) {
if (err) {
throw err;
}
});
}
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment