Commit 2e76a44a authored by Addy Osmani's avatar Addy Osmani

Merge pull request #112 from cburgdorf/angularRefactoring

Angular refactoring
parents b0c6707a 42c1985d
This diff is collapsed.
html,
body {
margin: 0;
padding: 0;
}
body {
font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #eeeeee;
color: #333333;
width: 520px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
}
#todoapp {
background: #fff;
padding: 20px;
margin-bottom: 40px;
-webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
-moz-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
-ms-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
-o-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
-webkit-border-radius: 0 0 5px 5px;
-moz-border-radius: 0 0 5px 5px;
-ms-border-radius: 0 0 5px 5px;
-o-border-radius: 0 0 5px 5px;
border-radius: 0 0 5px 5px;
}
#todoapp h1 {
font-size: 36px;
font-weight: bold;
text-align: center;
padding: 0 0 10px 0;
}
#todoapp input[type="text"] {
width: 466px;
font-size: 24px;
font-family: inherit;
line-height: 1.4em;
border: 0;
outline: none;
padding: 6px;
border: 1px solid #999999;
-webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
-moz-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
-ms-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
-o-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
}
#todoapp input::-webkit-input-placeholder {
font-style: italic;
}
#todo-list {
margin: 10px 0;
padding: 0;
list-style: none;
}
#todo-list li {
padding: 18px 20px 18px 0;
position: relative;
font-size: 24px;
border-bottom: 1px solid #cccccc;
}
#todo-list li:last-child {
border-bottom: none;
}
#todo-list li.done label {
color: #777777;
text-decoration: line-through;
}
#todo-list li .destroy {
display: none;
position: absolute;
top: 20px;
right: 10px;
cursor: pointer;
width: 20px;
height: 20px;
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUBAMAAAB/pwA+AAAABGdBTUEAALGPC/xhBQAAACdQTFRFzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMAAAA////zMzMhnu0WAAAAAt0Uk5T5u3pqtV3jFQEKAC0bVelAAAAfUlEQVQI12NYtWpFsc8R865VqxhWrZpyBgg8QcylZ8AgCsjMgTCPrWJYfgYKqhjWwJgaDDVnzpw+c2bPmTPHGWzOnNm95/TuM2cOM/AARXfvBooeZAAp270bRCIz4QoOIGtDMqwJZoUEQzvCYrhzuhhWtUKYEahOX7UK6iEA3A6NUGwCTZIAAAAASUVORK5CYII=') no-repeat center center;
}
#todo-list li:hover .destroy {
display: block;
}
#todo-list li.editing {
border-bottom: none;
margin-top: -1px;
padding: 0;
}
#todo-list li.editing:last-child {
margin-bottom: -1px;
}
#todo-list li.editing .edit {
display: block;
width: 444px;
padding: 13px 15px 14px 20px;
margin: 0;
}
#todo-list li.editing .view {
display: none;
}
#todo-list li .view label {
word-break: break-word;
}
#todo-list li .edit {
display: none;
}
#todoapp footer {
margin: 0 -20px -20px -20px;
overflow: hidden;
color: #555555;
background: #f4fce8;
border-top: 1px solid #ededed;
padding: 0 20px;
line-height: 37px;
-webkit-border-radius: 0 0 5px 5px;
-moz-border-radius: 0 0 5px 5px;
-ms-border-radius: 0 0 5px 5px;
-o-border-radius: 0 0 5px 5px;
border-radius: 0 0 5px 5px;
}
#clear-completed {
float: right;
line-height: 20px;
text-decoration: none;
background: rgba(0, 0, 0, 0.1);
color: #555555;
font-size: 11px;
margin-top: 8px;
margin-bottom: 8px;
padding: 0 10px 1px;
cursor: pointer;
-webkit-border-radius: 12px;
-moz-border-radius: 12px;
-ms-border-radius: 12px;
-o-border-radius: 12px;
border-radius: 12px;
-webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
-moz-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
-ms-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
-o-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
}
#clear-completed:hover {
background: rgba(0, 0, 0, 0.15);
-webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
-moz-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
-ms-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
-o-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
}
#clear-completed:active {
position: relative;
top: 1px;
}
#todo-count span {
font-weight: bold;
}
#instructions {
margin: 10px auto;
color: #777777;
text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0;
text-align: center;
}
#instructions a {
color: #336699;
}
#credits {
margin: 30px auto;
color: #999;
text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0;
text-align: center;
}
#credits a {
color: #888;
}
\ No newline at end of file
...@@ -2,75 +2,48 @@ ...@@ -2,75 +2,48 @@
<html xmlns:ng="http://angularjs.org/" xmlns:my="http://rx.org"> <html xmlns:ng="http://angularjs.org/" xmlns:my="http://rx.org">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>AngularJS Todo App</title> <title>AngularJS - TodoMVC</title>
<link rel="stylesheet" href="css/app.css"/> <link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="css/app.css">
</head> </head>
<body> <body>
<div ng:controller="App.Controllers.TodoController" id="todoapp"> <div ng:controller="App.Controllers.TodoController" id="todoapp">
<div class="title"> <header>
<h1> <h1>Todos</h1>
Todos
</h1>
</div>
<div class="content">
<div id="todo-form">
</div>
<form id="todo-form" ng:submit="addTodo()"> <form id="todo-form" ng:submit="addTodo()">
<input id="new-todo" name="newTodo" my:blur="addTodo()" placeholder="What needs to be done?" type="text"> <input id="new-todo" name="newTodo" type="text" placeholder="What needs to be done?">
<span class="ui-tooltip-top" ng:show="showHitEnterHint">
Press Enter to save this task
</span>
</form> </form>
<div id="todos"> </header>
<section id="main" ng:show="hasTodos()">
<input id="toggle-all" type="checkbox" name="allChecked" ng:click="toggleAllStates()">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list"> <ul id="todo-list">
<li class="todo" ng:class="'editing-' + todo.editing + ' done-' + todo.done" ng:repeat="todo in todos"> <li ng:repeat="todo in todos" my:dblclick="editTodo(todo)" ng:class="(todo.done && ' done ') + (todo.editing && ' editing ')">
<div class="display"> <div class="view">
<input class="check" type="checkbox" name="todo.done" / > <input class="toggle" type="checkbox" name="todo.done">
<div ng:click="editTodo(todo)" class="todo-content"> {{ todo.content }} </div> <label>{{ todo.title }}</label>
<span class="todo-destroy" ng:click="removeTodo(todo)"></span> <a class="destroy" ng:click="removeTodo(todo)"></a>
</div> </div>
<div class="edit">
<form ng:submit="finishEditing(todo)"> <form ng:submit="finishEditing(todo)">
<input class="todo-input" my:focus="todo.editing" my:blur="finishEditing(todo)" name="todo.content" type="text"> <input class="edit" type="text" name="todo.title" my:focus="todo.editing" my:blur="finishEditing(todo)">
</form> </form>
</div>
</li> </li>
</ul> </ul>
</section>
<footer ng:show="hasTodos()">
<a id="clear-completed" ng:click="clearCompletedItems()" ng:show="hasFinishedTodos()">{{ clearItemsText() }}</a>
<div id="todo-count"><b>{{ remainingTodos() }}</b> {{ itemsLeftText() }}</div>
</footer>
</div> </div>
<div id="todo-stats"> <div id="instructions">
<span class="todo-count" ng:show="hasTodos()"> Double-click to edit a todo.
<ng:pluralize count="remainingTodos()" when="{'0' : 'No items left.', '1': '1 item left.', 'other' : '{} items left.' }">
</ng:pluralize>
</span>
<span class="todo-clear" ng:show="hasFinishedTodos()">
<a ng:click="clearCompletedItems()">
Clear <ng:pluralize count="finishedTodos()" when="{'1': '1 completed item', 'other' : '{} completed items' }">
</ng:pluralize>
</a>
</span>
</div> </div>
</div>
</div>
<ul id="instructions">
<li>Click to edit a todo.</li>
</ul>
<div id="credits"> <div id="credits">
<p> Created by <a href="http://twitter.com/cburgdorf">Christoph Burgdorf</a>.
Originally Created by
<br>
<a href="http://jgn.me/">Jérôme Gravel-Niquet</a>
</p>
<p>
Rewritten to use <a href="http://angularjs.org">AngularJS </a> by
<br>
<a href="http://cburgdorf.wordpress.com/">Christoph Burgdorf</a>
<br>Cleanup, edits: <a href="http://www.linkedin.com/pub/dan-doyon/2/1b0/a83">Dan Doyon</a>
</p>
</div> </div>
<script src="js/libs/json2.js"></script>
<script src="js/booter.js"></script> <script src="js/booter.js"></script>
<script src="lib/angular/angular.min.js" ng:autobind></script> <script src="js/libs/angular/angular.min.js" ng:autobind></script>
<script src="lib/rx/rx.js"></script>
<script src="lib/rx/rx.angular.js"></script>
<script src="js/controllers.js"></script> <script src="js/controllers.js"></script>
<script src="js/directive.js"></script> <script src="js/directive.js"></script>
</body> </body>
......
...@@ -5,11 +5,31 @@ App.Controllers.TodoController = function () { ...@@ -5,11 +5,31 @@ App.Controllers.TodoController = function () {
self.newTodo = ""; self.newTodo = "";
var retrieveStore = function() {
var store = localStorage.getItem('todo-angularjs');
return ( store && JSON.parse( store ) ) || [];
};
var updateStore = function() {
var isEditing = angular.Array.count(self.todos, function(x) {
return x.editing;
});
if (!isEditing){
localStorage.setItem('todo-angularjs', JSON.stringify(self.todos));
}
};
//not sure if its intended to do so. However, we need a hook to update the store
//whenever angular changes any properties
self.$watch(updateStore);
self.todos = retrieveStore();
self.addTodo = function() { self.addTodo = function() {
if (self.newTodo.length === 0) return; if (self.newTodo.trim().length === 0) return;
self.todos.push({ self.todos.push({
content: self.newTodo, title: self.newTodo,
done: false, done: false,
editing: false editing: false
}); });
...@@ -25,15 +45,18 @@ App.Controllers.TodoController = function () { ...@@ -25,15 +45,18 @@ App.Controllers.TodoController = function () {
}; };
self.finishEditing = function(todo) { self.finishEditing = function(todo) {
if (todo.title.trim().length === 0){
self.removeTodo(todo);
}
else{
todo.editing = false; todo.editing = false;
}
}; };
self.removeTodo = function(todo) { self.removeTodo = function(todo) {
angular.Array.remove(self.todos, todo); angular.Array.remove(self.todos, todo);
}; };
self.todos = [];
var countTodos = function(done) { var countTodos = function(done) {
return function() { return function() {
return angular.Array.count(self.todos, function(x) { return angular.Array.count(self.todos, function(x) {
...@@ -42,16 +65,36 @@ App.Controllers.TodoController = function () { ...@@ -42,16 +65,36 @@ App.Controllers.TodoController = function () {
} }
}; };
var pluralize = function( count, word ) {
return count === 1 ? word : word + 's';
};
self.remainingTodos = countTodos("undone"); self.remainingTodos = countTodos("undone");
self.finishedTodos = countTodos("done"); self.finishedTodos = countTodos("done");
self.itemsLeftText = function(){
return pluralize(self.remainingTodos(), 'item') + ' left'
};
self.clearItemsText = function(){
var finishedTodos = self.finishedTodos();
return 'Clear ' + finishedTodos + ' completed ' + pluralize(finishedTodos, 'item');
};
self.clearCompletedItems = function() { self.clearCompletedItems = function() {
var oldTodos = self.todos; var oldTodos = self.todos;
self.todos = []; self.todos = [];
angular.forEach(oldTodos, function(todo) { angular.forEach(oldTodos, function(todo) {
if (!todo.done) self.todos.push(todo); if (!todo.done) self.todos.push(todo);
}); });
self.allChecked = false;
};
self.toggleAllStates = function(){
angular.forEach(self.todos, function(todo){
todo.done = self.allChecked;
})
}; };
self.hasFinishedTodos = function() { self.hasFinishedTodos = function() {
...@@ -61,19 +104,4 @@ App.Controllers.TodoController = function () { ...@@ -61,19 +104,4 @@ App.Controllers.TodoController = function () {
self.hasTodos = function() { self.hasTodos = function() {
return self.todos.length > 0; return self.todos.length > 0;
}; };
/*
The following code deals with hiding the hint *while* you are typing,
showing it once you did *finish* typing (aka 500 ms since you hit the last key)
*in case* the result is a non empty string
*/
Rx.Observable.FromAngularScope(self, "newTodo")
.Do(function() {
self.showHitEnterHint = false;
})
.Throttle(500)
.Select(function(x) {
return x.length > 0;
})
.ToOutputProperty(self, "showHitEnterHint");
}; };
...@@ -10,11 +10,24 @@ angular.directive('my:blur', function(expression, compiledElement) { ...@@ -10,11 +10,24 @@ angular.directive('my:blur', function(expression, compiledElement) {
}; };
}); });
angular.directive('my:dblclick', function(expression, compiledElement) {
var compiler = this;
return function(linkElement) {
var scope = this;
linkElement.bind('dblclick', function(event) {
scope.$apply(expression, linkElement);
event.stopPropagation();
});
};
});
angular.directive("my:focus", function(expression, compiledElement){ angular.directive("my:focus", function(expression, compiledElement){
return function(element){ return function(element){
this.$watch(expression, function(){ this.$watch(expression, function(){
if(angular.formatter.boolean.parse(expression)){ if(angular.formatter.boolean.parse(expression)){
element[0].focus(); element[0].focus();
element[0].select();
} }
}, element); }, element);
}; };
......
This diff is collapsed.
(function () {
var global = this,
root = (typeof ProvideCustomRxRootObject == "undefined") ? global.Rx : ProvideCustomRxRootObject();
var observable = root.Observable;
var observableCreate = observable.Create;
observable.FromAngularScope = function (angularScope, propertyName) {
return observableCreate(function (observer) {
var unwatch = angularScope.$watch(function(){
return angularScope[propertyName];
},
function(){
observer.OnNext(angularScope[propertyName]);
});
return function () {
unwatch();
};
})
.Skip(1); //In AngularJS 0.10.x There is no way to avoid initial evaluation. So we take care about it!
};
observable.prototype.ToOutputProperty = function (scope, propertyName) {
var disposable = this.Subscribe(function (data) {
scope[propertyName] = data;
scope.$apply();
});
scope.$on('$destroy', function(event){
//we need to asure that we only dispose the observable when it's our scope that
//was destroyed.
//TODO: Figure out if thats enough to asure the above (e.g what happens when
//a child scope will be destroyed but ours won't be affected. Or the other way around,
//if a higher scope will be destroyed (and therefore ours as well) does it mean that $destroy()
//will be also called on our scope or will our scope get destroyed without actually
//calling $destroy() on it?
if (event.targetScope === scope){
disposable.Dispose();
}
});
};
})();
\ No newline at end of file
This diff is collapsed.
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