Commit 8632a28c authored by Addy Osmani's avatar Addy Osmani

Merge pull request #406 from eastridge/gh-pages

Update Thorax to latest version
parents 255cb2d9 864194da
......@@ -157,14 +157,14 @@
<a href="labs/architecture-examples/backbone_marionette" data-source="http://marionettejs.com" data-content="Backbone.Marionette is a composite application library for Backbone.js that aims to simplify the construction of large scale JavaScript applications.">MarionetteJS</a>
</li>
<li class="routing labs">
<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>
<a href="labs/architecture-examples/thorax" data-source="http://thoraxjs.org" data-content="An opinionated, battle tested Backbone + Handlebars framework to build large scale web applications.">Thorax</a>
</li>
<li class="routing labs">
<a href="labs/dependency-examples/chaplin-brunch/public/" data-source="http://chaplinjs.org" data-content="Chaplin is an architecture for JavaScript applications using the Backbone.js library. Chaplin addresses Backbone’s limitations by providing a lightweight and flexible structure that features well-proven design patterns and best practises.">Chaplin + Brunch</a>
</li>
</ul>
<hr>
<h2>Apps using RequireJS/AMD</h2>
<h2>Module Loaders</h2>
<ul class="applist amd">
<li class="routing">
<a href="dependency-examples/backbone_require/" data-source="http://requirejs.org" data-content="RequireJS is a JavaScript file and module loader. It is optimized for in-browser use, but it can be used in other JavaScript environments, like Rhino and Node. Using a modular script loader like RequireJS will improve the speed and quality of your code.">Backbone.js + RequireJS</a>
......@@ -184,6 +184,9 @@
<li class="routing labs">
<a href="labs/architecture-examples/troopjs/" data-source="https://github.com/troopjs/" data-content="TroopJS attempts to package popular front-end technologies and bind them with minimal effort for the developer. It includes jQuery for DOM manipulation, ComposeJS for object composition, RequireJS for modularity and Has.js for feature detection. On top, it includes Pub/Sub support, templating, weaving (widgets to DOM) and auto-wiring.">TroopJS</a>
</li>
<li class="routing labs">
<a href="labs/dependency-examples/thorax_lumbar/" data-source="http://walmartlabs.github.com/lumbar" data-content="An opinionated, battle tested Backbone + Handlebars framework to build large scale web applications. This implementation uses Lumbar, a route based module loader.">Thorax + Lumbar</a>
</li>
</ul>
<hr>
<h2>Real-time</h2>
......
......@@ -4,12 +4,11 @@ This is a modified version of the Backbone TodoMVC app that uses [Thorax](http:/
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"}}
{{#collection 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.
`collection` was specified in `js/app.js`, 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.
Because the view containing the collection helper has an `itemFilter` method (in `views/app.js`) the collection will automatically be filtered on initial render, then as models are added, removed or changed. To force the collection to re-filter a `filter` event is triggered on the collection in `routers/router.js`.
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:
......
......@@ -16,13 +16,13 @@
<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?" autofocus>
</header>
{{^empty todosCollection}}
{{^empty collection}}
<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"}}
{{#collection item-view="todo-item" tag="ul" id="todo-list"}}
<div class="view">
<input class="toggle" type="checkbox" {{#if completed}}checked{{/if}}>
<input class="toggle" type="checkbox" {{#if completed}}checked="checked"{{/if}}>
<label>{{title}}</label>
<button class="destroy"></button>
</div>
......@@ -30,11 +30,11 @@
{{/collection}}
</section>
{{view "stats" tag="footer" id="footer"}}
{{/empty}}
{{/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>Written by <a href="https://github.com/addyosmani">Addy Osmani</a> &amp; <a href="https://github.com/eastridge">Ryan Eastridge</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</div>
</script>
......@@ -55,13 +55,8 @@
<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 src="js/lib/backbone-localstorage.js"></script>
<script>
// Grab the text from the templates we created above
Thorax.templates = {
......
......@@ -2,7 +2,9 @@ var ENTER_KEY = 13;
$(function() {
// Kick things off by creating the **App**.
var view = new Thorax.Views['app']();
$('body').append(view.el);
var view = new Thorax.Views['app']({
collection: window.app.Todos
});
view.appendTo('body');
Backbone.history.start();
});
......@@ -6,7 +6,7 @@
// The collection of todos is backed by *localStorage* instead of a remote
// server.
var TodoList = Backbone.Collection.extend({
var TodoList = Thorax.Collection.extend({
// Reference to this collection's model.
model: window.app.Todo,
......@@ -44,4 +44,7 @@
// Create our global collection of **Todos**.
window.app.Todos = new TodoList();
// Ensure that we always have data available
window.app.Todos.fetch();
}());
/**
* Backbone localStorage Adapter
* https://github.com/jeromegn/Backbone.localStorage
*/
(function(_, Backbone) {
// A simple module to replace `Backbone.sync` with *localStorage*-based
// persistence. Models are given GUIDS, and saved into a JSON object. Simple
// as that.
// Hold reference to Underscore.js and Backbone.js in the closure in order
// to make things work even if they are removed from the global namespace
// Generate four random hex digits.
function S4() {
return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
......@@ -14,71 +23,108 @@ function guid() {
// 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) {
// window.Store is deprectated, use Backbone.LocalStorage instead
Backbone.LocalStorage = window.Store = function(name) {
this.name = name;
var store = localStorage.getItem(this.name);
this.data = (store && JSON.parse(store)) || {};
var store = this.localStorage().getItem(this.name);
this.records = (store && store.split(",")) || [];
};
_.extend(Store.prototype, {
_.extend(Backbone.LocalStorage.prototype, {
// Save the current state of the **Store** to *localStorage*.
save: function() {
localStorage.setItem(this.name, JSON.stringify(this.data));
this.localStorage().setItem(this.name, this.records.join(","));
},
// 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;
if (!model.id) {
model.id = guid();
model.set(model.idAttribute, model.id);
}
this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model));
this.records.push(model.id.toString());
this.save();
return model;
return model.toJSON();
},
// Update a model by replacing its copy in `this.data`.
update: function(model) {
this.data[model.id] = model;
this.save();
return model;
this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model));
if (!_.include(this.records, model.id.toString())) this.records.push(model.id.toString()); this.save();
return model.toJSON();
},
// Retrieve a model from `this.data` by id.
find: function(model) {
return this.data[model.id];
return JSON.parse(this.localStorage().getItem(this.name+"-"+model.id));
},
// Return the array of all models currently in storage.
findAll: function() {
return _.values(this.data);
return _(this.records).chain()
.map(function(id){return JSON.parse(this.localStorage().getItem(this.name+"-"+id));}, this)
.compact()
.value();
},
// Delete a model from `this.data`, returning it.
destroy: function(model) {
delete this.data[model.id];
this.localStorage().removeItem(this.name+"-"+model.id);
this.records = _.reject(this.records, function(record_id){return record_id == model.id.toString();});
this.save();
return model;
},
localStorage: function() {
return localStorage;
}
});
// Override `Backbone.sync` to use delegate to the model or collection's
// localSync delegate to the model or collection's
// *localStorage* property, which should be an instance of `Store`.
Backbone.sync = function(method, model, options) {
var resp;
// window.Store.sync and Backbone.localSync is deprectated, use Backbone.LocalStorage.sync instead
Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(method, model, options) {
var store = model.localStorage || model.collection.localStorage;
var resp, syncDfd = $.Deferred && $.Deferred(); //If $ is having Deferred - use it.
switch (method) {
case "read": resp = model.id ? store.find(model) : store.findAll(); break;
case "read": resp = model.id != undefined ? 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);
if (options && options.success) options.success(resp);
if (syncDfd) syncDfd.resolve();
} else {
options.error("Record not found");
if (options && options.error) options.error("Record not found");
if (syncDfd) syncDfd.reject();
}
return syncDfd && syncDfd.promise();
};
Backbone.ajaxSync = Backbone.sync;
Backbone.getSyncMethod = function(model) {
if(model.localStorage || (model.collection && model.collection.localStorage))
{
return Backbone.localSync;
}
return Backbone.ajaxSync;
};
// Override 'Backbone.sync' to default to localSync,
// the original 'Backbone.sync' is still available in 'Backbone.ajaxSync'
Backbone.sync = function(method, model, options) {
return Backbone.getSyncMethod(model).apply(this, [method, model, options]);
};
})(_, Backbone);
\ No newline at end of file
This diff is collapsed.
......@@ -5,7 +5,7 @@
// ----------
// Our basic **Todo** model has `title`, `order`, and `completed` attributes.
window.app.Todo = Backbone.Model.extend({
window.app.Todo = Thorax.Model.extend({
// Default attributes for the todo
// and ensure that each todo created has `title` and `completed` keys.
......
......@@ -4,7 +4,7 @@
// Todo Router
// ----------
window.app.TodoRouter = new (Thorax.Router.extend({
window.app.TodoRouter = new (Backbone.Router.extend({
routes: {
'': 'setFilter',
':filter': 'setFilter'
......
......@@ -4,43 +4,32 @@ $(function( $ ) {
// The Application
// ---------------
// Our overall **AppView** is the top-level piece of UI.
// This view 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']
// Setting a name 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.
// Any events specified in the collection hash will be bound to the
// collection with `listenTo`. The collection was set in js/app.js
collection: {
all: 'toggleToggleAllButton'
'change:completed': 'toggleToggleAllButton',
filter: '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.$('#toggle-all')[0].checked = !this.collection.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) {
// When this function is specified, items will only be shown
// when this function returns true
itemFilter: function(model) {
return model.isVisible();
},
......@@ -48,7 +37,7 @@ $(function( $ ) {
newAttributes: function() {
return {
title: this.$('#new-todo').val().trim(),
order: window.app.Todos.nextOrder(),
order: this.collection.nextOrder(),
completed: false
};
},
......@@ -60,16 +49,15 @@ $(function( $ ) {
return;
}
window.app.Todos.create( this.newAttributes() );
this.collection.create( this.newAttributes() );
this.$('#new-todo').val('');
},
toggleAllComplete: function() {
var completed = this.$('#toggle-all')[0].checked;
window.app.Todos.each(function( todo ) {
this.collection.each(function( todo ) {
todo.save({
'completed': completed
completed: completed
});
});
}
......
......@@ -13,9 +13,9 @@ Thorax.View.extend({
// 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.listenTo(window.app.Todos, 'all', _.debounce(function() {
this.render();
}), this);
}));
},
// Clear all completed todo items, destroying their models.
......
......@@ -11,33 +11,13 @@
"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",
"src": "src/js/lib/thorax.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
......@@ -79,7 +59,8 @@
}
},
"templates": {
"template": "Thorax.templates['{{{without-extension name}}}'] = '{{{data}}}';",
"root": "templates/",
"template": "Thorax.templates['{{{without-extension name}}}'] = {{handlebarsCall}}({{{data}}});",
"src/js/views/app.js": [
"src/templates/app.handlebars"
],
......
......@@ -2,7 +2,7 @@
"name": "thorax-lumbar-todomvc",
"version": "0.0.1",
"devDependencies": {
"lumbar": "git://github.com/beastridge/lumbar.git"
"lumbar": "~2.0.0"
},
"scripts": {
"start": "lumbar build lumbar.json public"
......
Application['todomvc'] = (function() {
var module = {exports: {}};
var exports = module.exports;
/* router : todomvc */
var module = {exports: {}};
var exports = module.exports;
Application['todomvc'] = exports;
/* router : todomvc */
module.name = "todomvc";
module.routes = {"":"setFilter",":filter":"setFilter"};
(function() {
......@@ -11,7 +14,7 @@ module.routes = {"":"setFilter",":filter":"setFilter"};
// ----------
// Our basic **Todo** model has `title`, `order`, and `completed` attributes.
window.app.Todo = Backbone.Model.extend({
window.app.Todo = Thorax.Model.extend({
// Default attributes for the todo
// and ensure that each todo created has `title` and `completed` keys.
......@@ -41,6 +44,7 @@ module.routes = {"":"setFilter",":filter":"setFilter"};
});
}());
;;
(function() {
'use strict';
......@@ -50,13 +54,13 @@ module.routes = {"":"setFilter",":filter":"setFilter"};
// The collection of todos is backed by *localStorage* instead of a remote
// server.
var TodoList = Backbone.Collection.extend({
var TodoList = Thorax.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'),
localStorage: new Store('todos-backbone-thorax'),
// Filter down the list of all todo items that are finished.
completed: function() {
......@@ -88,7 +92,11 @@ module.routes = {"":"setFilter",":filter":"setFilter"};
// Create our global collection of **Todos**.
window.app.Todos = new TodoList();
// Ensure that we always have data available
window.app.Todos.fetch();
}());
;;
$(function() {
'use strict';
......@@ -157,6 +165,7 @@ $(function() {
}
});
});
;;
Thorax.View.extend({
name: 'stats',
......@@ -173,9 +182,9 @@ Thorax.View.extend({
// 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.listenTo(window.app.Todos, 'all', _.debounce(function() {
this.render();
}), this);
}));
},
// Clear all completed todo items, destroying their models.
......@@ -206,50 +215,40 @@ Thorax.View.extend({
.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( $ ) {
});
;;
Thorax.templates['src/templates/stats'] = Handlebars.compile('<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.
// This view 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']
// Setting a name 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.
// Any events specified in the collection hash will be bound to the
// collection with `listenTo`. The collection was set in js/app.js
collection: {
all: 'toggleToggleAllButton'
'change:completed': 'toggleToggleAllButton',
filter: '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.$('#toggle-all')[0].checked = !this.collection.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) {
// When this function is specified, items will only be shown
// when this function returns true
itemFilter: function(model) {
return model.isVisible();
},
......@@ -257,7 +256,7 @@ Thorax.templates['src/templates/stats'] = '<span id=\"todo-count\"><strong>{{rem
newAttributes: function() {
return {
title: this.$('#new-todo').val().trim(),
order: window.app.Todos.nextOrder(),
order: this.collection.nextOrder(),
completed: false
};
},
......@@ -269,23 +268,23 @@ Thorax.templates['src/templates/stats'] = '<span id=\"todo-count\"><strong>{{rem
return;
}
window.app.Todos.create( this.newAttributes() );
this.collection.create( this.newAttributes() );
this.$('#new-todo').val('');
},
toggleAllComplete: function() {
var completed = this.$('#toggle-all')[0].checked;
window.app.Todos.each(function( todo ) {
this.collection.each(function( todo ) {
todo.save({
'completed': completed
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() {
Thorax.templates['src/templates/app'] = Handlebars.compile('<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 collection}}\n <section id=\"main\">\n <input id=\"toggle-all\" type=\"checkbox\">\n <label for=\"toggle-all\">Mark all as complete</label>\n {{#collection item-view=\"todo-item\" tag=\"ul\" id=\"todo-list\"}}\n <div class=\"view\">\n <input class=\"toggle\" type=\"checkbox\" {{#if completed}}checked=\"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/eastridge\">Ryan Eastridge</a></p>\n <p>Part of <a href=\"http://todomvc.com\">TodoMVC</a></p>\n</div>\n');(function() {
'use strict';
// Todo Router
......@@ -307,14 +306,23 @@ Thorax.templates['src/templates/app'] = '<section id=\"todoapp\">\n <header id=
}));
}());
;;
var ENTER_KEY = 13;
$(function() {
// Kick things off by creating the **App**.
var view = new Thorax.Views['app']();
$('body').append(view.el);
var view = new Thorax.Views['app']({
collection: window.app.Todos
});
view.appendTo('body');
});
;;
return module.exports;
if (Application['todomvc'] !== module.exports) {
console.warn("Application['todomvc'] internally differs from global");
}
return module.exports;
}).call(this);
......@@ -2,6 +2,8 @@ var ENTER_KEY = 13;
$(function() {
// Kick things off by creating the **App**.
var view = new Thorax.Views['app']();
$('body').append(view.el);
var view = new Thorax.Views['app']({
collection: window.app.Todos
});
view.appendTo('body');
});
......@@ -6,13 +6,13 @@
// The collection of todos is backed by *localStorage* instead of a remote
// server.
var TodoList = Backbone.Collection.extend({
var TodoList = Thorax.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'),
localStorage: new Store('todos-backbone-thorax'),
// Filter down the list of all todo items that are finished.
completed: function() {
......@@ -44,4 +44,7 @@
// Create our global collection of **Todos**.
window.app.Todos = new TodoList();
// Ensure that we always have data available
window.app.Todos.fetch();
}());
/**
* Backbone localStorage Adapter
* https://github.com/jeromegn/Backbone.localStorage
*/
(function(_, Backbone) {
// A simple module to replace `Backbone.sync` with *localStorage*-based
// persistence. Models are given GUIDS, and saved into a JSON object. Simple
// as that.
// Hold reference to Underscore.js and Backbone.js in the closure in order
// to make things work even if they are removed from the global namespace
// Generate four random hex digits.
function S4() {
return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
......@@ -14,71 +23,108 @@ function guid() {
// 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) {
// window.Store is deprectated, use Backbone.LocalStorage instead
Backbone.LocalStorage = window.Store = function(name) {
this.name = name;
var store = localStorage.getItem(this.name);
this.data = (store && JSON.parse(store)) || {};
var store = this.localStorage().getItem(this.name);
this.records = (store && store.split(",")) || [];
};
_.extend(Store.prototype, {
_.extend(Backbone.LocalStorage.prototype, {
// Save the current state of the **Store** to *localStorage*.
save: function() {
localStorage.setItem(this.name, JSON.stringify(this.data));
this.localStorage().setItem(this.name, this.records.join(","));
},
// 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;
if (!model.id) {
model.id = guid();
model.set(model.idAttribute, model.id);
}
this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model));
this.records.push(model.id.toString());
this.save();
return model;
return model.toJSON();
},
// Update a model by replacing its copy in `this.data`.
update: function(model) {
this.data[model.id] = model;
this.save();
return model;
this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model));
if (!_.include(this.records, model.id.toString())) this.records.push(model.id.toString()); this.save();
return model.toJSON();
},
// Retrieve a model from `this.data` by id.
find: function(model) {
return this.data[model.id];
return JSON.parse(this.localStorage().getItem(this.name+"-"+model.id));
},
// Return the array of all models currently in storage.
findAll: function() {
return _.values(this.data);
return _(this.records).chain()
.map(function(id){return JSON.parse(this.localStorage().getItem(this.name+"-"+id));}, this)
.compact()
.value();
},
// Delete a model from `this.data`, returning it.
destroy: function(model) {
delete this.data[model.id];
this.localStorage().removeItem(this.name+"-"+model.id);
this.records = _.reject(this.records, function(record_id){return record_id == model.id.toString();});
this.save();
return model;
},
localStorage: function() {
return localStorage;
}
});
// Override `Backbone.sync` to use delegate to the model or collection's
// localSync delegate to the model or collection's
// *localStorage* property, which should be an instance of `Store`.
Backbone.sync = function(method, model, options) {
var resp;
// window.Store.sync and Backbone.localSync is deprectated, use Backbone.LocalStorage.sync instead
Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(method, model, options) {
var store = model.localStorage || model.collection.localStorage;
var resp, syncDfd = $.Deferred && $.Deferred(); //If $ is having Deferred - use it.
switch (method) {
case "read": resp = model.id ? store.find(model) : store.findAll(); break;
case "read": resp = model.id != undefined ? 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);
if (options && options.success) options.success(resp);
if (syncDfd) syncDfd.resolve();
} else {
options.error("Record not found");
if (options && options.error) options.error("Record not found");
if (syncDfd) syncDfd.reject();
}
return syncDfd && syncDfd.promise();
};
Backbone.ajaxSync = Backbone.sync;
Backbone.getSyncMethod = function(model) {
if(model.localStorage || (model.collection && model.collection.localStorage))
{
return Backbone.localSync;
}
return Backbone.ajaxSync;
};
// Override 'Backbone.sync' to default to localSync,
// the original 'Backbone.sync' is still available in 'Backbone.ajaxSync'
Backbone.sync = function(method, model, options) {
return Backbone.getSyncMethod(model).apply(this, [method, model, options]);
};
})(_, Backbone);
\ No newline at end of file
......@@ -5,7 +5,7 @@
// ----------
// Our basic **Todo** model has `title`, `order`, and `completed` attributes.
window.app.Todo = Backbone.Model.extend({
window.app.Todo = Thorax.Model.extend({
// Default attributes for the todo
// and ensure that each todo created has `title` and `completed` keys.
......
......@@ -4,43 +4,32 @@ $(function( $ ) {
// The Application
// ---------------
// Our overall **AppView** is the top-level piece of UI.
// This view 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']
// Setting a name 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.
// Any events specified in the collection hash will be bound to the
// collection with `listenTo`. The collection was set in js/app.js
collection: {
all: 'toggleToggleAllButton'
'change:completed': 'toggleToggleAllButton',
filter: '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.$('#toggle-all')[0].checked = !this.collection.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) {
// When this function is specified, items will only be shown
// when this function returns true
itemFilter: function(model) {
return model.isVisible();
},
......@@ -48,7 +37,7 @@ $(function( $ ) {
newAttributes: function() {
return {
title: this.$('#new-todo').val().trim(),
order: window.app.Todos.nextOrder(),
order: this.collection.nextOrder(),
completed: false
};
},
......@@ -60,16 +49,15 @@ $(function( $ ) {
return;
}
window.app.Todos.create( this.newAttributes() );
this.collection.create( this.newAttributes() );
this.$('#new-todo').val('');
},
toggleAllComplete: function() {
var completed = this.$('#toggle-all')[0].checked;
window.app.Todos.each(function( todo ) {
this.collection.each(function( todo ) {
todo.save({
'completed': completed
completed: completed
});
});
}
......
......@@ -13,9 +13,9 @@ Thorax.View.extend({
// 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.listenTo(window.app.Todos, 'all', _.debounce(function() {
this.render();
}), this);
}));
},
// Clear all completed todo items, destroying their models.
......
......@@ -3,13 +3,13 @@
<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?" autofocus>
</header>
{{^empty todosCollection}}
{{^empty collection}}
<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"}}
{{#collection item-view="todo-item" tag="ul" id="todo-list"}}
<div class="view">
<input class="toggle" type="checkbox" {{#if completed}}checked{{/if}}>
<input class="toggle" type="checkbox" {{#if completed}}checked="checked"{{/if}}>
<label>{{title}}</label>
<button class="destroy"></button>
</div>
......@@ -17,10 +17,10 @@
{{/collection}}
</section>
{{view "stats" tag="footer" id="footer"}}
{{/empty}}
{{/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>Written by <a href="https://github.com/addyosmani">Addy Osmani</a> &amp; <a href="https://github.com/eastridge">Ryan Eastridge</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</div>
Thorax + RequireJS TodoMVC
==========================
Unlike the vanilla Thorax and Thorax + Lumbar implementations, this example does not make use of the Thorax registry / named views and templates. The views are still assigned a name property for debugging and consistency (each view's element will be assigned a data-view-name HTML attribute), but each dependency is explicitly pulled in via `define` instead of being pulled in by the `view` or `template` helpers, or automatic assignment of templates to views when they share a name. For example in the require.js app:
# views/app.js
template: Handlebars.compile(appTemplate),
initialize: function() {
this.statsView = new StatsView;
}
# templates/app.handlebars
{{view statsView}}
In the Lumbar or vanilla Thorax implementations simply setting the name will auto assign the template of the same name, and the "stats" view can be included by name, rather than having to first initialize it.
# views/app.js
name: 'app'
# templates/app.handlebars
{{view "stats"}}
Thorax is flexible enough that the approach used in the require.js app will still work within lumbar or vanilla Thorax implementations, but the approach used in the require.js environment is the only one that will work with require.js
\ 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 + RequireJS • TodoMVC</title>
<link rel="stylesheet" href="../../../assets/base.css">
<script src="../../../assets/base.js"></script>
<script data-main="js/main" src="js/lib/require/require.js"></script>
<!--[if IE]>
<script src="../../../assets/ie.js"></script>
<![endif]-->
</head>
<body>
</body>
</html>
define([
'underscore',
'backbone',
'lib/backbone/localstorage',
'models/todo'
], function( _, Backbone, Store, Todo ) {
var TodoList = Backbone.Collection.extend({
// Reference to this collection's model.
model: 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');
}
});
return new TodoList();
});
define([], function() {
return {
// Which filter are we using?
TodoFilter: '', // empty, active, completed
// What is the enter key constant?
ENTER_KEY: 13
};
});
define(["underscore","backbone"],function(_,Backbone){function S4(){return((1+Math.random())*65536|0).toString(16).substring(1)}function guid(){return S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()}var Store=function(name){this.name=name;var store=localStorage.getItem(this.name);this.data=store&&JSON.parse(store)||{}};_.extend(Store.prototype,{save:function(){localStorage.setItem(this.name,JSON.stringify(this.data))},create:function(model){if(!model.id)model.id=model.attributes.id=guid();
this.data[model.id]=model;this.save();return model},update:function(model){this.data[model.id]=model;this.save();return model},find:function(model){return this.data[model.id]},findAll:function(){return _.values(this.data)},destroy:function(model){delete this.data[model.id];this.save();return model}});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")};return Store});
// Require.js allows us to configure shortcut alias
require.config({
// The shim config allows us to configure dependencies for
// scripts that do not call define() to register a module
shim: {
'underscore': {
exports: '_'
},
'backbone': {
deps: [
'underscore',
'jquery'
],
exports: 'Backbone'
},
'thorax': {
deps: [
'underscore',
'backbone',
'jquery',
'handlebars'
],
exports: 'Thorax'
}
},
paths: {
jquery: 'lib/jquery/jquery.min',
underscore: '../../../../assets/lodash.min',
backbone: 'lib/backbone/backbone',
handlebars: '../../../../assets/handlebars.min',
thorax: 'lib/thorax',
text: 'lib/require/text'
}
});
require([
'views/app',
'routers/todomvc'
], function( AppView, TodoMVCRouter ) {
// Initialize routing and start Backbone.history()
new TodoMVCRouter();
// Initialize the application view
var view = new AppView();
$('body').append(view.el);
Backbone.history.start();
});
define([
'underscore',
'backbone',
'common'
], function( _, Backbone, Common ) {
return 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 (Common.TodoFilter === '') {
return true;
} else if (Common.TodoFilter === 'completed') {
return isCompleted;
} else if (Common.TodoFilter === 'active') {
return !isCompleted;
}
}
});
});
define([
'jquery',
'backbone',
'thorax',
'collections/todos',
'common'
], function( $, Backbone, Thorax, Todos, Common ) {
return Thorax.Router.extend({
routes: {
"": "setFilter",
":filter": "setFilter"
},
setFilter: function( param ) {
// Set the current filter to be used
Common.TodoFilter = param ? param.trim().replace(/^\//, '') : '';
// Thorax listens for a `filter` event which will
// force the collection to re-filter
Todos.trigger('filter');
}
});
});
<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=todoItemView 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 statsView}}
{{/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>
<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}}
define([
'jquery',
'underscore',
'backbone',
'thorax',
'collections/todos',
'views/todo-item',
'views/stats',
'text!templates/app.handlebars',
'common'
], function( $, _, Backbone, Thorax, Todos, TodoItemView, StatsView, appTemplate, Common ) {
return Thorax.View.extend({
// In a require.js application the name is primarily for
// consistency and debugging purposes
name: 'app',
template: Handlebars.compile(appTemplate),
// 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.todoItemView = TodoItemView;
this.statsView = new StatsView;
this.todosCollection = 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: 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 !== Common.ENTER_KEY || !this.$('#new-todo').val().trim() ) {
return;
}
Todos.create( this.newAttributes() );
this.$('#new-todo').val('');
},
toggleAllComplete: function() {
var completed = this.$('#toggle-all')[0].checked;
Todos.each(function( todo ) {
todo.save({
'completed': completed
});
});
}
});
});
define([
'jquery',
'underscore',
'backbone',
'thorax',
'collections/todos',
'text!templates/stats.handlebars',
'common'
], function( $, _, Backbone, Thorax, Todos, statsTemplate, Common ) {
return Thorax.View.extend({
name: 'stats',
tagName: 'footer',
id: 'footer',
template: Handlebars.compile(statsTemplate),
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
Todos.on('all', _.debounce(function() {
this.render();
}), this);
},
// Clear all completed todo items, destroying their models.
clearCompleted: function() {
_.each( 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 = Todos.remaining().length;
return {
itemText: remaining === 1 ? 'item' : 'items',
completed: Todos.completed().length,
remaining: remaining
};
},
// Highlight which filter will appear to be active
highlightFilter: function() {
this.$('#filters li a')
.removeClass('selected')
.filter('[href="#/' + ( Common.TodoFilter || '' ) + '"]')
.addClass('selected');
}
});
});
\ No newline at end of file
define([
'jquery',
'underscore',
'backbone',
'thorax',
'common'
], function( $, _, Backbone, Common ) {
// This view has no template assigned in the class definition
// as it will recieve the capture block from the collection
// helper in templates/app.handlebars as it's template
return Thorax.View.extend({
name: 'todo-item',
//... is a list tag.
tagName: 'li',
// 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 === Common.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