Commit 23457d50 authored by Ryan Eastridge's avatar Ryan Eastridge

Update Thorax Todo app to latest version of Thorax

parent 410a831f
...@@ -4,12 +4,11 @@ This is a modified version of the Backbone TodoMVC app that uses [Thorax](http:/ ...@@ -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: 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" {{#collection item-view="todo-item" tag="ul" id="todo-list"}}
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: 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 @@ ...@@ -16,13 +16,13 @@
<h1>todos</h1> <h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?" autofocus> <input id="new-todo" placeholder="What needs to be done?" autofocus>
</header> </header>
{{^empty todosCollection}} {{^empty collection}}
<section id="main"> <section id="main">
<input id="toggle-all" type="checkbox"> <input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label> <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"> <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> <label>{{title}}</label>
<button class="destroy"></button> <button class="destroy"></button>
</div> </div>
...@@ -30,11 +30,11 @@ ...@@ -30,11 +30,11 @@
{{/collection}} {{/collection}}
</section> </section>
{{view "stats" tag="footer" id="footer"}} {{view "stats" tag="footer" id="footer"}}
{{/empty}} {{/empty}}
</section> </section>
<div id="info"> <div id="info">
<p>Double-click to edit a todo</p> <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> <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</div> </div>
</script> </script>
...@@ -55,13 +55,8 @@ ...@@ -55,13 +55,8 @@
<button id="clear-completed">Clear completed ({{completed}})</button> <button id="clear-completed">Clear completed ({{completed}})</button>
{{/if}} {{/if}}
</script> </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/thorax.js"></script>
<script src="js/lib/backbone-localstorage.js"></script>
<script> <script>
// Grab the text from the templates we created above // Grab the text from the templates we created above
Thorax.templates = { Thorax.templates = {
......
...@@ -2,7 +2,9 @@ var ENTER_KEY = 13; ...@@ -2,7 +2,9 @@ var ENTER_KEY = 13;
$(function() { $(function() {
// Kick things off by creating the **App**. // Kick things off by creating the **App**.
var view = new Thorax.Views['app'](); var view = new Thorax.Views['app']({
$('body').append(view.el); collection: window.app.Todos
});
view.appendTo('body');
Backbone.history.start(); Backbone.history.start();
}); });
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
// The collection of todos is backed by *localStorage* instead of a remote // The collection of todos is backed by *localStorage* instead of a remote
// server. // server.
var TodoList = Backbone.Collection.extend({ var TodoList = Thorax.Collection.extend({
// Reference to this collection's model. // Reference to this collection's model.
model: window.app.Todo, model: window.app.Todo,
...@@ -44,4 +44,7 @@ ...@@ -44,4 +44,7 @@
// Create our global collection of **Todos**. // Create our global collection of **Todos**.
window.app.Todos = new TodoList(); 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 // A simple module to replace `Backbone.sync` with *localStorage*-based
// persistence. Models are given GUIDS, and saved into a JSON object. Simple // persistence. Models are given GUIDS, and saved into a JSON object. Simple
// as that. // 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. // Generate four random hex digits.
function S4() { function S4() {
return (((1+Math.random())*0x10000)|0).toString(16).substring(1); return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
...@@ -14,71 +23,108 @@ function guid() { ...@@ -14,71 +23,108 @@ function guid() {
// Our Store is represented by a single JS object in *localStorage*. Create it // 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. // 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; this.name = name;
var store = localStorage.getItem(this.name); var store = this.localStorage().getItem(this.name);
this.data = (store && JSON.parse(store)) || {}; this.records = (store && store.split(",")) || [];
}; };
_.extend(Store.prototype, { _.extend(Backbone.LocalStorage.prototype, {
// Save the current state of the **Store** to *localStorage*. // Save the current state of the **Store** to *localStorage*.
save: function() { 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 // Add a model, giving it a (hopefully)-unique GUID, if it doesn't already
// have an id of it's own. // have an id of it's own.
create: function(model) { create: function(model) {
if (!model.id) model.id = model.attributes.id = guid(); if (!model.id) {
this.data[model.id] = model; 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(); this.save();
return model; return model.toJSON();
}, },
// Update a model by replacing its copy in `this.data`. // Update a model by replacing its copy in `this.data`.
update: function(model) { update: function(model) {
this.data[model.id] = model; this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model));
this.save(); if (!_.include(this.records, model.id.toString())) this.records.push(model.id.toString()); this.save();
return model; return model.toJSON();
}, },
// Retrieve a model from `this.data` by id. // Retrieve a model from `this.data` by id.
find: function(model) { 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. // Return the array of all models currently in storage.
findAll: function() { 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. // Delete a model from `this.data`, returning it.
destroy: function(model) { 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(); this.save();
return model; 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`. // *localStorage* property, which should be an instance of `Store`.
Backbone.sync = function(method, model, options) { // 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 resp;
var store = model.localStorage || model.collection.localStorage; var store = model.localStorage || model.collection.localStorage;
var resp, syncDfd = $.Deferred && $.Deferred(); //If $ is having Deferred - use it.
switch (method) { 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 "create": resp = store.create(model); break;
case "update": resp = store.update(model); break; case "update": resp = store.update(model); break;
case "delete": resp = store.destroy(model); break; case "delete": resp = store.destroy(model); break;
} }
if (resp) { if (resp) {
options.success(resp); if (options && options.success) options.success(resp);
if (syncDfd) syncDfd.resolve();
} else { } 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 @@ ...@@ -5,7 +5,7 @@
// ---------- // ----------
// Our basic **Todo** model has `title`, `order`, and `completed` attributes. // 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 // Default attributes for the todo
// and ensure that each todo created has `title` and `completed` keys. // and ensure that each todo created has `title` and `completed` keys.
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
// Todo Router // Todo Router
// ---------- // ----------
window.app.TodoRouter = new (Thorax.Router.extend({ window.app.TodoRouter = new (Backbone.Router.extend({
routes: { routes: {
'': 'setFilter', '': 'setFilter',
':filter': 'setFilter' ':filter': 'setFilter'
......
...@@ -4,43 +4,32 @@ $(function( $ ) { ...@@ -4,43 +4,32 @@ $(function( $ ) {
// The Application // The Application
// --------------- // ---------------
// Our overall **AppView** is the top-level piece of UI. // This view is the top-level piece of UI.
Thorax.View.extend({ Thorax.View.extend({
// This will assign the template Thorax.templates['app'] to the view and // Setting a name will assign the template Thorax.templates['app']
// create a view class at Thorax.Views['app'] // to the view and create a view class at Thorax.Views['app']
name: 'app', name: 'app',
// Delegated events for creating new items, and clearing completed ones. // Delegated events for creating new items, and clearing completed ones.
events: { events: {
'keypress #new-todo': 'createOnEnter', 'keypress #new-todo': 'createOnEnter',
'click #toggle-all': 'toggleAllComplete', 'click #toggle-all': 'toggleAllComplete',
// The collection helper in the template will bind the collection // Any events specified in the collection hash will be bound to the
// to the view. Any events in this hash will be bound to the // collection with `listenTo`. The collection was set in js/app.js
// collection.
collection: { collection: {
all: 'toggleToggleAllButton' 'change:completed': 'toggleToggleAllButton',
filter: 'toggleToggleAllButton'
}, },
rendered: '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() { 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 // When this function is specified, items will only be shown
// and will be called each time a model changes, or for each item // when this function returns true
// when the collection is rendered itemFilter: function(model) {
filterTodoItem: function(model) {
return model.isVisible(); return model.isVisible();
}, },
...@@ -48,7 +37,7 @@ $(function( $ ) { ...@@ -48,7 +37,7 @@ $(function( $ ) {
newAttributes: function() { newAttributes: function() {
return { return {
title: this.$('#new-todo').val().trim(), title: this.$('#new-todo').val().trim(),
order: window.app.Todos.nextOrder(), order: this.collection.nextOrder(),
completed: false completed: false
}; };
}, },
...@@ -60,16 +49,15 @@ $(function( $ ) { ...@@ -60,16 +49,15 @@ $(function( $ ) {
return; return;
} }
window.app.Todos.create( this.newAttributes() ); this.collection.create( this.newAttributes() );
this.$('#new-todo').val(''); this.$('#new-todo').val('');
}, },
toggleAllComplete: function() { toggleAllComplete: function() {
var completed = this.$('#toggle-all')[0].checked; var completed = this.$('#toggle-all')[0].checked;
this.collection.each(function( todo ) {
window.app.Todos.each(function( todo ) {
todo.save({ todo.save({
'completed': completed completed: completed
}); });
}); });
} }
......
...@@ -13,9 +13,9 @@ Thorax.View.extend({ ...@@ -13,9 +13,9 @@ Thorax.View.extend({
// Whenever the Todos collection changes re-render the stats // Whenever the Todos collection changes re-render the stats
// render() needs to be called with no arguments, otherwise calling // render() needs to be called with no arguments, otherwise calling
// it with arguments will insert the arguments as content // 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.render();
}), this); }));
}, },
// Clear all completed todo items, destroying their models. // Clear all completed todo items, destroying their models.
......
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