Commit 0ae76828 authored by Addy Osmani's avatar Addy Osmani

Merge pull request #282 from beastridge/gh-pages

Thorax
parents 7982a6f8 0e2547b5
......@@ -229,6 +229,9 @@
<li>
<a href="http://todomvc.crypho.com" data-source="https://github.com/ggozad/Backbone.xmpp" data-content="Backbone.xmpp is a drop-in replacement for Backbone’s RESTful API, allowing models/collections to be persisted on XMPP Pub-Sub nodes providing real-time updates.">Backbone.xmpp</a>
</li>
<li>
<a href="labs/architecture-examples/thorax" data-source="https://github.com/walmartlabs/thorax" data-content="Thorax is a Backbone superset that integrates deeply with Handlebars to provide easy model and collection binding.">Thorax</a>
</li>
<li>
<a href="labs/architecture-examples/canjs/dojo/" data-source="http://canjs.us" data-content="CanJS with Dojo. CanJS is a client-side, JavaScript framework that makes building rich web applications easy. It provides can.Model (for connecting to RESTful JSON interfaces), can.View (for template loading and caching), can.Observe (for key-value binding), can.EJS (live binding templates), can.Control (declarative event bindings) and can.route (routing support).">CanJS (Dojo)</a>
</li>
......
Thorax TodoMVC
==============
This is a modified version of the Backbone TodoMVC app that uses [Thorax](http://thoraxjs.org).
The Backbone implementation has code to manage the list items which are present on the page. Thorax provides collection bindings with the `collection` helper, which eleminate the need for most of this code:
{{#collection todosCollection filter="filterTodoItem"
item-view="todo-item" tag="ul" id="todo-list"}}
`todosCollection` was specified in js/views/app.js:initialize, all instance variables of the view are automatically made available to the associated template. The `item-view` attribute is optional for collections, but specified here since we want to initialize an `todo-item` view for each item. This class is defined in js/views/todo-item.js and is referenced here by it's `name` attribute which is defined in that file.
The `filter` attribute specifies a function to be called for each model in the collection and hide or show that item depending on wether the function returns true or false. It is called when the collection is first rendered, then as models are added or a model fires a change event. If a `filter` event is triggered on the collection (which it is in routers/router.js:setFilter) it will force the collection to re-filter each model in the colleciton.
In this implementation the `stats` view has it's own view class and is re-rendered instead of the `app` view being re-rendered. Thorax provides the ability to embed views by name or reference with the `view` helper:
{{view "stats"}}
\ No newline at end of file
<!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 type="text/template" data-template-name="app">
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?" autofocus>
</header>
{{^empty todosCollection}}
<section id="main">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
{{#collection todosCollection filter="filterTodoItem" item-view="todo-item" tag="ul" id="todo-list"}}
<div class="view">
<input class="toggle" type="checkbox" {{#if completed}}checked{{/if}}>
<label>{{title}}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="{{title}}">
{{/collection}}
</section>
{{view "stats" tag="footer" id="footer"}}
{{/empty}}
</section>
<div id="info">
<p>Double-click to edit a todo</p>
<p>Written by <a href="https://github.com/addyosmani">Addy Osmani</a> &amp; <a href="https://github.com/beastridge">Ryan Eastridge</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</div>
</script>
<script type="text/template" data-template-name="stats">
<span id="todo-count"><strong>{{remaining}}</strong> {{itemText}} left</span>
<ul id="filters">
<li>
{{#link "/" class="selected"}}All{{/link}}
</li>
<li>
{{#link "/active"}}Active{{/link}}
</li>
<li>
{{#link "/completed"}}Completed{{/link}}
</li>
</ul>
{{#if completed}}
<button id="clear-completed">Clear completed ({{completed}})</button>
{{/if}}
</script>
<script src="../../../assets/base.js"></script>
<script src="../../../assets/jquery.min.js"></script>
<script src="../../../assets/lodash.min.js"></script>
<script src="../../../assets/handlebars.min.js"></script>
<script src="js/lib/backbone.js"></script>
<script src="js/lib/backbone-localstorage.js"></script>
<script src="js/lib/thorax.js"></script>
<script>
// Grab the text from the templates we created above
Thorax.templates = {
app: Handlebars.compile($('script[data-template-name="app"]').html()),
stats: Handlebars.compile($('script[data-template-name="stats"]').html())
};
//initialize the app object
window.app = {};
</script>
<script src="js/models/todo.js"></script>
<script src="js/collections/todos.js"></script>
<script src="js/views/todo-item.js"></script>
<script src="js/views/stats.js"></script>
<script src="js/views/app.js"></script>
<script src="js/routers/router.js"></script>
<script src="js/app.js"></script>
</body>
</html>
var ENTER_KEY = 13;
$(function() {
Backbone.history.start();
// 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();
}());
// 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.
This diff is collapsed.
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
// ----------
window.app.TodoRouter = new (Thorax.Router.extend({
routes: {
'': 'setFilter',
':filter': 'setFilter'
},
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();
}
});
});
Thorax + Lumbar TodoMVC
=======================
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);
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
define([], function() {
return {
// Which filter are we using?
TodoFilter: '', // empty, active, completed
// What is the enter key constant?
ENTER_KEY: 13
};
});
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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