Commit fed955b1 authored by Addy Osmani's avatar Addy Osmani

Merge pull request #19 from jacobmumm/master

New Example using AngularJS + PersistenceJS
parents 3049562d 1e6515ff
This diff is collapsed.
<!doctype html>
<html xmlns:ng="http://angularjs.org/" xmlns:my="http://rx.org">
<head>
<meta charset="utf-8">
<title>AngularJS with PersistenceJS Storage Todo App</title>
<link rel="stylesheet" href="css/app.css"/>
</head>
<body>
<div ng:controller="App.Controllers.TodoController" id="todoapp">
<div class="title">
<h1>
Todos
</h1>
</div>
<div class="content">
<div id="todo-form">
</div>
<form id="todo-form" ng:submit="addTodo()">
<input id="new-todo" name="newTodo" my:blur="addTodo()" placeholder="What needs to be done?" type="text">
<span class="ui-tooltip-top" ng:show="showHitEnterHint">
Press Enter to save this task
</span>
</form>
<div id="todos">
<ul id="todo-list">
<li class="todo" ng:class="'editing-' + todo.editing + ' done-' + todo.done" ng:repeat="todo in todos">
<div class="display">
<input ng:change="changeStatus(todo)" class="check" type="checkbox" name="todo.done" / >
<div ng:click="editTodo(todo)" class="todo-content"> {{ todo.content }} </div>
<span class="todo-destroy" ng:click="removeTodo(todo)"></span>
</div>
<div class="edit">
<form ng:submit="finishEditing(todo)">
<input class="todo-input" my:focus="todo.editing" my:blur="finishEditing(todo)" name="todo.content" type="text">
</form>
</div>
</li>
</ul>
</div>
<div id="todo-stats">
<span class="todo-count" ng:show="hasTodos()">
<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>
<ul id="instructions">
<li>Click to edit a todo.</li>
</ul>
<div id="credits">
<p>
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>
<p>
Extended for persistent WebSQL storage by <br/>
<a href="http://jacobmumm.com">Jacob Mumm</a><br/>
Using <a href="http://persistencejs.org">PersistenceJS</a>
</p>
</div>
<script src="js/booter.js"></script>
<script src="lib/angular/angular.min.js" ng:autobind></script>
<script src="lib/rx/rx.js"></script>
<script src="lib/rx/rx.angular.js"></script>
<script src="lib/persistence/persistence.js"></script>
<script src="lib/persistence/persistence.store.sql.js"></script>
<script src="lib/persistence/persistence.store.websql.js"></script>
<script src="js/controllers.js"></script>
<script src="js/directive.js"></script>
<script src="js/services.js"></script>
</body>
</html>
var App = {};
App.Controllers = {};
/* App Controllers */
App.Controllers.TodoController = function (persistencejs) {
var self = this;
self.newTodo = "";
self.editTodoStartContent = "";
self.addTodo = function() {
if (self.newTodo.length === 0) return;
self.todos.push({
content: self.newTodo,
done: false,
editing: false
});
persistencejs.add(self.newTodo);
self.newTodo = "";
};
self.editTodo = function(todo) {
angular.forEach(self.todos, function(value) {
value.editing = false;
});
todo.editing = true;
self.editTodoStartContent = todo.content;
};
self.changeStatus = function(todo){
persistencejs.changeStatus(todo);
};
self.finishEditing = function(todo) {
todo.editing = false;
persistencejs.edit(self.editTodoStartContent, todo.content);
};
self.removeTodo = function(todo) {
angular.Array.remove(self.todos, todo);
persistencejs.remove(todo);
};
self.todos = [];
var countTodos = function(done) {
return function() {
return angular.Array.count(self.todos, function(x) {
return x.done === (done === "done");
});
}
};
self.remainingTodos = countTodos("undone");
self.finishedTodos = countTodos("done");
self.clearCompletedItems = function() {
var oldTodos = self.todos;
self.todos = [];
angular.forEach(oldTodos, function(todo) {
if (!todo.done) self.todos.push(todo);
});
};
self.hasFinishedTodos = function() {
return self.finishedTodos() > 0;
};
self.hasTodos = function() {
return self.todos.length > 0;
};
self.loadTodos = function(){
persistencejs.fetchAll(self);
}
self.refresh = function(){ self.$apply(); }
self.loadTodos();
/*
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");
};
App.Controllers.TodoController.$inject = ['persistencejs'];
\ No newline at end of file
angular.directive('my:blur', function(expression, compiledElement) {
var compiler = this;
return function(linkElement) {
var scope = this;
linkElement.bind('blur', function(event) {
scope.$apply(expression, linkElement);
event.stopPropagation();
});
};
});
angular.directive("my:focus", function(expression, compiledElement){
return function(element){
this.$watch(expression, function(){
if(angular.formatter.boolean.parse(expression)){
element[0].focus();
}
}, element);
};
});
angular.service('persistencejs', function() {
persistence.store.websql.config(persistence, 'todo', 'todo database', 5*1024*1024);
var Todo = persistence.define('todo', {
content: 'TEXT',
done: 'BOOL'
});
persistence.schemaSync();
return {
add: function(item){
var t = new Todo();
t.content = item;
t.done = false;
persistence.add(t);
persistence.flush();
},
edit: function(startContent, endContent){
Todo.all().filter('content','=',startContent).one(function(item){
item.content = endContent;
persistence.flush();
});
},
changeStatus: function(item){
Todo.all().filter('content','=',item.content).one(function(todo){
todo.done = item.done;
persistence.flush();
});
},
remove: function(item){
Todo.all().filter('content','=',item.content).destroyAll();
},
fetchAll: function(controller){
Todo.all().list(function(items){
var itemCount = items.length;
var todos = [];
items.forEach(function(item){
todos.push({
content: item.content,
done: item.done,
editing: false
});
if(--itemCount == 0){
controller.todos = todos;
controller.refresh();
}
});
});
},
};
});
\ No newline at end of file
/*
Content-Type: multipart/related; boundary="_"
--_
Content-Location:img0
Content-Transfer-Encoding:base64
R0lGODlhCwAXAKIAAMzMzO/v7/f39////////wAAAAAAAAAAACH5BAUUAAQALAAAAAALABcAAAMrSLoc/AG8FeUUIN+sGebWAnbKSJodqqlsOxJtqYooU9vvk+vcJIcTkg+QAAA7
--_
Content-Location:img1
Content-Transfer-Encoding:base64
R0lGODlhCwAXAKIAAMzMzO/v7/f39////////wAAAAAAAAAAACH5BAUUAAQALAAAAAALABcAAAMrCLTcoM29yN6k9socs91e5X3EyJloipYrO4ohTMqA0Fn2XVNswJe+H+SXAAA7
--_
Content-Location:img2
Content-Transfer-Encoding:base64
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
--_--
*/
(function(){
var jsUri = document.location.href.replace(/\/[^\/]+(#.*)?$/, '/') +
document.getElementById('ng-ie-compat').src,
css = '#ng-callout .ng-arrow-left{*background-image:url("mhtml:' + jsUri + '!img0")}#ng-callout .ng-arrow-right{*background-image:url("mhtml:' + jsUri + '!img1")}.ng-input-indicator-wait {*background-image:url("mhtml:' + jsUri + '!img2")}',
s = document.createElement('style');
s.setAttribute('type', 'text/css');
if (s.styleSheet) {
s.styleSheet.cssText = css;
} else {
s.appendChild(document.createTextNode(css));
}
document.getElementsByTagName('head')[0].appendChild(s);
})();
This diff is collapsed.
try {
if(!window) {
window = {};
//exports.console = console;
}
} catch(e) {
window = {};
exports.console = console;
}
var persistence = (window && window.persistence) ? window.persistence : {};
if(!persistence.store) {
persistence.store = {};
}
persistence.store.websql = {};
persistence.store.websql.config = function(persistence, dbname, description, size) {
var conn = null;
/**
* Create a transaction
*
* @param callback,
* the callback function to be invoked when the transaction
* starts, taking the transaction object as argument
*/
persistence.transaction = function (callback) {
if(!conn) {
throw new Error("No ongoing database connection, please connect first.");
} else {
conn.transaction(callback);
}
};
////////// Low-level database interface, abstracting from HTML5 and Gears databases \\\\
persistence.db = persistence.db || {};
persistence.db.implementation = "unsupported";
persistence.db.conn = null;
// window object does not exist on Qt Declarative UI (http://doc.trolltech.org/4.7-snapshot/declarativeui.html)
if (window && window.openDatabase) {
persistence.db.implementation = "html5";
} else if (window && window.google && google.gears) {
persistence.db.implementation = "gears";
} else {
try {
if (openDatabaseSync) {
// TODO: find a browser that implements openDatabaseSync and check out if
// it is attached to the window or some other object
persistence.db.implementation = "html5-sync";
}
} catch(e) {
}
}
persistence.db.html5 = {};
persistence.db.html5.connect = function (dbname, description, size) {
var that = {};
var conn = openDatabase(dbname, '1.0', description, size);
that.transaction = function (fn) {
return conn.transaction(function (sqlt) {
return fn(persistence.db.html5.transaction(sqlt));
});
};
return that;
};
persistence.db.html5.transaction = function (t) {
var that = {};
that.executeSql = function (query, args, successFn, errorFn) {
if(persistence.debug) {
console.log(query, args);
}
t.executeSql(query, args, function (_, result) {
if (successFn) {
var results = [];
for ( var i = 0; i < result.rows.length; i++) {
results.push(result.rows.item(i));
}
successFn(results);
}
}, errorFn);
};
return that;
};
persistence.db.html5Sync = {};
persistence.db.html5Sync.connect = function (dbname, description, size) {
var that = {};
var conn = openDatabaseSync(dbname, '1.0', description, size);
that.transaction = function (fn) {
return conn.transaction(function (sqlt) {
return fn(persistence.db.html5Sync.transaction(sqlt));
});
};
return that;
};
persistence.db.html5Sync.transaction = function (t) {
var that = {};
that.executeSql = function (query, args, successFn, errorFn) {
if (args == null) args = [];
if(persistence.debug) {
console.log(query, args);
}
var result = t.executeSql(query, args);
if (result) {
if (successFn) {
var results = [];
for ( var i = 0; i < result.rows.length; i++) {
results.push(result.rows.item(i));
}
successFn(results);
}
}
};
return that;
};
persistence.db.gears = {};
persistence.db.gears.connect = function (dbname) {
var that = {};
var conn = google.gears.factory.create('beta.database');
conn.open(dbname);
that.transaction = function (fn) {
fn(persistence.db.gears.transaction(conn));
};
return that;
};
persistence.db.gears.transaction = function (conn) {
var that = {};
that.executeSql = function (query, args, successFn, errorFn) {
if(persistence.debug) {
console.log(query, args);
}
var rs = conn.execute(query, args);
if (successFn) {
var results = [];
while (rs.isValidRow()) {
var result = {};
for ( var i = 0; i < rs.fieldCount(); i++) {
result[rs.fieldName(i)] = rs.field(i);
}
results.push(result);
rs.next();
}
successFn(results);
}
};
return that;
};
persistence.db.connect = function (dbname, description, size) {
if (persistence.db.implementation == "html5") {
return persistence.db.html5.connect(dbname, description, size);
} else if (persistence.db.implementation == "html5-sync") {
return persistence.db.html5Sync.connect(dbname, description, size);
} else if (persistence.db.implementation == "gears") {
return persistence.db.gears.connect(dbname);
}
};
///////////////////////// SQLite dialect
persistence.store.websql.sqliteDialect = {
// columns is an array of arrays, e.g.
// [["id", "VARCHAR(32)", "PRIMARY KEY"], ["name", "TEXT"]]
createTable: function(tableName, columns) {
var tm = persistence.typeMapper;
var sql = "CREATE TABLE IF NOT EXISTS `" + tableName + "` (";
var defs = [];
for(var i = 0; i < columns.length; i++) {
var column = columns[i];
defs.push("`" + column[0] + "` " + tm.columnType(column[1]) + (column[2] ? " " + column[2] : ""));
}
sql += defs.join(", ");
sql += ')';
return sql;
},
// columns is array of column names, e.g.
// ["id"]
createIndex: function(tableName, columns, options) {
options = options || {};
return "CREATE "+(options.unique?"UNIQUE ":"")+"INDEX IF NOT EXISTS `" + tableName + "__" + columns.join("_") +
"` ON `" + tableName + "` (" +
columns.map(function(col) { return "`" + col + "`"; }).join(", ") + ")";
}
};
// Configure persistence for generic sql persistence, using sqliteDialect
persistence.store.sql.config(persistence, persistence.store.websql.sqliteDialect);
// Make the connection
conn = persistence.db.connect(dbname, description, size);
if(!conn) {
throw new Error("No supported database found in this browser.");
}
};
try {
exports.persistence = persistence;
} catch(e) {}
(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