Commit c7284e71 authored by Ryan Eastridge's avatar Ryan Eastridge

add Thorax + Lumbar example

parent 627a254c
Thorax + Lumbar TodoMVC Example
===============================
This example uses [Thorax](http://thoraxjs.org) and [Lumbar](http://walmartlabs.github.com/lumbar). The compiled JavaScript is included in the repo, to re-build the files run:
npm install
npm start
Lumbar will create a `public/base.js` file that contains the core libraries needed to run the application, and a master router that listens to all routes defined in `lumbar.json`. When one of those routes is visited the appropriate module file is loaded, in this case `public/todomvc.js`.
\ No newline at end of file
{
"application": {
"name": "Application",
"module": "base"
},
"modules": {
"base": {
"scripts": [
{
"src": "../../../assets/base.js",
"global": true
},
{
"src": "../../../assets/jquery.min.js",
"global": true
},
{
"src": "../../../assets/lodash.min.js",
"global": true
},
{
"src": "../../../assets/handlebars.min.js",
"global": true
},
{
"src": "src/js/lib/backbone.js",
"global": true
},
{
"src": "src/js/lib/backbone.js",
"global": true
},
{
"src": "src/js/lib/backbone-localstorage.js",
"global": true
},
{
"src": "src/js/lib/thorax.js",
"global": true
},
{
"src": "src/js/lib/script.js",
"global": true
},
{
"src": "src/js/lib/lumbar-loader.js"
},
{
"src": "src/js/lib/lumbar-loader-events.js"
},
{
"src": "src/js/lib/lumbar-loader-standard.js"
},
{
"src": "src/js/lib/lumbar-loader-backbone.js"
},
{
"src": "src/js/init.js"
},
{
"module-map": true
}
]
},
"todomvc": {
"routes": {
"": "setFilter",
":filter": "setFilter"
},
"scripts": [
"src/js/models/todo.js",
"src/js/collections/todos.js",
"src/js/views/todo-item.js",
"src/js/views/stats.js",
"src/js/views/app.js",
"src/js/routers/todomvc.js",
"src/js/app.js"
]
}
},
"templates": {
"template": "Thorax.templates['{{{without-extension name}}}'] = '{{{data}}}';",
"src/js/views/app.js": [
"src/templates/app.handlebars"
],
"src/js/views/stats.js": [
"src/templates/stats.handlebars"
]
}
}
{
"name": "thorax-lumbar-todomvc",
"version": "0.0.1",
"devDependencies": {
"lumbar": "git://github.com/beastridge/lumbar.git"
},
"scripts": {
"start": "lumbar build lumbar.json public"
}
}
\ No newline at end of file
This diff is collapsed.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Thorax • TodoMVC</title>
<link rel="stylesheet" href="../../../../assets/base.css">
<!--[if IE]>
<script src="../../../../assets/ie.js"></script>
<![endif]-->
</head>
<body>
<script src="base.js"></script>
</body>
</html>
Application['todomvc'] = (function() {
var module = {exports: {}};
var exports = module.exports;
/* router : todomvc */
module.name = "todomvc";
module.routes = {"":"setFilter",":filter":"setFilter"};
(function() {
'use strict';
// Todo Model
// ----------
// Our basic **Todo** model has `title`, `order`, and `completed` attributes.
window.app.Todo = Backbone.Model.extend({
// Default attributes for the todo
// and ensure that each todo created has `title` and `completed` keys.
defaults: {
title: '',
completed: false
},
// Toggle the `completed` state of this todo item.
toggle: function() {
this.save({
completed: !this.get('completed')
});
},
isVisible: function () {
var isCompleted = this.get('completed');
if (window.app.TodoFilter === '') {
return true;
} else if (window.app.TodoFilter === 'completed') {
return isCompleted;
} else if (window.app.TodoFilter === 'active') {
return !isCompleted;
}
}
});
}());
;;
(function() {
'use strict';
// Todo Collection
// ---------------
// The collection of todos is backed by *localStorage* instead of a remote
// server.
var TodoList = Backbone.Collection.extend({
// Reference to this collection's model.
model: window.app.Todo,
// Save all of the todo items under the `"todos"` namespace.
localStorage: new Store('todos-backbone'),
// Filter down the list of all todo items that are finished.
completed: function() {
return this.filter(function( todo ) {
return todo.get('completed');
});
},
// Filter down the list to only todo items that are still not finished.
remaining: function() {
return this.without.apply( this, this.completed() );
},
// We keep the Todos in sequential order, despite being saved by unordered
// GUID in the database. This generates the next order number for new items.
nextOrder: function() {
if ( !this.length ) {
return 1;
}
return this.last().get('order') + 1;
},
// Todos are sorted by their original insertion order.
comparator: function( todo ) {
return todo.get('order');
}
});
// Create our global collection of **Todos**.
window.app.Todos = new TodoList();
}());
;;
$(function() {
'use strict';
// Todo Item View
// --------------
// The DOM element for a todo item...
Thorax.View.extend({
//... is a list tag.
tagName: 'li',
// Cache the template function for a single item.
name: 'todo-item',
// The DOM events specific to an item.
events: {
'click .toggle': 'toggleCompleted',
'dblclick label': 'edit',
'click .destroy': 'clear',
'keypress .edit': 'updateOnEnter',
'blur .edit': 'close',
// The "rendered" event is triggered by Thorax each time render()
// is called and the result of the template has been appended
// to the View's $el
rendered: function() {
this.$el.toggleClass( 'completed', this.model.get('completed') );
}
},
// Toggle the `"completed"` state of the model.
toggleCompleted: function() {
this.model.toggle();
},
// Switch this view into `"editing"` mode, displaying the input field.
edit: function() {
this.$el.addClass('editing');
this.$('.edit').focus();
},
// Close the `"editing"` mode, saving changes to the todo.
close: function() {
var value = this.$('.edit').val().trim();
if ( value ) {
this.model.save({ title: value });
} else {
this.clear();
}
this.$el.removeClass('editing');
},
// If you hit `enter`, we're through editing the item.
updateOnEnter: function( e ) {
if ( e.which === ENTER_KEY ) {
this.close();
}
},
// Remove the item, destroy the model from *localStorage* and delete its view.
clear: function() {
this.model.destroy();
}
});
});
;;
Thorax.View.extend({
name: 'stats',
events: {
'click #clear-completed': 'clearCompleted',
// The "rendered" event is triggered by Thorax each time render()
// is called and the result of the template has been appended
// to the View's $el
rendered: 'highlightFilter'
},
initialize: function() {
// Whenever the Todos collection changes re-render the stats
// render() needs to be called with no arguments, otherwise calling
// it with arguments will insert the arguments as content
window.app.Todos.on('all', _.debounce(function() {
this.render();
}), this);
},
// Clear all completed todo items, destroying their models.
clearCompleted: function() {
_.each( window.app.Todos.completed(), function( todo ) {
todo.destroy();
});
return false;
},
// Each time the stats view is rendered this function will
// be called to generate the context / scope that the template
// will be called with. "context" defaults to "return this"
context: function() {
var remaining = window.app.Todos.remaining().length;
return {
itemText: remaining === 1 ? 'item' : 'items',
completed: window.app.Todos.completed().length,
remaining: remaining
};
},
// Highlight which filter will appear to be active
highlightFilter: function() {
this.$('#filters li a')
.removeClass('selected')
.filter('[href="#/' + ( window.app.TodoFilter || '' ) + '"]')
.addClass('selected');
}
});;;
Thorax.templates['src/templates/stats'] = '<span id=\"todo-count\"><strong>{{remaining}}</strong> {{itemText}} left</span>\n<ul id=\"filters\">\n <li>\n {{#link \"/\" class=\"selected\"}}All{{/link}}\n </li>\n <li>\n {{#link \"/active\"}}Active{{/link}}\n </li>\n <li>\n {{#link \"/completed\"}}Completed{{/link}}\n </li>\n</ul>\n{{#if completed}}\n <button id=\"clear-completed\">Clear completed ({{completed}})</button>\n{{/if}}\n';$(function( $ ) {
'use strict';
// The Application
// ---------------
// Our overall **AppView** is the top-level piece of UI.
Thorax.View.extend({
// This will assign the template Thorax.templates['app'] to the view and
// create a view class at Thorax.Views['app']
name: 'app',
// Delegated events for creating new items, and clearing completed ones.
events: {
'keypress #new-todo': 'createOnEnter',
'click #toggle-all': 'toggleAllComplete',
// The collection helper in the template will bind the collection
// to the view. Any events in this hash will be bound to the
// collection.
collection: {
all: 'toggleToggleAllButton'
},
rendered: 'toggleToggleAllButton'
},
// Unless the "context" method is overriden any attributes on the view
// will be availble to the context / scope of the template, make the
// global Todos collection available to the template.
// Load any preexisting todos that might be saved in *localStorage*.
initialize: function() {
this.todosCollection = window.app.Todos;
this.todosCollection.fetch();
this.render();
},
toggleToggleAllButton: function() {
this.$('#toggle-all').attr('checked', !this.todosCollection.remaining().length);
},
// This function is specified in the collection helper as the filter
// and will be called each time a model changes, or for each item
// when the collection is rendered
filterTodoItem: function(model) {
return model.isVisible();
},
// Generate the attributes for a new Todo item.
newAttributes: function() {
return {
title: this.$('#new-todo').val().trim(),
order: window.app.Todos.nextOrder(),
completed: false
};
},
// If you hit return in the main input field, create new **Todo** model,
// persisting it to *localStorage*.
createOnEnter: function( e ) {
if ( e.which !== ENTER_KEY || !this.$('#new-todo').val().trim() ) {
return;
}
window.app.Todos.create( this.newAttributes() );
this.$('#new-todo').val('');
},
toggleAllComplete: function() {
var completed = this.$('#toggle-all')[0].checked;
window.app.Todos.each(function( todo ) {
todo.save({
'completed': completed
});
});
}
});
});
;;
Thorax.templates['src/templates/app'] = '<section id=\"todoapp\">\n <header id=\"header\">\n <h1>todos</h1>\n <input id=\"new-todo\" placeholder=\"What needs to be done?\" autofocus>\n </header>\n {{^empty todosCollection}}\n <section id=\"main\">\n <input id=\"toggle-all\" type=\"checkbox\">\n <label for=\"toggle-all\">Mark all as complete</label>\n {{#collection todosCollection filter=\"filterTodoItem\" item-view=\"todo-item\" tag=\"ul\" id=\"todo-list\"}}\n <div class=\"view\">\n <input class=\"toggle\" type=\"checkbox\" {{#if completed}}checked{{/if}}>\n <label>{{title}}</label>\n <button class=\"destroy\"></button>\n </div>\n <input class=\"edit\" value=\"{{title}}\">\n {{/collection}}\n </section>\n {{view \"stats\" tag=\"footer\" id=\"footer\"}}\n {{/empty}}\n</section>\n<div id=\"info\">\n <p>Double-click to edit a todo</p>\n <p>Written by <a href=\"https://github.com/addyosmani\">Addy Osmani</a> &amp; <a href=\"https://github.com/beastridge\">Ryan Eastridge</a></p>\n <p>Part of <a href=\"http://todomvc.com\">TodoMVC</a></p>\n</div>\n';(function() {
'use strict';
// Todo Router
// ----------
new (Thorax.Router.extend({
// The module variable is set inside of the file
// generated by Lumbar
name: module.name,
routes: module.routes,
setFilter: function( param ) {
// Set the current filter to be used
window.app.TodoFilter = param ? param.trim().replace(/^\//, '') : '';
// Thorax listens for a `filter` event which will
// force the collection to re-filter
window.app.Todos.trigger('filter');
}
}));
}());
;;
var ENTER_KEY = 13;
$(function() {
// Kick things off by creating the **App**.
var view = new Thorax.Views['app']();
$('body').append(view.el);
});
;;
return module.exports;
}).call(this);
var ENTER_KEY = 13;
$(function() {
// Kick things off by creating the **App**.
var view = new Thorax.Views['app']();
$('body').append(view.el);
});
(function() {
'use strict';
// Todo Collection
// ---------------
// The collection of todos is backed by *localStorage* instead of a remote
// server.
var TodoList = Backbone.Collection.extend({
// Reference to this collection's model.
model: window.app.Todo,
// Save all of the todo items under the `"todos"` namespace.
localStorage: new Store('todos-backbone'),
// Filter down the list of all todo items that are finished.
completed: function() {
return this.filter(function( todo ) {
return todo.get('completed');
});
},
// Filter down the list to only todo items that are still not finished.
remaining: function() {
return this.without.apply( this, this.completed() );
},
// We keep the Todos in sequential order, despite being saved by unordered
// GUID in the database. This generates the next order number for new items.
nextOrder: function() {
if ( !this.length ) {
return 1;
}
return this.last().get('order') + 1;
},
// Todos are sorted by their original insertion order.
comparator: function( todo ) {
return todo.get('order');
}
});
// Create our global collection of **Todos**.
window.app.Todos = new TodoList();
}());
//all templates are assumed to be in the templates directory
Thorax.templatePathPrefix = 'src/templates/';
var app = window.app = module.exports;
$(function() {
app.initBackboneLoader();
Backbone.history.start();
});
// A simple module to replace `Backbone.sync` with *localStorage*-based
// persistence. Models are given GUIDS, and saved into a JSON object. Simple
// as that.
// Generate four random hex digits.
function S4() {
return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
};
// Generate a pseudo-GUID by concatenating random hexadecimal.
function guid() {
return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
};
// Our Store is represented by a single JS object in *localStorage*. Create it
// with a meaningful name, like the name you'd give a table.
var Store = function(name) {
this.name = name;
var store = localStorage.getItem(this.name);
this.data = (store && JSON.parse(store)) || {};
};
_.extend(Store.prototype, {
// Save the current state of the **Store** to *localStorage*.
save: function() {
localStorage.setItem(this.name, JSON.stringify(this.data));
},
// Add a model, giving it a (hopefully)-unique GUID, if it doesn't already
// have an id of it's own.
create: function(model) {
if (!model.id) model.id = model.attributes.id = guid();
this.data[model.id] = model;
this.save();
return model;
},
// Update a model by replacing its copy in `this.data`.
update: function(model) {
this.data[model.id] = model;
this.save();
return model;
},
// Retrieve a model from `this.data` by id.
find: function(model) {
return this.data[model.id];
},
// Return the array of all models currently in storage.
findAll: function() {
return _.values(this.data);
},
// Delete a model from `this.data`, returning it.
destroy: function(model) {
delete this.data[model.id];
this.save();
return model;
}
});
// Override `Backbone.sync` to use delegate to the model or collection's
// *localStorage* property, which should be an instance of `Store`.
Backbone.sync = function(method, model, options) {
var resp;
var store = model.localStorage || model.collection.localStorage;
switch (method) {
case "read": resp = model.id ? store.find(model) : store.findAll(); break;
case "create": resp = store.create(model); break;
case "update": resp = store.update(model); break;
case "delete": resp = store.destroy(model); break;
}
if (resp) {
options.success(resp);
} else {
options.error("Record not found");
}
};
This diff is collapsed.
module.exports.initBackboneLoader = function(loaderModule, failure) {
var lumbarLoader = (loaderModule || module.exports).loader;
// Setup backbone route loading
var handlers = {
routes: {}
};
var pendingModules = {};
for (var moduleName in lumbarLoader.map.modules) {
handlers['loader_' + moduleName] = (function(moduleName) {
return function() {
if (lumbarLoader.isLoaded(moduleName)) {
// The module didn't implement the proper route
failure && failure('missing-route', moduleName);
return;
} else if (pendingModules[moduleName]) {
// Do not exec the backbone callback multiple times
return;
}
pendingModules[moduleName] = true;
lumbarLoader.loadModule(moduleName, function(err) {
pendingModules[moduleName] = false;
if (err) {
failure && failure(err, moduleName);
return;
}
// Reload with the new route
Backbone.history.loadUrl();
});
};
})(moduleName);
}
// For each route create a handler that will load the associated module on request
for (var route in lumbarLoader.map.routes) {
handlers.routes[route] = 'loader_' + lumbarLoader.map.routes[route];
}
new (Backbone.Router.extend(handlers));
};
// Automatically initialize the loader if everything is setup already
if (module.exports.loader && module.exports.loader.map && window.Backbone) {
module.exports.initBackboneLoader();
}
(function() {
lumbarLoader.initEvents = function() {
// Needs to be defered until we know that backbone has been loaded
_.extend(lumbarLoader, Backbone.Events);
};
if (window.Backbone) {
lumbarLoader.initEvents();
}
var baseLoadModule = lumbarLoader.loadModule;
lumbarLoader.loadModule = function(moduleName, callback, options) {
options = options || {};
if (!options.silent) {
lumbarLoader.trigger && lumbarLoader.trigger('load:start', moduleName, undefined, lumbarLoader);
}
baseLoadModule(moduleName, function(error) {
if (!options.silent) {
lumbarLoader.trigger && lumbarLoader.trigger('load:end', lumbarLoader);
}
callback(error);
}, options);
};
})();
lumbarLoader.loadJS = function(moduleName, callback) {
return loadResources(moduleName, 'js', callback, function(href, callback) {
loadViaXHR(href, function(err, data) {
if (!err && data) {
try {
window.eval(data);
callback();
return true;
} catch (err) {
/* NOP */
}
}
callback(err ? 'connection' : 'javascript');
});
return 1;
}).length;
};
lumbarLoader.loadCSS = function(moduleName, callback) {
return loadResources(moduleName, 'css', callback, function(href) {
loadViaXHR(href, function(err, data) {
data && exports.loader.loadInlineCSS(data);
callback(err ? 'connecion' : undefined);
return !err;
});
return 1;
}).length;
};
function loadViaXHR(href, callback) {
var cache = LocalCache.get(href);
if (cache) {
// Dump off the stack to prevent any errors with loader module interaction
setTimeout(function() {
callback(undefined, cache);
}, 0);
return;
}
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if (xhr.readyState == 4) {
var success = (xhr.status >= 200 && xhr.status < 300) || (xhr.status == 0 && xhr.responseText);
if (callback(!success, xhr.responseText)) {
LocalCache.store(href, xhr.responseText, LocalCache.TTL.WEEK);
}
}
};
xhr.open('GET', href, true);
xhr.send(null);
}
lumbarLoader.loadJS = function(moduleName, callback) {
var loaded = loadResources(moduleName, 'js', callback, function(href, callback) {
$script(href, callback);
return 1;
});
return loaded.length;
};
lumbarLoader.loadCSS = function(moduleName, callback) {
var loaded = loadResources(moduleName, 'css', callback, function(href) {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = href;
return link;
});
if (callback) {
var interval = setInterval(function() {
var i = loaded.length;
while (i--) {
var sheet = loaded[i];
if ((sheet.sheet && ('length' in sheet.sheet.cssRules)) || (sheet.styleSheet && sheet.styleSheet.cssText)) {
loaded.splice(i, 1);
callback();
}
}
if (!loaded.length) {
clearInterval(interval);
}
}, 100);
}
return loaded.length;
};
var lumbarLoader = exports.loader = {
loadPrefix: typeof lumbarLoadPrefix === 'undefined' ? '' : lumbarLoadPrefix,
isLoaded: function(moduleName) {
return lumbarLoadedModules[moduleName] === true;
},
isLoading: function(moduleName) {
return !!lumbarLoadedModules[moduleName];
},
loadModule: function(moduleName, callback, options) {
options = options || {};
var loaded = lumbarLoadedModules[moduleName];
if (loaded) {
// We have already been loaded or there is something pending. Handle it
if (loaded === true) {
callback();
} else {
loaded.push(callback);
}
return;
}
loaded = lumbarLoadedModules[moduleName] = [callback];
var loadCount = 0,
expected = 1,
allInit = false;
function complete(error) {
loadCount++;
if (error || (allInit && loadCount >= expected)) {
lumbarLoadedModules[moduleName] = !error;
var moduleInfo = lumbarLoader.modules && lumbarLoader.modules[moduleName];
if (moduleInfo && moduleInfo.preload && !options.silent) {
preloadModules(moduleInfo.preload);
}
for (var i = 0, len = loaded.length; i < len; i++) {
loaded[i](error);
}
lumbarLoader.loadComplete && lumbarLoader.loadComplete(moduleName, error);
}
}
expected += lumbarLoader.loadCSS(moduleName, complete);
expected += lumbarLoader.loadJS(moduleName, complete);
// If everything was done sync then fire away
allInit = true;
complete();
},
loadInlineCSS: function(content) {
var style = document.createElement('style');
style.textContent = content;
appendResourceElement(style);
return style;
}
};
var lumbarLoadedModules = {},
lumbarLoadedResources = {},
fieldAttr = {
js: 'src',
css: 'href'
};
function loadResources(moduleName, field, callback, create) {
var module = moduleName === 'base' ? lumbarLoader.map.base : lumbarLoader.modules[moduleName], // Special case for the base case
loaded = [],
attr = fieldAttr[field];
field = module[field] || [];
if (Array.isArray ? !Array.isArray(field) : Object.prototype.toString.call(field) !== '[object Array]') {
field = [field];
}
for (var i = 0, len = field.length; i < len; i++) {
var object = field[i];
var href = checkLoadResource(object, attr);
if (href && !lumbarLoadedResources[href]) {
var el = create(href, function(err) {
if (err === 'connection') {
lumbarLoadedResources[href] = false;
}
callback(err);
});
lumbarLoadedResources[href] = true;
if (el && el.nodeType === 1) {
appendResourceElement(el);
}
loaded.push(el);
}
}
return loaded;
}
function appendResourceElement(element) {
return (document.head || document.getElementsByTagName('head')[0] || document.body).appendChild(element);
}
function preloadModules(modules) {
var moduleList = modules.slice();
for (var i = 0, len = modules.length; i < len; i++) {
lumbarLoader.loadModule(modules[i], function() {}, {silent: true});
}
}
var devicePixelRatio = parseFloat(sessionStorage.getItem('dpr') || window.devicePixelRatio || 1);
exports.devicePixelRatio = devicePixelRatio;
function checkLoadResource(object, attr) {
var href = lumbarLoader.loadPrefix + (object.href || object);
if ((!object.maxRatio || devicePixelRatio < object.maxRatio) && (!object.minRatio || object.minRatio <= devicePixelRatio)) {
if (document.querySelector('[' + attr + '="' + href + '"]')) {
return;
}
return href;
}
}
exports.moduleMap = function(map, loadPrefix) {
lumbarLoader.map = map;
lumbarLoader.modules = map.modules;
lumbarLoader.loadPrefix = loadPrefix || lumbarLoader.loadPrefix;
};
/*!
* $script.js Async loader & dependency manager
* https://github.com/ded/script.js
* (c) Dustin Diaz, Jacob Thornton 2011
* License: MIT
*/
!function (name, definition) {
if (typeof define == 'function') define(definition)
else if (typeof module != 'undefined') module.exports = definition()
else this[name] = definition()
}('$script', function() {
var win = this, doc = document, timeout = setTimeout
, head = doc.getElementsByTagName('head')[0]
, validBase = /^https?:\/\//
, old = win.$script, list = {}, ids = {}, delay = {}, scriptpath
, scripts = {}, s = 'string', f = false
, push = 'push', domContentLoaded = 'DOMContentLoaded', readyState = 'readyState'
, addEventListener = 'addEventListener', onreadystatechange = 'onreadystatechange'
function every(ar, fn, i) {
for (i = 0, j = ar.length; i < j; ++i) if (!fn(ar[i])) return f
return 1
}
function each(ar, fn) {
every(ar, function(el) {
return !fn(el)
})
}
if (!doc[readyState] && doc[addEventListener]) {
doc[addEventListener](domContentLoaded, function fn() {
doc.removeEventListener(domContentLoaded, fn, f)
doc[readyState] = 'complete'
}, f)
doc[readyState] = 'loading'
}
function $script(paths, idOrDone, optDone) {
paths = paths[push] ? paths : [paths];
var idOrDoneIsDone = idOrDone && idOrDone.call
, done = idOrDoneIsDone ? idOrDone : optDone
, id = idOrDoneIsDone ? paths.join('') : idOrDone
, queue = paths.length
function loopFn(item) {
return item.call ? item() : list[item]
}
function callback() {
if (!--queue) {
list[id] = 1
done && done()
for (var dset in delay) {
every(dset.split('|'), loopFn) && !each(delay[dset], loopFn) && (delay[dset] = [])
}
}
}
timeout(function() {
each(paths, function(path) {
if (scripts[path]) {
id && (ids[id] = 1)
return scripts[path] == 2 && callback()
}
scripts[path] = 1
id && (ids[id] = 1)
create(!validBase.test(path) && scriptpath ? scriptpath + path + '.js' : path, callback)
})
}, 0)
return $script
}
function create(path, fn) {
var el = doc.createElement('script')
, loaded = f
el.onload = el.onerror = el[onreadystatechange] = function () {
if ((el[readyState] && !(/^c|loade/.test(el[readyState]))) || loaded) return;
el.onload = el[onreadystatechange] = null
loaded = 1
scripts[path] = 2
fn()
}
el.async = 1
el.src = path
head.insertBefore(el, head.firstChild)
}
$script.get = create
$script.order = function (scripts, id, done) {
(function callback(s) {
s = scripts.shift()
if (!scripts.length) $script(s, id, done)
else $script(s, callback)
}())
}
$script.path = function(p) {
scriptpath = p
}
$script.ready = function(deps, ready, req) {
deps = deps[push] ? deps : [deps]
var missing = [];
!each(deps, function(dep) {
list[dep] || missing[push](dep);
}) && every(deps, function(dep) {return list[dep]}) ?
ready() : !function(key) {
delay[key] = delay[key] || []
delay[key][push](ready)
req && req(missing)
}(deps.join('|'))
return $script
}
$script.noConflict = function () {
win.$script = old;
return this
}
return $script
})
\ No newline at end of file
This diff is collapsed.
(function() {
'use strict';
// Todo Model
// ----------
// Our basic **Todo** model has `title`, `order`, and `completed` attributes.
window.app.Todo = Backbone.Model.extend({
// Default attributes for the todo
// and ensure that each todo created has `title` and `completed` keys.
defaults: {
title: '',
completed: false
},
// Toggle the `completed` state of this todo item.
toggle: function() {
this.save({
completed: !this.get('completed')
});
},
isVisible: function () {
var isCompleted = this.get('completed');
if (window.app.TodoFilter === '') {
return true;
} else if (window.app.TodoFilter === 'completed') {
return isCompleted;
} else if (window.app.TodoFilter === 'active') {
return !isCompleted;
}
}
});
}());
(function() {
'use strict';
// Todo Router
// ----------
new (Thorax.Router.extend({
// The module variable is set inside of the file
// generated by Lumbar
name: module.name,
routes: module.routes,
setFilter: function( param ) {
// Set the current filter to be used
window.app.TodoFilter = param ? param.trim().replace(/^\//, '') : '';
// Thorax listens for a `filter` event which will
// force the collection to re-filter
window.app.Todos.trigger('filter');
}
}));
}());
$(function( $ ) {
'use strict';
// The Application
// ---------------
// Our overall **AppView** is the top-level piece of UI.
Thorax.View.extend({
// This will assign the template Thorax.templates['app'] to the view and
// create a view class at Thorax.Views['app']
name: 'app',
// Delegated events for creating new items, and clearing completed ones.
events: {
'keypress #new-todo': 'createOnEnter',
'click #toggle-all': 'toggleAllComplete',
// The collection helper in the template will bind the collection
// to the view. Any events in this hash will be bound to the
// collection.
collection: {
all: 'toggleToggleAllButton'
},
rendered: 'toggleToggleAllButton'
},
// Unless the "context" method is overriden any attributes on the view
// will be availble to the context / scope of the template, make the
// global Todos collection available to the template.
// Load any preexisting todos that might be saved in *localStorage*.
initialize: function() {
this.todosCollection = window.app.Todos;
this.todosCollection.fetch();
this.render();
},
toggleToggleAllButton: function() {
this.$('#toggle-all').attr('checked', !this.todosCollection.remaining().length);
},
// This function is specified in the collection helper as the filter
// and will be called each time a model changes, or for each item
// when the collection is rendered
filterTodoItem: function(model) {
return model.isVisible();
},
// Generate the attributes for a new Todo item.
newAttributes: function() {
return {
title: this.$('#new-todo').val().trim(),
order: window.app.Todos.nextOrder(),
completed: false
};
},
// If you hit return in the main input field, create new **Todo** model,
// persisting it to *localStorage*.
createOnEnter: function( e ) {
if ( e.which !== ENTER_KEY || !this.$('#new-todo').val().trim() ) {
return;
}
window.app.Todos.create( this.newAttributes() );
this.$('#new-todo').val('');
},
toggleAllComplete: function() {
var completed = this.$('#toggle-all')[0].checked;
window.app.Todos.each(function( todo ) {
todo.save({
'completed': completed
});
});
}
});
});
Thorax.View.extend({
name: 'stats',
events: {
'click #clear-completed': 'clearCompleted',
// The "rendered" event is triggered by Thorax each time render()
// is called and the result of the template has been appended
// to the View's $el
rendered: 'highlightFilter'
},
initialize: function() {
// Whenever the Todos collection changes re-render the stats
// render() needs to be called with no arguments, otherwise calling
// it with arguments will insert the arguments as content
window.app.Todos.on('all', _.debounce(function() {
this.render();
}), this);
},
// Clear all completed todo items, destroying their models.
clearCompleted: function() {
_.each( window.app.Todos.completed(), function( todo ) {
todo.destroy();
});
return false;
},
// Each time the stats view is rendered this function will
// be called to generate the context / scope that the template
// will be called with. "context" defaults to "return this"
context: function() {
var remaining = window.app.Todos.remaining().length;
return {
itemText: remaining === 1 ? 'item' : 'items',
completed: window.app.Todos.completed().length,
remaining: remaining
};
},
// Highlight which filter will appear to be active
highlightFilter: function() {
this.$('#filters li a')
.removeClass('selected')
.filter('[href="#/' + ( window.app.TodoFilter || '' ) + '"]')
.addClass('selected');
}
});
\ No newline at end of file
$(function() {
'use strict';
// Todo Item View
// --------------
// The DOM element for a todo item...
Thorax.View.extend({
//... is a list tag.
tagName: 'li',
// Cache the template function for a single item.
name: 'todo-item',
// The DOM events specific to an item.
events: {
'click .toggle': 'toggleCompleted',
'dblclick label': 'edit',
'click .destroy': 'clear',
'keypress .edit': 'updateOnEnter',
'blur .edit': 'close',
// The "rendered" event is triggered by Thorax each time render()
// is called and the result of the template has been appended
// to the View's $el
rendered: function() {
this.$el.toggleClass( 'completed', this.model.get('completed') );
}
},
// Toggle the `"completed"` state of the model.
toggleCompleted: function() {
this.model.toggle();
},
// Switch this view into `"editing"` mode, displaying the input field.
edit: function() {
this.$el.addClass('editing');
this.$('.edit').focus();
},
// Close the `"editing"` mode, saving changes to the todo.
close: function() {
var value = this.$('.edit').val().trim();
if ( value ) {
this.model.save({ title: value });
} else {
this.clear();
}
this.$el.removeClass('editing');
},
// If you hit `enter`, we're through editing the item.
updateOnEnter: function( e ) {
if ( e.which === ENTER_KEY ) {
this.close();
}
},
// Remove the item, destroy the model from *localStorage* and delete its view.
clear: function() {
this.model.destroy();
}
});
});
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