Commit a7a4d9ce authored by Sindre Sorhus's avatar Sindre Sorhus

Backbone+Require app improvements

- Cleanup
- Code style
- Fix bug with removing todo after editing and the todo is empty
- Update dependencies
parent 5995093b
...@@ -3,9 +3,13 @@ ...@@ -3,9 +3,13 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Backbone.js</title> <title>Backbone.js + RequireJS • TodoMVC</title>
<link rel="stylesheet" href="../../assets/base.css"> <link rel="stylesheet" href="../../assets/base.css">
<script data-main="js/main" src="js/libs/require/require.js"></script> <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> </head>
<body> <body>
<section id="todoapp"> <section id="todoapp">
...@@ -26,4 +30,4 @@ ...@@ -26,4 +30,4 @@
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p> <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer> </footer>
</body> </body>
</html> </html>
\ No newline at end of file
...@@ -3,40 +3,41 @@ define([ ...@@ -3,40 +3,41 @@ define([
'backbone', 'backbone',
'lib/backbone/localstorage', 'lib/backbone/localstorage',
'models/todo' 'models/todo'
], function(_, Backbone, Store, Todo){ ], function( _, Backbone, Store, Todo ) {
var TodosCollection = Backbone.Collection.extend({ var TodosCollection = Backbone.Collection.extend({
// Reference to this collection's model. // Reference to this collection's model.
model: Todo, model: Todo,
// Save all of the todo items under the `"todos"` namespace. // Save all of the todo items under the `"todos"` namespace.
localStorage: new Store("todos-backbone"), localStorage: new Store('todos-backbone'),
// Filter down the list of all todo items that are finished. // Filter down the list of all todo items that are finished.
completed: function() { completed: function() {
return this.filter(function(todo){ return todo.get('completed'); }); return this.filter(function( todo ) {
return todo.get('completed');
});
}, },
// Filter down the list to only todo items that are still not finished. // Filter down the list to only todo items that are still not finished.
remaining: function() { remaining: function() {
return this.without.apply(this, this.completed()); return this.without.apply( this, this.completed() );
}, },
// We keep the Todos in sequential order, despite being saved by unordered // 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. // GUID in the database. This generates the next order number for new items.
nextOrder: function() { nextOrder: function() {
if ( !this.length ){ if ( !this.length ) {
return 1; return 1;
} }
return this.last().get('order') + 1; return this.last().get('order') + 1;
}, },
// Todos are sorted by their original insertion order. // Todos are sorted by their original insertion order.
comparator: function(todo) { comparator: function( todo ) {
return todo.get('order'); return todo.get('order');
} }
}); });
return new TodosCollection;
return new TodosCollection();
}); });
define([], function(){ define([], function() {
return { return {
// Which filter are we using? // Which filter are we using?
TodoFilter: "", // empty, active, completed TodoFilter: '', // empty, active, completed
// What is the enter key constant? // What is the enter key constant?
ENTER_KEY: 13 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});
This diff is collapsed.
This diff is collapsed.
define(['underscore', 'backbone'], 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.
// 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");
}
};
return Store;
});
/*
RequireJS text 1.0.6 Copyright (c) 2010-2011, The Dojo Foundation All Rights Reserved.
Available via the MIT or new BSD license.
see: http://github.com/jrburke/requirejs for details
*/
(function(){var k=["Msxml2.XMLHTTP","Microsoft.XMLHTTP","Msxml2.XMLHTTP.4.0"],n=/^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im,o=/<body[^>]*>\s*([\s\S]+)\s*<\/body>/im,i=typeof location!=="undefined"&&location.href,p=i&&location.protocol&&location.protocol.replace(/\:/,""),q=i&&location.hostname,r=i&&(location.port||void 0),j=[];define(function(){var g,h,l;typeof window!=="undefined"&&window.navigator&&window.document?h=function(a,c){var b=g.createXhr();b.open("GET",a,!0);b.onreadystatechange=
function(){b.readyState===4&&c(b.responseText)};b.send(null)}:typeof process!=="undefined"&&process.versions&&process.versions.node?(l=require.nodeRequire("fs"),h=function(a,c){var b=l.readFileSync(a,"utf8");b.indexOf("\ufeff")===0&&(b=b.substring(1));c(b)}):typeof Packages!=="undefined"&&(h=function(a,c){var b=new java.io.File(a),e=java.lang.System.getProperty("line.separator"),b=new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(b),"utf-8")),d,f,g="";try{d=new java.lang.StringBuffer;
(f=b.readLine())&&f.length()&&f.charAt(0)===65279&&(f=f.substring(1));for(d.append(f);(f=b.readLine())!==null;)d.append(e),d.append(f);g=String(d.toString())}finally{b.close()}c(g)});return g={version:"1.0.6",strip:function(a){if(a){var a=a.replace(n,""),c=a.match(o);c&&(a=c[1])}else a="";return a},jsEscape:function(a){return a.replace(/(['\\])/g,"\\$1").replace(/[\f]/g,"\\f").replace(/[\b]/g,"\\b").replace(/[\n]/g,"\\n").replace(/[\t]/g,"\\t").replace(/[\r]/g,"\\r")},createXhr:function(){var a,c,
b;if(typeof XMLHttpRequest!=="undefined")return new XMLHttpRequest;else for(c=0;c<3;c++){b=k[c];try{a=new ActiveXObject(b)}catch(e){}if(a){k=[b];break}}if(!a)throw Error("createXhr(): XMLHttpRequest not available");return a},get:h,parseName:function(a){var c=!1,b=a.indexOf("."),e=a.substring(0,b),a=a.substring(b+1,a.length),b=a.indexOf("!");b!==-1&&(c=a.substring(b+1,a.length),c=c==="strip",a=a.substring(0,b));return{moduleName:e,ext:a,strip:c}},xdRegExp:/^((\w+)\:)?\/\/([^\/\\]+)/,useXhr:function(a,
c,b,e){var d=g.xdRegExp.exec(a),f;if(!d)return!0;a=d[2];d=d[3];d=d.split(":");f=d[1];d=d[0];return(!a||a===c)&&(!d||d===b)&&(!f&&!d||f===e)},finishLoad:function(a,c,b,e,d){b=c?g.strip(b):b;d.isBuild&&(j[a]=b);e(b)},load:function(a,c,b,e){if(e.isBuild&&!e.inlineText)b();else{var d=g.parseName(a),f=d.moduleName+"."+d.ext,m=c.toUrl(f),h=e&&e.text&&e.text.useXhr||g.useXhr;!i||h(m,p,q,r)?g.get(m,function(c){g.finishLoad(a,d.strip,c,b,e)}):c([f],function(a){g.finishLoad(d.moduleName+"."+d.ext,d.strip,a,
b,e)})}},write:function(a,c,b){if(c in j){var e=g.jsEscape(j[c]);b.asModule(a+"!"+c,"define(function () { return '"+e+"';});\n")}},writeFile:function(a,c,b,e,d){var c=g.parseName(c),f=c.moduleName+"."+c.ext,h=b.toUrl(c.moduleName+"."+c.ext)+".js";g.load(f,b,function(){var b=function(a){return e(h,a)};b.asModule=function(a,b){return e.asModule(a,h,b)};g.write(a,f,b,d)},d)}}})})();
// Require.js allows us to configure shortcut alias // Require.js allows us to configure shortcut alias
require.config({ require.config({
// The shim config allows us to configure dependencies for
// The shim config allows us to configure dependencies for // scripts that do not call define() to register a module
// scripts that do not call define() to register a module
shim: { shim: {
'underscore': { 'underscore': {
exports: '_' exports: '_'
}, },
'backbone': {
'backbone': { deps: [
deps: ['underscore', 'jquery'], 'underscore',
exports: 'Backbone' 'jquery'
} ],
exports: 'Backbone'
}
}, },
paths: { paths: {
jquery: 'libs/jquery/jquery-min', jquery: 'lib/jquery/jquery.min',
underscore: 'libs/underscore/underscore', underscore: 'lib/underscore/underscore',
backbone: 'libs/backbone/backbone', backbone: 'lib/backbone/backbone',
text: 'libs/require/text' text: 'lib/require/text'
} }
}); });
require(['views/app', 'routers/router'], function( AppView, Workspace ){ require([
'views/app',
// Initialize routing and start Backbone.history() 'routers/router'
var TodoRouter = new Workspace; ], function( AppView, Workspace ) {
Backbone.history.start(); // Initialize routing and start Backbone.history()
new Workspace();
// Initialize the application view Backbone.history.start();
var app_view = new AppView;
// Initialize the application view
new AppView();
}); });
define(['underscore', 'backbone'], function(_, Backbone) { define([
var TodoModel = Backbone.Model.extend({ 'underscore',
'backbone'
], function( _, Backbone ) {
var TodoModel = Backbone.Model.extend({
// Default attributes for the todo. // Default attributes for the todo.
defaults: { defaults: {
title: "empty todo...", title: '',
completed: false completed: false
}, },
// Ensure that each todo created has `title`. // Ensure that each todo created has `title`.
initialize: function() { initialize: function() {
if (!this.get("title")) { if ( !this.get('title') ) {
this.set({"title": this.defaults.title}); this.set({
'title': this.defaults.title
});
} }
}, },
// Toggle the `completed` state of this todo item. // Toggle the `completed` state of this todo item.
toggle: function() { toggle: function() {
this.save({completed: !this.get("completed")}); this.save({
completed: !this.get('completed')
});
}, },
// Remove this Todo from *localStorage* and delete its view. // Remove this Todo from *localStorage* and delete its view.
clear: function() { clear: function() {
this.destroy(); this.destroy();
} }
}); });
return TodoModel; return TodoModel;
}); });
define([
define(['jquery','backbone', 'collections/todos', 'common'], function($, Backbone, Todos, Common){ 'jquery',
'backbone',
'collections/todos',
'common'
], function( $, Backbone, Todos, Common ) {
var Workspace = Backbone.Router.extend({ var Workspace = Backbone.Router.extend({
routes:{ routes:{
"*filter": "setFilter" '*filter': 'setFilter'
}, },
setFilter: function(param){ setFilter: function( param ) {
// Set the current filter to be used // Set the current filter to be used
Common.TodoFilter = param.trim() || ""; Common.TodoFilter = param.trim() || '';
// Trigger a collection reset/addAll // Trigger a collection reset/addAll
Todos.trigger('reset'); Todos.trigger('reset');
} }
}); });
return Workspace; return Workspace;
});
});
\ No newline at end of file
...@@ -10,6 +10,6 @@ ...@@ -10,6 +10,6 @@
<a href="#/completed">Completed</a> <a href="#/completed">Completed</a>
</li> </li>
</ul> </ul>
<% if (completed) { %> <% if ( completed ) { %>
<button id="clear-completed">Clear completed (<%= completed %>)</button> <button id="clear-completed">Clear completed (<%= completed %>)</button>
<% } %> <% } %>
<div class="view"> <div class="view">
<input class="toggle" type="checkbox" <%= completed ? 'checked="checked"' : '' %> /> <input class="toggle" type="checkbox" <%= completed ? 'checked' : '' %>>
<label><%- title %></label> <label><%- title %></label>
<button class="destroy"></button> <button class="destroy"></button>
</div> </div>
<input class="edit" type="text" value="<%- title %>" /> <input class="edit" value="<%- title %>">
...@@ -6,40 +6,36 @@ define([ ...@@ -6,40 +6,36 @@ define([
'views/todos', 'views/todos',
'text!templates/stats.html', 'text!templates/stats.html',
'common' 'common'
], function($, _, Backbone, Todos, TodoView, statsTemplate, Common){ ], function( $, _, Backbone, Todos, TodoView, statsTemplate, Common ) {
var AppView = Backbone.View.extend({ var AppView = Backbone.View.extend({
// Instead of generating a new element, bind to the existing skeleton of // Instead of generating a new element, bind to the existing skeleton of
// the App already present in the HTML. // the App already present in the HTML.
el: $("#todoapp"), el: $('#todoapp'),
// Compile our stats template // Compile our stats template
template: _.template(statsTemplate), template: _.template( statsTemplate ),
// 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 #clear-completed": "clearCompleted", 'click #clear-completed': 'clearCompleted',
"click #toggle-all": "toggleAllComplete" 'click #toggle-all': 'toggleAllComplete'
}, },
// At initialization we bind to the relevant events on the `Todos` // At initialization we bind to the relevant events on the `Todos`
// collection, when items are added or changed. Kick things off by // collection, when items are added or changed. Kick things off by
// loading any preexisting todos that might be saved in *localStorage*. // loading any preexisting todos that might be saved in *localStorage*.
initialize: function() { initialize: function() {
this.input = this.$('#new-todo');
this.input = this.$("#new-todo"); this.allCheckbox = this.$('#toggle-all')[0];
this.allCheckbox = this.$("#toggle-all")[0];
Todos.on('add', this.addOne, this);
Todos.on('reset', this.addAll, this);
Todos.on('all', this.render, this);
this.$footer = $('#footer'); this.$footer = $('#footer');
this.$main = $('#main'); this.$main = $('#main');
Todos.on( 'add', this.addOne, this );
Todos.on( 'reset', this.addAll, this );
Todos.on( 'all', this.render, this );
Todos.fetch(); Todos.fetch();
}, },
...@@ -49,22 +45,19 @@ define([ ...@@ -49,22 +45,19 @@ define([
var completed = Todos.completed().length; var completed = Todos.completed().length;
var remaining = Todos.remaining().length; var remaining = Todos.remaining().length;
if (Todos.length) { if ( Todos.length ) {
this.$main.show(); this.$main.show();
this.$footer.show(); this.$footer.show();
this.$footer.html(this.template({ this.$footer.html(this.template({
completed: completed, completed: completed,
remaining: remaining remaining: remaining
})); }));
this.$('#filters li a') this.$('#filters li a')
.removeClass('selected') .removeClass('selected')
.filter("[href='#/" + (Common.TodoFilter || "") + "']") .filter( '[href="#/' + ( Common.TodoFilter || '' ) + '"]' )
.addClass('selected'); .addClass('selected');
} else { } else {
this.$main.hide(); this.$main.hide();
this.$footer.hide(); this.$footer.hide();
...@@ -75,28 +68,26 @@ define([ ...@@ -75,28 +68,26 @@ define([
// Add a single todo item to the list by creating a view for it, and // Add a single todo item to the list by creating a view for it, and
// appending its element to the `<ul>`. // appending its element to the `<ul>`.
addOne: function(todo) { addOne: function( todo ) {
var view = new TodoView({model: todo}); var view = new TodoView({ model: todo });
$("#todo-list").append(view.render().el); $('#todo-list').append( view.render().el );
}, },
// Add all items in the **Todos** collection at once. // Add all items in the **Todos** collection at once.
addAll: function() { addAll: function() {
this.$('#todo-list').html('');
this.$("#todo-list").html(''); switch( Common.TodoFilter ) {
case 'active':
switch(Common.TodoFilter){ _.each( Todos.remaining(), this.addOne );
case "active":
_.each(Todos.remaining(), this.addOne);
break; break;
case "completed": case 'completed':
_.each(Todos.completed(), this.addOne); _.each( Todos.completed(), this.addOne );
break; break;
default: default:
Todos.each(this.addOne, this); Todos.each( this.addOne, this );
break; break;
} }
}, },
// Generate the attributes for a new Todo item. // Generate the attributes for a new Todo item.
...@@ -110,33 +101,34 @@ define([ ...@@ -110,33 +101,34 @@ define([
// If you hit return in the main input field, create new **Todo** model, // If you hit return in the main input field, create new **Todo** model,
// persisting it to *localStorage*. // persisting it to *localStorage*.
createOnEnter: function(e) { createOnEnter: function( e ) {
if ( e.which !== Common.ENTER_KEY || !this.input.val().trim() ) {
if ( e.keyCode !== Common.ENTER_KEY ){
return;
}
if ( !this.input.val().trim() ){
return; return;
} }
Todos.create(this.newAttributes()); Todos.create( this.newAttributes() );
this.input.val(''); this.input.val('');
}, },
// Clear all completed todo items, destroying their models. // Clear all completed todo items, destroying their models.
clearCompleted: function() { clearCompleted: function() {
_.each(Todos.completed(), function(todo){ todo.clear(); }); _.each( Todos.completed(), function( todo ) {
todo.clear();
});
return false; return false;
}, },
toggleAllComplete: function () { toggleAllComplete: function() {
var completed = this.allCheckbox.checked; var completed = this.allCheckbox.checked;
Todos.each(function (todo) { todo.save({'completed': completed}); });
Todos.each(function( todo ) {
todo.save({
'completed': completed
});
});
} }
}); });
return AppView; return AppView;
}); });
...@@ -4,36 +4,37 @@ define([ ...@@ -4,36 +4,37 @@ define([
'backbone', 'backbone',
'text!templates/todos.html', 'text!templates/todos.html',
'common' 'common'
], function($, _, Backbone, todosTemplate, Common){ ], function( $, _, Backbone, todosTemplate, Common ) {
var TodoView = Backbone.View.extend({ var TodoView = Backbone.View.extend({
//... is a list tag. tagName: 'li',
tagName: "li",
template: _.template(todosTemplate), template: _.template( todosTemplate ),
// The DOM events specific to an item. // The DOM events specific to an item.
events: { events: {
"click .toggle" : "togglecompleted", 'click .toggle': 'togglecompleted',
"dblclick .view" : "edit", 'dblclick .view': 'edit',
"click .destroy" : "clear", 'click .destroy': 'clear',
"keypress .edit" : "updateOnEnter", 'keypress .edit': 'updateOnEnter',
"blur .edit" : "close" 'blur .edit': 'close'
}, },
// The TodoView listens for changes to its model, re-rendering. Since there's // The TodoView listens for changes to its model, re-rendering. Since there's
// a one-to-one correspondence between a **Todo** and a **TodoView** in this // a one-to-one correspondence between a **Todo** and a **TodoView** in this
// app, we set a direct reference on the model for convenience. // app, we set a direct reference on the model for convenience.
initialize: function() { initialize: function() {
this.model.on('change', this.render, this); this.model.on( 'change', this.render, this );
this.model.on('destroy', this.remove, this); this.model.on( 'destroy', this.remove, this );
}, },
// Re-render the titles of the todo item. // Re-render the titles of the todo item.
render: function() { render: function() {
var $el = $(this.el); var $el = $( this.el );
$el.html(this.template(this.model.toJSON()));
$el.toggleClass('completed', this.model.get('completed')); $el.html( this.template( this.model.toJSON() ) );
$el.toggleClass( 'completed', this.model.get('completed') );
this.input = this.$('.edit'); this.input = this.$('.edit');
return this; return this;
...@@ -46,7 +47,7 @@ define([ ...@@ -46,7 +47,7 @@ define([
// Switch this view into `"editing"` mode, displaying the input field. // Switch this view into `"editing"` mode, displaying the input field.
edit: function() { edit: function() {
$(this.el).addClass("editing"); $( this.el ).addClass('editing');
this.input.focus(); this.input.focus();
}, },
...@@ -54,17 +55,18 @@ define([ ...@@ -54,17 +55,18 @@ define([
close: function() { close: function() {
var value = this.input.val().trim(); var value = this.input.val().trim();
if ( !value ){ if ( value ){
this.model.save({ title: value });
} else {
this.clear(); this.clear();
} }
this.model.save({title: value}); $( this.el ).removeClass('editing');
$(this.el).removeClass("editing");
}, },
// If you hit `enter`, we're through editing the item. // If you hit `enter`, we're through editing the item.
updateOnEnter: function(e) { updateOnEnter: function( e ) {
if ( e.keyCode === Common.ENTER_KEY ){ if ( e.keyCode === Common.ENTER_KEY ) {
this.close(); this.close();
} }
}, },
...@@ -75,6 +77,5 @@ define([ ...@@ -75,6 +77,5 @@ define([
} }
}); });
return TodoView; return TodoView;
}); });
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