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 @@
<head>
<meta charset="utf-8">
<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">
<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>
<body>
<section id="todoapp">
......@@ -26,4 +30,4 @@
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</body>
</html>
\ No newline at end of file
</html>
......@@ -3,40 +3,41 @@ define([
'backbone',
'lib/backbone/localstorage',
'models/todo'
], function(_, Backbone, Store, Todo){
], function( _, Backbone, Store, Todo ) {
var TodosCollection = 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"),
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'); });
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());
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 ){
if ( !this.length ) {
return 1;
}
return this.last().get('order') + 1;
},
// Todos are sorted by their original insertion order.
comparator: function(todo) {
comparator: function( todo ) {
return todo.get('order');
}
});
return new TodosCollection;
return new TodosCollection();
});
define([], function(){
define([], function() {
return {
// Which filter are we using?
TodoFilter: "", // empty, active, completed
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});
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.config({
// The shim config allows us to configure dependencies for
// scripts that do not call define() to register a module
// 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'
}
'underscore': {
exports: '_'
},
'backbone': {
deps: [
'underscore',
'jquery'
],
exports: 'Backbone'
}
},
paths: {
jquery: 'libs/jquery/jquery-min',
underscore: 'libs/underscore/underscore',
backbone: 'libs/backbone/backbone',
text: 'libs/require/text'
jquery: 'lib/jquery/jquery.min',
underscore: 'lib/underscore/underscore',
backbone: 'lib/backbone/backbone',
text: 'lib/require/text'
}
});
require(['views/app', 'routers/router'], function( AppView, Workspace ){
// Initialize routing and start Backbone.history()
var TodoRouter = new Workspace;
Backbone.history.start();
// Initialize the application view
var app_view = new AppView;
require([
'views/app',
'routers/router'
], function( AppView, Workspace ) {
// Initialize routing and start Backbone.history()
new Workspace();
Backbone.history.start();
// Initialize the application view
new AppView();
});
define(['underscore', 'backbone'], function(_, Backbone) {
var TodoModel = Backbone.Model.extend({
define([
'underscore',
'backbone'
], function( _, Backbone ) {
var TodoModel = Backbone.Model.extend({
// Default attributes for the todo.
defaults: {
title: "empty todo...",
title: '',
completed: false
},
// Ensure that each todo created has `title`.
initialize: function() {
if (!this.get("title")) {
this.set({"title": this.defaults.title});
if ( !this.get('title') ) {
this.set({
'title': this.defaults.title
});
}
},
// Toggle the `completed` state of this todo item.
toggle: function() {
this.save({completed: !this.get("completed")});
this.save({
completed: !this.get('completed')
});
},
// Remove this Todo from *localStorage* and delete its view.
clear: function() {
this.destroy();
}
});
return TodoModel;
});
define(['jquery','backbone', 'collections/todos', 'common'], function($, Backbone, Todos, Common){
define([
'jquery',
'backbone',
'collections/todos',
'common'
], function( $, Backbone, Todos, Common ) {
var Workspace = Backbone.Router.extend({
routes:{
"*filter": "setFilter"
'*filter': 'setFilter'
},
setFilter: function(param){
setFilter: function( param ) {
// Set the current filter to be used
Common.TodoFilter = param.trim() || "";
Common.TodoFilter = param.trim() || '';
// Trigger a collection reset/addAll
Todos.trigger('reset');
}
});
return Workspace;
});
\ No newline at end of file
});
......@@ -10,6 +10,6 @@
<a href="#/completed">Completed</a>
</li>
</ul>
<% if (completed) { %>
<% if ( completed ) { %>
<button id="clear-completed">Clear completed (<%= completed %>)</button>
<% } %>
<div class="view">
<input class="toggle" type="checkbox" <%= completed ? 'checked="checked"' : '' %> />
<input class="toggle" type="checkbox" <%= completed ? 'checked' : '' %>>
<label><%- title %></label>
<button class="destroy"></button>
</div>
<input class="edit" type="text" value="<%- title %>" />
<input class="edit" value="<%- title %>">
......@@ -6,40 +6,36 @@ define([
'views/todos',
'text!templates/stats.html',
'common'
], function($, _, Backbone, Todos, TodoView, statsTemplate, Common){
], function( $, _, Backbone, Todos, TodoView, statsTemplate, Common ) {
var AppView = Backbone.View.extend({
// Instead of generating a new element, bind to the existing skeleton of
// the App already present in the HTML.
el: $("#todoapp"),
el: $('#todoapp'),
// Compile our stats template
template: _.template(statsTemplate),
template: _.template( statsTemplate ),
// Delegated events for creating new items, and clearing completed ones.
events: {
"keypress #new-todo": "createOnEnter",
"click #clear-completed": "clearCompleted",
"click #toggle-all": "toggleAllComplete"
'keypress #new-todo': 'createOnEnter',
'click #clear-completed': 'clearCompleted',
'click #toggle-all': 'toggleAllComplete'
},
// At initialization we bind to the relevant events on the `Todos`
// collection, when items are added or changed. Kick things off by
// loading any preexisting todos that might be saved in *localStorage*.
initialize: function() {
this.input = this.$("#new-todo");
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.input = this.$('#new-todo');
this.allCheckbox = this.$('#toggle-all')[0];
this.$footer = $('#footer');
this.$main = $('#main');
Todos.on( 'add', this.addOne, this );
Todos.on( 'reset', this.addAll, this );
Todos.on( 'all', this.render, this );
Todos.fetch();
},
......@@ -49,22 +45,19 @@ define([
var completed = Todos.completed().length;
var remaining = Todos.remaining().length;
if (Todos.length) {
if ( Todos.length ) {
this.$main.show();
this.$footer.show();
this.$footer.html(this.template({
completed: completed,
remaining: remaining
completed: completed,
remaining: remaining
}));
this.$('#filters li a')
.removeClass('selected')
.filter("[href='#/" + (Common.TodoFilter || "") + "']")
.filter( '[href="#/' + ( Common.TodoFilter || '' ) + '"]' )
.addClass('selected');
} else {
this.$main.hide();
this.$footer.hide();
......@@ -75,28 +68,26 @@ define([
// Add a single todo item to the list by creating a view for it, and
// appending its element to the `<ul>`.
addOne: function(todo) {
var view = new TodoView({model: todo});
$("#todo-list").append(view.render().el);
addOne: function( todo ) {
var view = new TodoView({ model: todo });
$('#todo-list').append( view.render().el );
},
// Add all items in the **Todos** collection at once.
addAll: function() {
this.$('#todo-list').html('');
this.$("#todo-list").html('');
switch(Common.TodoFilter){
case "active":
_.each(Todos.remaining(), this.addOne);
switch( Common.TodoFilter ) {
case 'active':
_.each( Todos.remaining(), this.addOne );
break;
case "completed":
_.each(Todos.completed(), this.addOne);
case 'completed':
_.each( Todos.completed(), this.addOne );
break;
default:
Todos.each(this.addOne, this);
Todos.each( this.addOne, this );
break;
}
},
// Generate the attributes for a new Todo item.
......@@ -110,33 +101,34 @@ define([
// If you hit return in the main input field, create new **Todo** model,
// persisting it to *localStorage*.
createOnEnter: function(e) {
if ( e.keyCode !== Common.ENTER_KEY ){
return;
}
if ( !this.input.val().trim() ){
createOnEnter: function( e ) {
if ( e.which !== Common.ENTER_KEY || !this.input.val().trim() ) {
return;
}
Todos.create(this.newAttributes());
Todos.create( this.newAttributes() );
this.input.val('');
},
// Clear all completed todo items, destroying their models.
clearCompleted: function() {
_.each(Todos.completed(), function(todo){ todo.clear(); });
_.each( Todos.completed(), function( todo ) {
todo.clear();
});
return false;
},
toggleAllComplete: function () {
toggleAllComplete: function() {
var completed = this.allCheckbox.checked;
Todos.each(function (todo) { todo.save({'completed': completed}); });
Todos.each(function( todo ) {
todo.save({
'completed': completed
});
});
}
});
return AppView;
});
......@@ -4,36 +4,37 @@ define([
'backbone',
'text!templates/todos.html',
'common'
], function($, _, Backbone, todosTemplate, Common){
], function( $, _, Backbone, todosTemplate, Common ) {
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.
events: {
"click .toggle" : "togglecompleted",
"dblclick .view" : "edit",
"click .destroy" : "clear",
"keypress .edit" : "updateOnEnter",
"blur .edit" : "close"
'click .toggle': 'togglecompleted',
'dblclick .view': 'edit',
'click .destroy': 'clear',
'keypress .edit': 'updateOnEnter',
'blur .edit': 'close'
},
// 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
// app, we set a direct reference on the model for convenience.
initialize: function() {
this.model.on('change', this.render, this);
this.model.on('destroy', this.remove, this);
this.model.on( 'change', this.render, this );
this.model.on( 'destroy', this.remove, this );
},
// Re-render the titles of the todo item.
render: function() {
var $el = $(this.el);
$el.html(this.template(this.model.toJSON()));
$el.toggleClass('completed', this.model.get('completed'));
var $el = $( this.el );
$el.html( this.template( this.model.toJSON() ) );
$el.toggleClass( 'completed', this.model.get('completed') );
this.input = this.$('.edit');
return this;
......@@ -46,7 +47,7 @@ define([
// Switch this view into `"editing"` mode, displaying the input field.
edit: function() {
$(this.el).addClass("editing");
$( this.el ).addClass('editing');
this.input.focus();
},
......@@ -54,17 +55,18 @@ define([
close: function() {
var value = this.input.val().trim();
if ( !value ){
if ( value ){
this.model.save({ title: value });
} else {
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.
updateOnEnter: function(e) {
if ( e.keyCode === Common.ENTER_KEY ){
updateOnEnter: function( e ) {
if ( e.keyCode === Common.ENTER_KEY ) {
this.close();
}
},
......@@ -75,6 +77,5 @@ define([
}
});
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