Commit 421fcbaa authored by Sindre Sorhus's avatar Sindre Sorhus

drop `agility` app - #1173

parent 5ff915aa
......@@ -82,7 +82,6 @@ module.exports = [
'TodoMVC - flight, Routing, should respect the back button',
'TodoMVC - lavaca_require, Routing, should respect the back button',
'TodoMVC - somajs_require, Routing, should respect the back button',
'TodoMVC - agilityjs, Routing, should respect the back button',
'TodoMVC - maria, Routing, should respect the back button',
// the following are covered by this issue:
......
{
"name": "agilityjs",
"version": "0.0.0",
"dependencies": {
"todomvc-common": "~0.3.0",
"agility": "~0.1.3",
"jquery": "~1.9.1"
}
}
/*
Agility.js
Licensed under the MIT license
Copyright (c) Artur B. Adib, 2011
http://agilityjs.com
*/
// Sandboxed, so kids don't get hurt. Inspired by jQuery's code:
// Creates local ref to window for performance reasons (as JS looks up local vars first)
// Redefines undefined as it could have been tampered with
(function(window, undefined){
if (!window.jQuery) {
throw "agility.js: jQuery not found";
}
// Local references
var document = window.document,
location = window.location,
// In case $ is being used by another lib
$ = jQuery,
// Main agility object builder
agility,
// Internal utility functions
util = {},
// Default object prototype
defaultPrototype = {},
// Global object counter
idCounter = 0,
// Constant
ROOT_SELECTOR = '&';
//////////////////////////////////////////////////////////////////////////
//
// Modernizing old JS
//
// Modified from Douglas Crockford's Object.create()
// The condition below ensures we override other manual implementations (most are not adequate)
if (!Object.create || Object.create.toString().search(/native code/i)<0) {
Object.create = function(obj){
var Aux = function(){};
$.extend(Aux.prototype, obj); // simply setting Aux.prototype = obj somehow messes with constructor, so getPrototypeOf wouldn't work in IE
return new Aux();
};
}
// Modified from John Resig's Object.getPrototypeOf()
// The condition below ensures we override other manual implementations (most are not adequate)
if (!Object.getPrototypeOf || Object.getPrototypeOf.toString().search(/native code/i)<0) {
if ( typeof "test".__proto__ === "object" ) {
Object.getPrototypeOf = function(object){
return object.__proto__;
};
} else {
Object.getPrototypeOf = function(object){
// May break if the constructor has been tampered with
return object.constructor.prototype;
};
}
}
//////////////////////////////////////////////////////////////////////////
//
// util.*
//
// Checks if provided obj is an agility object
util.isAgility = function(obj){
return obj._agility === true;
};
// Scans object for functions (depth=2) and proxies their 'this' to dest.
// * To ensure it works with previously proxied objects, we save the original function as
// a '._preProxy' method and when available always use that as the proxy source.
// * To skip a given method, create a sub-method called '_noProxy'.
util.proxyAll = function(obj, dest){
if (!obj || !dest) {
throw "agility.js: util.proxyAll needs two arguments";
}
for (var attr1 in obj) {
var proxied = obj[attr1];
// Proxy root methods
if (typeof obj[attr1] === 'function') {
proxied = obj[attr1]._noProxy ? obj[attr1] : $.proxy(obj[attr1]._preProxy || obj[attr1], dest);
proxied._preProxy = obj[attr1]._noProxy ? undefined : (obj[attr1]._preProxy || obj[attr1]); // save original
obj[attr1] = proxied;
}
// Proxy sub-methods (model.*, view.*, etc)
else if (typeof obj[attr1] === 'object') {
for (var attr2 in obj[attr1]) {
var proxied2 = obj[attr1][attr2];
if (typeof obj[attr1][attr2] === 'function') {
proxied2 = obj[attr1][attr2]._noProxy ? obj[attr1][attr2] : $.proxy(obj[attr1][attr2]._preProxy || obj[attr1][attr2], dest);
proxied2._preProxy = obj[attr1][attr2]._noProxy ? undefined : (obj[attr1][attr2]._preProxy || obj[attr1][attr2]); // save original
proxied[attr2] = proxied2;
}
} // for attr2
obj[attr1] = proxied;
} // if not func
} // for attr1
}; // proxyAll
// Reverses the order of events attached to an object
util.reverseEvents = function(obj, eventType){
var events = $(obj).data('events');
if (events !== undefined && events[eventType] !== undefined){
// can't reverse what's not there
var reverseEvents = [];
for (var e in events[eventType]){
if (!events[eventType].hasOwnProperty(e)) continue;
reverseEvents.unshift(events[eventType][e]);
}
events[eventType] = reverseEvents;
}
}; //reverseEvents
// Determines # of attributes of given object (prototype inclusive)
util.size = function(obj){
var size = 0, key;
for (key in obj) {
size++;
}
return size;
};
// Find controllers to be extended (with syntax '~'), redefine those to encompass previously defined controllers
// Example:
// var a = $$({}, '<button>A</button>', {'click &': function(){ alert('A'); }});
// var b = $$(a, {}, '<button>B</button>', {'~click &': function(){ alert('B'); }});
// Clicking on button B will alert both 'A' and 'B'.
util.extendController = function(object) {
for (var controllerName in object.controller) {
(function(){ // new scope as we need one new function handler per controller
var matches, extend, eventName,
previousHandler, currentHandler, newHandler;
if (typeof object.controller[controllerName] === 'function') {
matches = controllerName.match(/^(\~)*(.+)/); // 'click button', '~click button', '_create', etc
extend = matches[1];
eventName = matches[2];
if (!extend) return; // nothing to do
// Redefine controller:
// '~click button' ---> 'click button' = previousHandler + currentHandler
previousHandler = object.controller[eventName] ? (object.controller[eventName]._preProxy || object.controller[eventName]) : undefined;
currentHandler = object.controller[controllerName];
newHandler = function() {
if (previousHandler) previousHandler.apply(this, arguments);
if (currentHandler) currentHandler.apply(this, arguments);
};
object.controller[eventName] = newHandler;
delete object.controller[controllerName]; // delete '~click button'
} // if function
})();
} // for controllerName
};
//////////////////////////////////////////////////////////////////////////
//
// Default object prototype
//
defaultPrototype = {
_agility: true,
//////////////////////////////////////////////////////////////////////////
//
// _container
//
// API and related auxiliary functions for storing child Agility objects.
// Not all methods are exposed. See 'shortcuts' below for exposed methods.
//
_container: {
// Adds child object to container, appends/prepends/etc view, listens for child removal
_insertObject: function(obj, selector, method){
var self = this;
if (!util.isAgility(obj)) {
throw "agility.js: append argument is not an agility object";
}
this._container.children[obj._id] = obj; // children is *not* an array; this is for simpler lookups by global object id
this.trigger(method, [obj, selector]);
obj._parent = this;
// ensures object is removed from container when destroyed:
obj.bind('destroy', function(event, id){
self._container.remove(id);
});
return this;
},
append: function(obj, selector) {
return this._container._insertObject.call(this, obj, selector, 'append');
},
prepend: function(obj, selector) {
return this._container._insertObject.call(this, obj, selector, 'prepend');
},
after: function(obj, selector) {
return this._container._insertObject.call(this, obj, selector, 'after');
},
before: function(obj, selector) {
return this._container._insertObject.call(this, obj, selector, 'before');
},
// Removes child object from container
remove: function(id){
delete this._container.children[id];
this.trigger('remove', id);
return this;
},
// Iterates over all child objects in container
each: function(fn){
$.each(this._container.children, fn);
return this; // for chainable calls
},
// Removes all objects in container
empty: function(){
this.each(function(){
this.destroy();
});
return this;
},
// Number of children
size: function() {
return util.size(this._container.children);
}
},
//////////////////////////////////////////////////////////////////////////
//
// _events
//
// API and auxiliary functions for handling events. Not all methods are exposed.
// See 'shortcuts' below for exposed methods.
//
_events: {
// Parses event string like:
// 'event' : custom event
// 'event selector' : DOM event using 'selector'
// Returns { type:'event' [, selector:'selector'] }
parseEventStr: function(eventStr){
var eventObj = { type:eventStr },
spacePos = eventStr.search(/\s/);
// DOM event 'event selector', e.g. 'click button'
if (spacePos > -1) {
eventObj.type = eventStr.substr(0, spacePos);
eventObj.selector = eventStr.substr(spacePos+1);
}
return eventObj;
},
// Binds eventStr to fn. eventStr is parsed as per parseEventStr()
bind: function(eventStr, fn){
var eventObj = this._events.parseEventStr(eventStr);
// DOM event 'event selector', e.g. 'click button'
if (eventObj.selector) {
// Manually override root selector, as jQuery selectors can't select self object
if (eventObj.selector === ROOT_SELECTOR) {
this.view.$().bind(eventObj.type, fn);
}
else {
this.view.$().delegate(eventObj.selector, eventObj.type, fn);
}
}
// Custom event
else {
$(this._events.data).bind(eventObj.type, fn);
}
return this; // for chainable calls
}, // bind
// Triggers eventStr. Syntax for eventStr is same as that for bind()
trigger: function(eventStr, params){
var eventObj = this._events.parseEventStr(eventStr);
// DOM event 'event selector', e.g. 'click button'
if (eventObj.selector) {
// Manually override root selector, as jQuery selectors can't select self object
if (eventObj.selector === ROOT_SELECTOR) {
this.view.$().trigger(eventObj.type, params);
}
else {
this.view.$().find(eventObj.selector).trigger(eventObj.type, params);
}
}
// Custom event
else {
$(this._events.data).trigger('_'+eventObj.type, params);
// fire 'pre' hooks in reverse attachment order ( last first )
util.reverseEvents(this._events.data, 'pre:' + eventObj.type);
$(this._events.data).trigger('pre:' + eventObj.type, params);
// put the order of events back
util.reverseEvents(this._events.data, 'pre:' + eventObj.type);
$(this._events.data).trigger(eventObj.type, params);
if(this.parent())
this.parent().trigger((eventObj.type.match(/^child:/) ? '' : 'child:') + eventObj.type, params);
$(this._events.data).trigger('post:' + eventObj.type, params);
}
return this; // for chainable calls
} // trigger
}, // _events
//////////////////////////////////////////////////////////////////////////
//
// Model
//
// Main model API. All methods are exposed, but methods starting with '_'
// are meant to be used internally only.
//
model: {
// Setter
set: function(arg, params) {
var self = this;
var modified = []; // list of modified model attributes
if (typeof arg === 'object') {
var _clone = false;
if (params && params.reset) {
_clone = this.model._data; // hold on to data for change events
this.model._data = $.extend({}, arg); // erases previous model attributes without pointing to object
}
else {
$.extend(this.model._data, arg); // default is extend
}
for (var key in arg) {
delete _clone[ key ]; // no need to fire change twice
modified.push(key);
}
for (key in _clone) {
modified.push(key);
}
}
else {
throw "agility.js: unknown argument type in model.set()";
}
// Events
if (params && params.silent===true) return this; // do not fire events
this.trigger('change');
$.each(modified, function(index, val){
self.trigger('change:'+val);
});
return this; // for chainable calls
},
// Getter
get: function(arg){
// Full model getter
if (arg === undefined) {
return this.model._data;
}
// Attribute getter
if (typeof arg === 'string') {
return this.model._data[arg];
}
throw 'agility.js: unknown argument for getter';
},
// Resetter (to initial model upon object initialization)
reset: function(){
this.model.set(this.model._initData, {reset:true});
return this; // for chainable calls
},
// Number of model properties
size: function(){
return util.size(this.model._data);
},
// Convenience function - loops over each model property
each: function(fn){
$.each(this.model._data, fn);
return this; // for chainable calls
}
}, // model prototype
//////////////////////////////////////////////////////////////////////////
//
// View
//
// Main view API. All methods are exposed, but methods starting with '_'
// are meant to be used internally only.
//
view: {
// Defaults
format: '<div/>',
style: '',
// Shortcut to view.$root or view.$root.find(), depending on selector presence
$: function(selector){
return (!selector || selector === ROOT_SELECTOR) ? this.view.$root : this.view.$root.find(selector);
},
// Render $root
// Only function to access $root directly other than $()
render: function(){
// Without format there is no view
if (this.view.format.length === 0) {
throw "agility.js: empty format in view.render()";
}
if (this.view.$root.size() === 0) {
this.view.$root = $(this.view.format);
}
else {
this.view.$root.html( $(this.view.format).html() ); // can't overwrite $root as this would reset its presence in the DOM and all events already bound, and
}
// Ensure we have a valid (non-empty) $root
if (this.view.$root.size() === 0) {
throw 'agility.js: could not generate html from format';
}
return this;
}, // render
// Parse data-bind string of the type '[attribute][=] variable[, [attribute][=] variable ]...'
// If the variable is not an attribute, it must occur by itself
// all pairs in the list are assumed to be attributes
// Returns { key:'model key', attr: [ {attr : 'attribute', attrVar : 'variable' }... ] }
_parseBindStr: function(str){
var obj = {key:null, attr:[]},
pairs = str.split(','),
regex = /([a-zA-Z0-9_\-]+)(?:[\s=]+([a-zA-Z0-9_\-]+))?/,
keyAssigned = false,
matched;
if (pairs.length > 0) {
for (var i = 0; i < pairs.length; i++) {
matched = pairs[i].match(regex);
// [ "attribute variable", "attribute", "variable" ]
// or [ "attribute=variable", "attribute", "variable" ]
// or
// [ "variable", "variable", undefined ]
// in some IE it will be [ "variable", "variable", "" ]
// or
// null
if (matched) {
if (typeof(matched[2]) === "undefined" || matched[2] === "") {
if (keyAssigned) {
throw new Error("You may specify only one key (" +
keyAssigned + " has already been specified in data-bind=" +
str + ")");
} else {
keyAssigned = matched[1];
obj.key = matched[1];
}
} else {
obj.attr.push({attr: matched[1], attrVar: matched[2]});
}
} // if (matched)
} // for (pairs.length)
} // if (pairs.length > 0)
return obj;
},
// Apply two-way (DOM <--> Model) bindings to elements with 'data-bind' attributes
bindings: function(){
var self = this;
var $rootNode = this.view.$().filter('[data-bind]');
var $childNodes = this.view.$('[data-bind]');
var createAttributePairClosure = function(bindData, node, i) {
var attrPair = bindData.attr[i]; // capture the attribute pair in closure
return function() {
node.attr(attrPair.attr, self.model.get(attrPair.attrVar));
};
};
$rootNode.add($childNodes).each(function(){
var $node = $(this);
var bindData = self.view._parseBindStr( $node.data('bind') );
var bindAttributesOneWay = function() {
// 1-way attribute binding
if (bindData.attr) {
for (var i = 0; i < bindData.attr.length; i++) {
self.bind('_change:'+bindData.attr[i].attrVar,
createAttributePairClosure(bindData, $node, i));
} // for (bindData.attr)
} // if (bindData.attr)
}; // bindAttributesOneWay()
// <input type="checkbox">: 2-way binding
if ($node.is('input:checkbox')) {
// Model --> DOM
self.bind('_change:'+bindData.key, function(){
$node.prop("checked", self.model.get(bindData.key)); // this won't fire a DOM 'change' event, saving us from an infinite event loop (Model <--> DOM)
});
// DOM --> Model
$node.change(function(){
var obj = {};
obj[bindData.key] = $(this).prop("checked");
self.model.set(obj); // not silent as user might be listening to change events
});
// 1-way attribute binding
bindAttributesOneWay();
}
// <select>: 2-way binding
else if ($node.is('select')) {
// Model --> DOM
self.bind('_change:'+bindData.key, function(){
var nodeName = $node.attr('name');
var modelValue = self.model.get(bindData.key);
$node.val(modelValue);
});
// DOM --> Model
$node.change(function(){
var obj = {};
obj[bindData.key] = $node.val();
self.model.set(obj); // not silent as user might be listening to change events
});
// 1-way attribute binding
bindAttributesOneWay();
}
// <input type="radio">: 2-way binding
else if ($node.is('input:radio')) {
// Model --> DOM
self.bind('_change:'+bindData.key, function(){
var nodeName = $node.attr('name');
var modelValue = self.model.get(bindData.key);
$node.siblings('input[name="'+nodeName+'"]').filter('[value="'+modelValue+'"]').prop("checked", true); // this won't fire a DOM 'change' event, saving us from an infinite event loop (Model <--> DOM)
});
// DOM --> Model
$node.change(function(){
if (!$node.prop("checked")) return; // only handles check=true events
var obj = {};
obj[bindData.key] = $node.val();
self.model.set(obj); // not silent as user might be listening to change events
});
// 1-way attribute binding
bindAttributesOneWay();
}
// <input type="search"> (model is updated after every keypress event)
else if ($node.is('input[type="search"]')) {
// Model --> DOM
self.bind('_change:'+bindData.key, function(){
$node.val(self.model.get(bindData.key)); // this won't fire a DOM 'change' event, saving us from an infinite event loop (Model <--> DOM)
});
// Model <-- DOM
$node.keypress(function(){
// Without timeout $node.val() misses the last entered character
setTimeout(function(){
var obj = {};
obj[bindData.key] = $node.val();
self.model.set(obj); // not silent as user might be listening to change events
}, 50);
});
// 1-way attribute binding
bindAttributesOneWay();
}
// <input type="text">, <input>, and <textarea>: 2-way binding
else if ($node.is('input:text, textarea')) {
// Model --> DOM
self.bind('_change:'+bindData.key, function(){
$node.val(self.model.get(bindData.key)); // this won't fire a DOM 'change' event, saving us from an infinite event loop (Model <--> DOM)
});
// Model <-- DOM
$node.change(function(){
var obj = {};
obj[bindData.key] = $(this).val();
self.model.set(obj); // not silent as user might be listening to change events
});
// 1-way attribute binding
bindAttributesOneWay();
}
// all other <tag>s: 1-way binding
else {
if (bindData.key) {
self.bind('_change:'+bindData.key, function(){
if (self.model.get(bindData.key)) {
$node.text(self.model.get(bindData.key).toString());
} else {
$node.text('');
}
});
}
bindAttributesOneWay();
}
}); // nodes.each()
return this;
}, // bindings()
// Triggers _change and _change:* events so that view is updated as per view.bindings()
sync: function(){
var self = this;
// Trigger change events so that view is updated according to model
this.model.each(function(key, val){
self.trigger('_change:'+key);
});
if (this.model.size() > 0) {
this.trigger('_change');
}
return this;
},
// Applies style dynamically
stylize: function(){
var objClass,
regex = new RegExp(ROOT_SELECTOR, 'g');
if (this.view.style.length === 0 || this.view.$().size() === 0) {
return;
}
// Own style
// Object gets own class name ".agility_123", and <head> gets a corresponding <style>
if (this.view.hasOwnProperty('style')) {
objClass = 'agility_' + this._id;
var styleStr = this.view.style.replace(regex, '.'+objClass);
$('head', window.document).append('<style type="text/css">'+styleStr+'</style>');
this.view.$().addClass(objClass);
}
// Inherited style
// Object inherits CSS class name from first ancestor to have own view.style
else {
// Returns id of first ancestor to have 'own' view.style
var ancestorWithStyle = function(object) {
while (object !== null) {
object = Object.getPrototypeOf(object);
if (object.view.hasOwnProperty('style'))
return object._id;
}
return undefined;
}; // ancestorWithStyle
var ancestorId = ancestorWithStyle(this);
objClass = 'agility_' + ancestorId;
this.view.$().addClass(objClass);
}
return this;
}
}, // view prototype
//////////////////////////////////////////////////////////////////////////
//
// Controller
//
// Default controllers, i.e. event handlers. Event handlers that start
// with '_' are of internal use only, and take precedence over any other
// handler without that prefix. (See trigger()).
//
controller: {
// Triggered after self creation
_create: function(event){
this.view.stylize();
this.view.bindings(); // Model-View bindings
this.view.sync(); // syncs View with Model
},
// Triggered upon removing self
_destroy: function(event){
// destroy any appended agility objects
this._container.empty();
// destroy self
this.view.$().remove();
},
// Triggered after child obj is appended to container
_append: function(event, obj, selector){
this.view.$(selector).append(obj.view.$());
},
// Triggered after child obj is prepended to container
_prepend: function(event, obj, selector){
this.view.$(selector).prepend(obj.view.$());
},
// Triggered after child obj is inserted in the container
_before: function(event, obj, selector){
if (!selector) throw 'agility.js: _before needs a selector';
this.view.$(selector).before(obj.view.$());
},
// Triggered after child obj is inserted in the container
_after: function(event, obj, selector){
if (!selector) throw 'agility.js: _after needs a selector';
this.view.$(selector).after(obj.view.$());
},
// Triggered after a child obj is removed from container (or self-removed)
_remove: function(event, id){
},
// Triggered after model is changed
'_change': function(event){
}
}, // controller prototype
//////////////////////////////////////////////////////////////////////////
//
// Shortcuts
//
//
// Self
//
destroy: function() {
this.trigger('destroy', this._id); // parent must listen to 'remove' event and handle container removal!
// can't return this as it might not exist anymore!
},
parent: function(){
return this._parent;
},
//
// _container shortcuts
//
append: function(){
this._container.append.apply(this, arguments);
return this; // for chainable calls
},
prepend: function(){
this._container.prepend.apply(this, arguments);
return this; // for chainable calls
},
after: function(){
this._container.after.apply(this, arguments);
return this; // for chainable calls
},
before: function(){
this._container.before.apply(this, arguments);
return this; // for chainable calls
},
remove: function(){
this._container.remove.apply(this, arguments);
return this; // for chainable calls
},
size: function(){
return this._container.size.apply(this, arguments);
},
each: function(){
return this._container.each.apply(this, arguments);
},
empty: function(){
return this._container.empty.apply(this, arguments);
},
//
// _events shortcuts
//
bind: function(){
this._events.bind.apply(this, arguments);
return this; // for chainable calls
},
trigger: function(){
this._events.trigger.apply(this, arguments);
return this; // for chainable calls
}
}; // prototype
//////////////////////////////////////////////////////////////////////////
//
// Main object builder
//
// Main agility object builder
agility = function(){
// Real array of arguments
var args = Array.prototype.slice.call(arguments, 0),
// Object to be returned by builder
object = {},
prototype = defaultPrototype;
//////////////////////////////////////////////////////////////////////////
//
// Define object prototype
//
// Inherit object prototype
if (typeof args[0] === "object" && util.isAgility(args[0])) {
prototype = args[0];
args.shift(); // remaining args now work as though object wasn't specified
} // build from agility object
// Build object from prototype as well as the individual prototype parts
// This enables differential inheritance at the sub-object level, e.g. object.view.format
object = Object.create(prototype);
object.model = Object.create(prototype.model);
object.view = Object.create(prototype.view);
object.controller = Object.create(prototype.controller);
object._container = Object.create(prototype._container);
object._events = Object.create(prototype._events);
// Fresh 'own' properties (i.e. properties that are not inherited at all)
object._id = idCounter++;
object._parent = null;
object._events.data = {}; // event bindings will happen below
object._container.children = {};
object.view.$root = $(); // empty jQuery object
// Cloned own properties (i.e. properties that are inherited by direct copy instead of by prototype chain)
// This prevents children from altering parents models
object.model._data = prototype.model._data ? $.extend(true, {}, prototype.model._data) : {};
object._data = prototype._data ? $.extend(true, {}, prototype._data) : {};
//////////////////////////////////////////////////////////////////////////
//
// Extend model, view, controller
//
// Just the default prototype
if (args.length === 0) {
}
// Prototype differential from single {model,view,controller} object
else if (args.length === 1 && typeof args[0] === 'object' && (args[0].model || args[0].view || args[0].controller) ) {
for (var prop in args[0]) {
if (prop === 'model') {
$.extend(object.model._data, args[0].model);
}
else if (prop === 'view') {
$.extend(object.view, args[0].view);
}
else if (prop === 'controller') {
$.extend(object.controller, args[0].controller);
util.extendController(object);
}
// User-defined methods
else {
object[prop] = args[0][prop];
}
}
} // {model, view, controller} arg
// Prototype differential from separate {model}, {view}, {controller} arguments
else {
// Model from string
if (typeof args[0] === 'object') {
$.extend(object.model._data, args[0]);
}
else if (args[0]) {
throw "agility.js: unknown argument type (model)";
}
// View format from shorthand string (..., '<div>whatever</div>', ...)
if (typeof args[1] === 'string') {
object.view.format = args[1]; // extend view with .format
}
// View from object (..., {format:'<div>whatever</div>'}, ...)
else if (typeof args[1] === 'object') {
$.extend(object.view, args[1]);
}
else if (args[1]) {
throw "agility.js: unknown argument type (view)";
}
// View style from shorthand string (..., ..., 'p {color:red}', ...)
if (typeof args[2] === 'string') {
object.view.style = args[2];
args.splice(2, 1); // so that controller code below works
}
// Controller from object (..., ..., {method:function(){}})
if (typeof args[2] === 'object') {
$.extend(object.controller, args[2]);
util.extendController(object);
}
else if (args[2]) {
throw "agility.js: unknown argument type (controller)";
}
} // ({model}, {view}, {controller}) args
//////////////////////////////////////////////////////////////////////////
//
// Bootstrap: Bindings, initializations, etc
//
// Save model's initial state (so it can be .reset() later)
object.model._initData = $.extend({}, object.model._data);
// object.* will have their 'this' === object. This should come before call to object.* below.
util.proxyAll(object, object);
// Initialize $root, needed for DOM events binding below
object.view.render();
// Bind all controllers to their events
var bindEvent = function(ev, handler){
if (typeof handler === 'function') {
object.bind(ev, handler);
}
};
for (var eventStr in object.controller) {
var events = eventStr.split(';');
var handler = object.controller[eventStr];
$.each(events, function(i, ev){
ev = ev.trim();
bindEvent(ev, handler);
});
}
// Auto-triggers create event
object.trigger('create');
return object;
}; // agility
//////////////////////////////////////////////////////////////////////////
//
// Global objects
//
// $$.document is a special Agility object, whose view is attached to <body>
// This object is the main entry point for all DOM operations
agility.document = agility({
view: {
$: function(selector){ return selector ? $(selector, 'body') : $('body'); }
},
controller: {
// Override default controller
// (don't render, don't stylize, etc)
_create: function(){}
}
});
// Shortcut to prototype for plugins
agility.fn = defaultPrototype;
// isAgility test
agility.isAgility = function(obj) {
if (typeof obj !== 'object') return false;
return util.isAgility(obj);
};
// Globals
window.agility = window.$$ = agility;
//////////////////////////////////////////////////////////////////////////
//
// Bundled plugin: persist
//
// Main initializer
agility.fn.persist = function(adapter, params){
var id = 'id'; // name of id attribute
this._data.persist = $.extend({adapter:adapter}, params);
this._data.persist.openRequests = 0;
if (params && params.id) {
id = params.id;
}
// Creates persist methods
// .save()
// Creates new model or update existing one, depending on whether model has 'id' property
this.save = function(){
var self = this;
if (this._data.persist.openRequests === 0) {
this.trigger('persist:start');
}
this._data.persist.openRequests++;
this._data.persist.adapter.call(this, {
type: this.model.get(id) ? 'PUT' : 'POST', // update vs. create
id: this.model.get(id),
data: this.model.get(),
complete: function(){
self._data.persist.openRequests--;
if (self._data.persist.openRequests === 0) {
self.trigger('persist:stop');
}
},
success: function(data, textStatus, jqXHR){
if (data[id]) {
// id in body
self.model.set({id:data[id]}, {silent:true});
}
else if (jqXHR.getResponseHeader('Location')) {
// parse id from Location
self.model.set({ id: jqXHR.getResponseHeader('Location').match(/\/([0-9]+)$/)[1] }, {silent:true});
}
self.trigger('persist:save:success');
},
error: function(){
self.trigger('persist:error');
self.trigger('persist:save:error');
}
});
return this; // for chainable calls
}; // save()
// .load()
// Loads model with given id
this.load = function(){
var self = this;
if (this.model.get(id) === undefined) throw 'agility.js: load() needs model id';
if (this._data.persist.openRequests === 0) {
this.trigger('persist:start');
}
this._data.persist.openRequests++;
this._data.persist.adapter.call(this, {
type: 'GET',
id: this.model.get(id),
complete: function(){
self._data.persist.openRequests--;
if (self._data.persist.openRequests === 0) {
self.trigger('persist:stop');
}
},
success: function(data, textStatus, jqXHR){
self.model.set(data);
self.trigger('persist:load:success');
},
error: function(){
self.trigger('persist:error');
self.trigger('persist:load:error');
}
});
return this; // for chainable calls
}; // load()
// .erase()
// Erases model with given id
this.erase = function(){
var self = this;
if (this.model.get(id) === undefined) throw 'agility.js: erase() needs model id';
if (this._data.persist.openRequests === 0) {
this.trigger('persist:start');
}
this._data.persist.openRequests++;
this._data.persist.adapter.call(this, {
type: 'DELETE',
id: this.model.get(id),
complete: function(){
self._data.persist.openRequests--;
if (self._data.persist.openRequests === 0) {
self.trigger('persist:stop');
}
},
success: function(data, textStatus, jqXHR){
self.destroy();
self.trigger('persist:erase:success');
},
error: function(){
self.trigger('persist:error');
self.trigger('persist:erase:error');
}
});
return this; // for chainable calls
}; // erase()
// .gather()
// Loads collection and appends/prepends (depending on method) at selector. All persistence data including adapter comes from proto, not self
this.gather = function(proto, method, selectorOrQuery, query){
var selector, self = this;
if (!proto) throw "agility.js plugin persist: gather() needs object prototype";
if (!proto._data.persist) throw "agility.js plugin persist: prototype doesn't seem to contain persist() data";
// Determines arguments
if (query) {
selector = selectorOrQuery;
}
else {
if (typeof selectorOrQuery === 'string') {
selector = selectorOrQuery;
}
else {
selector = undefined;
query = selectorOrQuery;
}
}
if (this._data.persist.openRequests === 0) {
this.trigger('persist:start');
}
this._data.persist.openRequests++;
proto._data.persist.adapter.call(proto, {
type: 'GET',
data: query,
complete: function(){
self._data.persist.openRequests--;
if (self._data.persist.openRequests === 0) {
self.trigger('persist:stop');
}
},
success: function(data){
$.each(data, function(index, entry){
var obj = $$(proto, entry);
if (typeof method === 'string') {
self[method](obj, selector);
}
});
self.trigger('persist:gather:success', {data:data});
},
error: function(){
self.trigger('persist:error');
self.trigger('persist:gather:error');
}
});
return this; // for chainable calls
}; // gather()
return this; // for chainable calls
}; // fn.persist()
// Persistence adapters
// These are functions. Required parameters:
// {type: 'GET' || 'POST' || 'PUT' || 'DELETE'}
agility.adapter = {};
// RESTful JSON adapter using jQuery's ajax()
agility.adapter.restful = function(_params){
var params = $.extend({
dataType: 'json',
url: (this._data.persist.baseUrl || 'api/') + this._data.persist.collection + (_params.id ? '/'+_params.id : '')
}, _params);
$.ajax(params);
};
})(window);
This source diff could not be displayed because it is too large. You can view the blob instead.
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
color: inherit;
-webkit-appearance: none;
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #eaeaea url('bg.png');
color: #4d4d4d;
width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
button,
input[type="checkbox"] {
outline: none;
}
#todoapp {
background: #fff;
background: rgba(255, 255, 255, 0.9);
margin: 130px 0 40px 0;
border: 1px solid #ccc;
position: relative;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.15);
}
#todoapp:before {
content: '';
border-left: 1px solid #f5d6d6;
border-right: 1px solid #f5d6d6;
width: 2px;
position: absolute;
top: 0;
left: 40px;
height: 100%;
}
#todoapp input::-webkit-input-placeholder {
font-style: italic;
}
#todoapp input::-moz-placeholder {
font-style: italic;
color: #a9a9a9;
}
#todoapp h1 {
position: absolute;
top: -120px;
width: 100%;
font-size: 70px;
font-weight: bold;
text-align: center;
color: #b3b3b3;
color: rgba(255, 255, 255, 0.3);
text-shadow: -1px -1px rgba(0, 0, 0, 0.2);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
-ms-text-rendering: optimizeLegibility;
-o-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
#header {
padding-top: 15px;
border-radius: inherit;
}
#header:before {
content: '';
position: absolute;
top: 0;
right: 0;
left: 0;
height: 15px;
z-index: 2;
border-bottom: 1px solid #6c615c;
background: #8d7d77;
background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8)));
background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');
border-top-left-radius: 1px;
border-top-right-radius: 1px;
}
#new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
line-height: 1.4em;
border: 0;
outline: none;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
#new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.02);
z-index: 2;
box-shadow: none;
}
#main {
position: relative;
z-index: 2;
border-top: 1px dotted #adadad;
}
label[for='toggle-all'] {
display: none;
}
#toggle-all {
position: absolute;
top: -42px;
left: -4px;
width: 40px;
text-align: center;
/* Mobile Safari */
border: none;
}
#toggle-all:before {
content: '»';
font-size: 28px;
color: #d9d9d9;
padding: 0 25px 7px;
}
#toggle-all:checked:before {
color: #737373;
}
#todo-list {
margin: 0;
padding: 0;
list-style: none;
}
#todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px dotted #ccc;
}
#todo-list li:last-child {
border-bottom: none;
}
#todo-list li.editing {
border-bottom: none;
padding: 0;
}
#todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
#todo-list li.editing .view {
display: none;
}
#todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
/* Mobile Safari */
border: none;
-webkit-appearance: none;
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
#todo-list li .toggle:after {
content: '✔';
/* 40 + a couple of pixels visual adjustment */
line-height: 43px;
font-size: 20px;
color: #d9d9d9;
text-shadow: 0 -1px 0 #bfbfbf;
}
#todo-list li .toggle:checked:after {
color: #85ada7;
text-shadow: 0 1px 0 #669991;
bottom: 1px;
position: relative;
}
#todo-list li label {
white-space: pre;
word-break: break-word;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
-webkit-transition: color 0.4s;
transition: color 0.4s;
}
#todo-list li.completed label {
color: #a9a9a9;
text-decoration: line-through;
}
#todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 22px;
color: #a88a8a;
-webkit-transition: all 0.2s;
transition: all 0.2s;
}
#todo-list li .destroy:hover {
text-shadow: 0 0 1px #000,
0 0 10px rgba(199, 107, 107, 0.8);
-webkit-transform: scale(1.3);
transform: scale(1.3);
}
#todo-list li .destroy:after {
content: '✖';
}
#todo-list li:hover .destroy {
display: block;
}
#todo-list li .edit {
display: none;
}
#todo-list li.editing:last-child {
margin-bottom: -1px;
}
#footer {
color: #777;
padding: 0 15px;
position: absolute;
right: 0;
bottom: -31px;
left: 0;
height: 20px;
z-index: 1;
text-align: center;
}
#footer:before {
content: '';
position: absolute;
right: 0;
bottom: 31px;
left: 0;
height: 50px;
z-index: -1;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
0 6px 0 -3px rgba(255, 255, 255, 0.8),
0 7px 1px -3px rgba(0, 0, 0, 0.3),
0 43px 0 -6px rgba(255, 255, 255, 0.8),
0 44px 2px -6px rgba(0, 0, 0, 0.2);
}
#todo-count {
float: left;
text-align: left;
}
#filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
#filters li {
display: inline;
}
#filters li a {
color: #83756f;
margin: 2px;
text-decoration: none;
}
#filters li a.selected {
font-weight: bold;
}
#clear-completed {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
background: rgba(0, 0, 0, 0.1);
font-size: 11px;
padding: 0 10px;
border-radius: 3px;
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2);
}
#clear-completed:hover {
background: rgba(0, 0, 0, 0.15);
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);
}
#info {
margin: 65px auto 0;
color: #a6a6a6;
font-size: 12px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
text-align: center;
}
#info a {
color: inherit;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox and Opera
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
#toggle-all,
#todo-list li .toggle {
background: none;
}
#todo-list li .toggle {
height: 40px;
}
#toggle-all {
top: -56px;
left: -15px;
width: 65px;
height: 41px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
.hidden {
display: none;
}
hr {
margin: 20px 0;
border: 0;
border-top: 1px dashed #C5C5C5;
border-bottom: 1px dashed #F7F7F7;
}
.learn a {
font-weight: normal;
text-decoration: none;
color: #b83f45;
}
.learn a:hover {
text-decoration: underline;
color: #787e7e;
}
.learn h3,
.learn h4,
.learn h5 {
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
color: #000;
}
.learn h3 {
font-size: 24px;
}
.learn h4 {
font-size: 18px;
}
.learn h5 {
margin-bottom: 0;
font-size: 14px;
}
.learn ul {
padding: 0;
margin: 0 0 30px 25px;
}
.learn li {
line-height: 20px;
}
.learn p {
font-size: 15px;
font-weight: 300;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}
.quote {
border: none;
margin: 20px 0 60px 0;
}
.quote p {
font-style: italic;
}
.quote p:before {
content: '“';
font-size: 50px;
opacity: .15;
position: absolute;
top: -20px;
left: 3px;
}
.quote p:after {
content: '”';
font-size: 50px;
opacity: .15;
position: absolute;
bottom: -42px;
right: 3px;
}
.quote footer {
position: absolute;
bottom: -40px;
right: 0;
}
.quote footer img {
border-radius: 3px;
}
.quote footer a {
margin-left: 5px;
vertical-align: middle;
}
.speech-bubble {
position: relative;
padding: 10px;
background: rgba(0, 0, 0, .04);
border-radius: 5px;
}
.speech-bubble:after {
content: '';
position: absolute;
top: 100%;
right: 30px;
border: 13px solid transparent;
border-top-color: rgba(0, 0, 0, .04);
}
.learn-bar > .learn {
position: absolute;
width: 272px;
top: 8px;
left: -300px;
padding: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, .6);
-webkit-transition-property: left;
transition-property: left;
-webkit-transition-duration: 500ms;
transition-duration: 500ms;
}
@media (min-width: 899px) {
.learn-bar {
width: auto;
margin: 0 0 0 300px;
}
.learn-bar > .learn {
left: 8px;
}
.learn-bar #todoapp {
width: 550px;
margin: 130px auto 40px auto;
}
}
(function () {
'use strict';
// Underscore's Template Module
// Courtesy of underscorejs.org
var _ = (function (_) {
_.defaults = function (object) {
if (!object) {
return object;
}
for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) {
var iterable = arguments[argsIndex];
if (iterable) {
for (var key in iterable) {
if (object[key] == null) {
object[key] = iterable[key];
}
}
}
}
return object;
}
// By default, Underscore uses ERB-style template delimiters, change the
// following template settings to use alternative delimiters.
_.templateSettings = {
evaluate : /<%([\s\S]+?)%>/g,
interpolate : /<%=([\s\S]+?)%>/g,
escape : /<%-([\s\S]+?)%>/g
};
// When customizing `templateSettings`, if you don't want to define an
// interpolation, evaluation or escaping regex, we need one that is
// guaranteed not to match.
var noMatch = /(.)^/;
// Certain characters need to be escaped so that they can be put into a
// string literal.
var escapes = {
"'": "'",
'\\': '\\',
'\r': 'r',
'\n': 'n',
'\t': 't',
'\u2028': 'u2028',
'\u2029': 'u2029'
};
var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
// JavaScript micro-templating, similar to John Resig's implementation.
// Underscore templating handles arbitrary delimiters, preserves whitespace,
// and correctly escapes quotes within interpolated code.
_.template = function(text, data, settings) {
var render;
settings = _.defaults({}, settings, _.templateSettings);
// Combine delimiters into one regular expression via alternation.
var matcher = new RegExp([
(settings.escape || noMatch).source,
(settings.interpolate || noMatch).source,
(settings.evaluate || noMatch).source
].join('|') + '|$', 'g');
// Compile the template source, escaping string literals appropriately.
var index = 0;
var source = "__p+='";
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
source += text.slice(index, offset)
.replace(escaper, function(match) { return '\\' + escapes[match]; });
if (escape) {
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
}
if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
}
if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
index = offset + match.length;
return match;
});
source += "';\n";
// If a variable is not specified, place data values in local scope.
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + "return __p;\n";
try {
render = new Function(settings.variable || 'obj', '_', source);
} catch (e) {
e.source = source;
throw e;
}
if (data) return render(data, _);
var template = function(data) {
return render.call(this, data, _);
};
// Provide the compiled function source as a convenience for precompilation.
template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
return template;
};
return _;
})({});
if (location.hostname === 'todomvc.com') {
window._gaq = [['_setAccount','UA-31081062-1'],['_trackPageview']];(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src='//www.google-analytics.com/ga.js';s.parentNode.insertBefore(g,s)}(document,'script'));
}
function redirect() {
if (location.hostname === 'tastejs.github.io') {
location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com');
}
}
function findRoot() {
var base = location.href.indexOf('examples/');
return location.href.substr(0, base);
}
function getFile(file, callback) {
if (!location.host) {
return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.');
}
var xhr = new XMLHttpRequest();
xhr.open('GET', findRoot() + file, true);
xhr.send();
xhr.onload = function () {
if (xhr.status === 200 && callback) {
callback(xhr.responseText);
}
};
}
function Learn(learnJSON, config) {
if (!(this instanceof Learn)) {
return new Learn(learnJSON, config);
}
var template, framework;
if (typeof learnJSON !== 'object') {
try {
learnJSON = JSON.parse(learnJSON);
} catch (e) {
return;
}
}
if (config) {
template = config.template;
framework = config.framework;
}
if (!template && learnJSON.templates) {
template = learnJSON.templates.todomvc;
}
if (!framework && document.querySelector('[data-framework]')) {
framework = document.querySelector('[data-framework]').dataset.framework;
}
this.template = template;
if (learnJSON.backend) {
this.frameworkJSON = learnJSON.backend;
this.append({
backend: true
});
} else if (learnJSON[framework]) {
this.frameworkJSON = learnJSON[framework];
this.append();
}
}
Learn.prototype.append = function (opts) {
var aside = document.createElement('aside');
aside.innerHTML = _.template(this.template, this.frameworkJSON);
aside.className = 'learn';
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);
};
redirect();
getFile('learn.json', Learn);
})();
<!doctype html>
<html lang="en" data-framework="agilityjs">
<head>
<meta charset="utf-8">
<title>Agility.js • TodoMVC</title>
<link rel="stylesheet" href="bower_components/todomvc-common/base.css">
</head>
<body>
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo" type="text" data-bind="newtitle" placeholder="What needs to be done?" autofocus>
</header>
<section id="main" data-bind="class = mainStyle">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list">
<li>
<div class="view">
<input class="toggle" type="checkbox" data-bind="completed">
<label data-bind="title"></label>
<button class="destroy"></button>
</div>
<input class="edit" data-bind="title">
</li>
</ul>
</section>
<footer id="footer" data-bind="class = mainStyle">
<span id="todo-count"><strong data-bind='todoCount'></strong> item<span data-bind='pluralizer'></span> left</span>
<ul id="filters">
<li>
<a class="selected" href="#/">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<button id="clear-completed" data-bind="class = clearBtnStyle">Clear completed (<span data-bind="completedCount"></span>)</button>
</footer>
</section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="http://github.com/tshm/todomvc/">Tosh Shimayama</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script src="bower_components/todomvc-common/base.js"></script>
<script src="bower_components/jquery/jquery.js"></script>
<script src="bower_components/agility/agility.js"></script>
<script src="js/localstorage.js"></script>
<script src="js/app.js"></script>
</body>
</html>
(function ($, $$) {
'use strict';
var ENTER_KEY = 13;
var ESCAPE_KEY = 27;
// hack of taking out html elements from DOM so that agility's view can use it
// we need 'outerhtml' also, as agilityjs will append DOM, so removing it
var drawHtml = function (selector) {
return $(selector).remove().wrap('<div>').parent().html();
};
// simple Two layer composition:
// individual 'todoitem' and 'app' which holds multiple todoitems
$(function () {
// todo item
var todoitem = $$({
model: {
title: '',
completed: false
},
view: {
format: drawHtml('#todo-list li'),
style: '.hidden { display: none }'
},
controller: {
'change:completed': function () {
this.view.$().toggleClass('completed', this.model.get('completed'));
app.updateStatus();
},
'dblclick &': function () {
this.view.$().addClass('editing');
this.view.$('.edit').focus();
},
'click .destroy': function () {
this.destroy();
},
'create': function () {
this.view.$().toggleClass('completed', this.model.get('completed'));
},
'change': function () {
this.save();
},
'destroy': function () {
this.erase();
},
'blur input': function () {
this.updateTitle();
},
'keyup input': function () {
if (event.which === ENTER_KEY) {
this.updateTitle();
} else if (event.which === ESCAPE_KEY) {
this.revertTitleEdit();
}
}
},
updateTitle: function () {
var title = this.model.get('title').trim();
this.view.$().removeClass('editing');
if (title) {
this.model.set({
title: title
});
} else {
this.destroy();
}
},
revertTitleEdit: function() {
this.view.$().removeClass('editing');
// restore title
this.view.$(".edit").val(this.model.get('title'));
}
}).persist($$.adapter.localStorage, {
collection: 'todos-agilityjs'
});
// the main application which holds todo items
var app = $$({
model: {
todoCount: '0',
pluralizer: '',
completedCount: '0',
newtitle: '',
mainStyle: '',
clearBtnStyle: ''
},
view: {
format: drawHtml('#todoapp'),
style: '.hidden { display: none }'
},
controller: {
'remove': function () {
this.updateStatus();
},
'append': function () {
this.updateStatus();
},
'keyup #new-todo': function (e) {
var title = $('#new-todo').val().trim();
if (e.which === ENTER_KEY && title) {
var item = $$(todoitem, {
title: title
}).save();
this.append(item, '#todo-list');
e.target.value = ''; // clear input field
}
},
'click #toggle-all': function () {
var ischecked = this.view.$('#toggle-all').prop('checked');
this.each(function (id, item) {
item.model.set({
completed: ischecked
});
});
},
'click #clear-completed': function () {
this.each(function (id, item) {
if (item.model.get('completed')) {
item.destroy();
}
});
}
},
// utility functions
updateStatus: function () {
// update counts
var count = this.size();
var completedCount = 0;
this.each(function (id, item) {
if (item.model.get('completed')) {
completedCount++;
}
});
this.model.set({
todoCount: count - completedCount + '',
pluralizer: count - completedCount === 1 ? '' : 's',
completedCount: completedCount + '',
mainStyle: count === 0 ? 'hidden' : '',
clearBtnStyle: completedCount === 0 ? 'hidden' : ''
});
// update toggle-all checked status
$('#toggle-all').prop('checked', completedCount === count);
// update the view according to the current filter.
this.applyFilter();
},
// filter handler
filters: {
'#/': function () {
return true;
},
'#/active': function (item) {
return !item.model.get('completed');
},
'#/completed': function (item) {
return item.model.get('completed');
}
},
applyFilter: function (hash) {
var isVisible = this.filters[hash || location.hash];
if (isVisible) {
this.each(function (id, item) {
item.view.$().toggleClass('hidden', !isVisible(item));
});
}
}
}).persist();
$$.document.prepend(app);
// load from localStorage
app.gather(todoitem, 'append', '#todo-list').updateStatus();
// manual routing (not supported by agilityjs)
$(window).on('hashchange', function () {
var hash = location.hash;
app.applyFilter(hash);
$('#filters a').each(function () {
$(this).toggleClass('selected', hash === $(this).attr('href'));
});
});
if (location.hash) {
$(window).trigger('hashchange');
}
});
})(window.jQuery, window.agility);
// custom agilityjs adapter for localstorage
(function ($$, undefined) {
'use strict';
$$.adapter.localStorage = function (_params) {
var storageKey = (this._data.persist.baseUrl || '') + this._data.persist.collection;
var storageStr = localStorage[storageKey];
var items = (storageStr ? JSON.parse(storageStr) : {});
if (_params.type === 'GET') {
if (_params.id !== undefined) { // normal get
if (typeof items[_params.id] === 'object') {
_params.success(items[_params.id]);
} else {
_params.error();
}
} else { // gather call
_params.success(items);
}
} else if (_params.type === 'DELETE') {
delete items[_params.id];
localStorage[storageKey] = JSON.stringify(items);
} else if (_params.type === 'PUT' || _params.type === 'POST') {
if (_params.id === undefined) {
_params.id = (new Date()).getTime();
_params.data.id = _params.id;
}
items[_params.id] = _params.data;
localStorage[storageKey] = JSON.stringify(items);
} else {
_params.error();
}
_params.complete();
};
})(window.agility);
# Agility.js TodoMVC Example
> [Agility.js](http://agilityjs.com) is an MVC library for Javascript that lets you write maintainable and reusable browser code without the verbose or infrastructural overhead found in other MVC libraries. The goal is to enable developers to write web apps at least as quickly as with jQuery, while simplifying long-term maintainability through MVC objects.
> _[Agility.js - agilityjs.com](http://agilityjs.com)_
## Learning Agility.js
The [Agility.js website](http://agilityjs.com) is a great resource for getting started.
Here are some links you may find helpful:
* [Official Documentation](http://agilityjs.com/docs/docs.html)
* [Try it out on JSBin](http://jsbin.com/agility/224/edit)
* [Applications built with Agility.js](http://agilityjs.com/docs/gallery.html)
Articles and guides from the community:
* [Step by step from jQuery to Agility.js](https://gist.github.com/pindia/3166678)
Get help from other Agility.js users:
* [Google Groups mailing list](http://groups.google.com/group/agilityjs)
* [agility.js on Stack Overflow](http://stackoverflow.com/questions/tagged/agility.js)
* [Agility.js on Twitter](https://twitter.com/agilityjs)
* [Agility.js on Google +](https://plus.google.com/116251025970928820842/posts)
_If you have other helpful links to share, or find any of the links above no longer work, please [let us know](https://github.com/tastejs/todomvc/issues)._
## Credit
This TodoMVC application was created by [tshm](https://github.com/tshm).
......@@ -99,9 +99,6 @@
<li class="routing">
<a href="examples/yui/" data-source="http://yuilibrary.com" data-content="YUI's lightweight core and modular architecture make it scalable, fast, and robust. Built by frontend engineers at Yahoo!, YUI powers the most popular websites in the world.">YUI</a>
</li>
<li class="routing">
<a href="examples/agilityjs/" data-source="http://agilityjs.com" data-content="Agility.js is an MVC library for Javascript that lets you write maintainable and reusable browser code without the infrastructural overhead found in other MVC libraries. The goal is to enable developers to write web apps at least as quickly as with jQuery, while simplifying long-term maintainability through MVC objects.">Agility.js</a>
</li>
<li class="routing">
<a href="examples/knockback/" data-source="http://kmalakoff.github.com/knockback/" data-content="Knockback.js provides Knockout.js magic for Backbone.js Models and Collections.">Knockback.js</a>
</li>
......
{
"agilityjs": {
"name": "Agility.js",
"description": "Agility.js is an MVC library for Javascript that lets you write maintainable and reusable browser code without the verbose or infrastructural overhead found in other MVC libraries. The goal is to enable developers to write web apps at least as quickly as with jQuery, while simplifying long-term maintainability through MVC objects.",
"homepage": "agilityjs.com",
"examples": [{
"name": "Example",
"url": "examples/agilityjs"
}],
"link_groups": [{
"heading": "Official Resources",
"links": [{
"name": "Official Documentation",
"url": "http://agilityjs.com/docs/docs.html"
}, {
"name": "Try it out on JSBin",
"url": "http://jsbin.com/agility/224/edit"
}, {
"name": "Applications built with Agility.js",
"url": "http://agilityjs.com/docs/gallery.html"
}]
}, {
"heading": "Articles and Guides",
"links": [{
"name": "Step by step from jQuery to Agility.js",
"url": "https://gist.github.com/pindia/3166678"
}]
}, {
"heading": "Community",
"links": [{
"name": "Google Groups mailing list",
"url": "http://groups.google.com/group/agilityjs"
}, {
"name": "agility.js on Stack Overflow",
"url": "http://stackoverflow.com/questions/tagged/agility.js"
}, {
"name": "Agility.js on Twitter",
"url": "https://twitter.com/agilityjs"
}, {
"name": "Agility.js on Google+",
"url": "https://plus.google.com/116251025970928820842/posts"
}]
}]
},
"angularjs": {
"name": "AngularJS",
"description": "HTML is great for declaring static documents, but it falters when we try to use it for declaring dynamic views in web-applications. AngularJS lets you extend HTML vocabulary for your application. The resulting environment is extraordinarily expressive, readable, and quick to develop.",
......
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