todos.js | |
---|---|
An example Backbone application contributed by Jérôme Gravel-Niquet. This demo uses a simple LocalStorage adapter to persist Backbone models within your browser. | |
Load the application once the DOM is ready, using | $(function(){ |
Todo Model | |
Our basic Todo model has | window.Todo = Backbone.Model.extend({ |
Default attributes for the todo. | defaults: {
content: "empty todo...",
done: false
}, |
Ensure that each todo created has | initialize: function() {
if (!this.get("content")) {
this.set({"content": this.defaults.content});
}
}, |
Toggle the | toggle: function() {
this.save({done: !this.get("done")});
}, |
Remove this Todo from localStorage and delete its view. | clear: function() {
this.destroy();
this.view.remove();
}
}); |
Todo Collection | |
The collection of todos is backed by localStorage instead of a remote server. | window.TodoList = Backbone.Collection.extend({ |
Reference to this collection's model. | model: Todo, |
Save all of the todo items under the | localStorage: new Store("todos"), |
Filter down the list of all todo items that are finished. | done: function() {
return this.filter(function(todo){ return todo.get('done'); });
}, |
Filter down the list to only todo items that are still not finished. | remaining: function() {
return this.without.apply(this, this.done());
}, |
We keep the Todos in sequential order, despite being saved by unordered GUID in the database. This generates the next order number for new items. | nextOrder: function() {
if (!this.length) return 1;
return this.last().get('order') + 1;
}, |
Todos are sorted by their original insertion order. | comparator: function(todo) {
return todo.get('order');
}
}); |
Create our global collection of Todos. | window.Todos = new TodoList; |
Todo Item View | |
The DOM element for a todo item... | window.TodoView = Backbone.View.extend({ |
... is a list tag. | tagName: "li", |
Cache the template function for a single item. | template: _.template($('#item-template').html()), |
The DOM events specific to an item. | events: {
"click .check" : "toggleDone",
"dblclick div.todo-content" : "edit",
"click span.todo-destroy" : "clear",
"keypress .todo-input" : "updateOnEnter"
}, |
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.bind('change', this.render, this);
this.model.view = this;
}, |
Re-render the contents of the todo item. | render: function() {
$(this.el).html(this.template(this.model.toJSON()));
this.setContent();
return this;
}, |
To avoid XSS (not that it would be harmful in this particular app),
we use | setContent: function() {
var content = this.model.get('content');
this.$('.todo-content').text(content);
this.input = this.$('.todo-input');
this.input.bind('blur', _.bind(this.close, this));
this.input.val(content);
}, |
Toggle the | toggleDone: function() {
this.model.toggle();
}, |
Switch this view into | edit: function() {
$(this.el).addClass("editing");
this.input.focus();
}, |
Close the | close: function() {
this.model.save({content: this.input.val()});
$(this.el).removeClass("editing");
}, |
If you hit | updateOnEnter: function(e) {
if (e.keyCode == 13) this.close();
}, |
Remove this view from the DOM. | remove: function() {
$(this.el).remove();
}, |
Remove the item, destroy the model. | clear: function() {
this.model.clear();
}
}); |
The Application | |
Our overall AppView is the top-level piece of UI. | window.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"), |
Our template for the line of statistics at the bottom of the app. | statsTemplate: _.template($('#stats-template').html()), |
Delegated events for creating new items, and clearing completed ones. | events: {
"keypress #new-todo": "createOnEnter",
"keyup #new-todo": "showTooltip",
"click .todo-clear a": "clearCompleted"
}, |
At initialization we bind to the relevant events on the | initialize: function() {
this.input = this.$("#new-todo");
Todos.bind('add', this.addOne, this);
Todos.bind('reset', this.addAll, this);
Todos.bind('all', this.render, this);
Todos.fetch();
}, |
Re-rendering the App just means refreshing the statistics -- the rest of the app doesn't change. | render: function() {
this.$('#todo-stats').html(this.statsTemplate({
total: Todos.length,
done: Todos.done().length,
remaining: Todos.remaining().length
}));
}, |
Add a single todo item to the list by creating a view for it, and
appending its element to the | addOne: function(todo) {
var view = new TodoView({model: todo});
this.$("#todo-list").append(view.render().el);
}, |
Add all items in the Todos collection at once. | addAll: function() {
Todos.each(this.addOne);
}, |
Generate the attributes for a new Todo item. | newAttributes: function() {
return {
content: this.input.val(),
order: Todos.nextOrder(),
done: false
};
}, |
If you hit return in the main input field, create new Todo model, persisting it to localStorage. | createOnEnter: function(e) {
if (e.keyCode != 13) return;
Todos.create(this.newAttributes());
this.input.val('');
}, |
Clear all done todo items, destroying their models. | clearCompleted: function() {
_.each(Todos.done(), function(todo){ todo.clear(); });
return false;
}, |
Lazily show the tooltip that tells you to press | showTooltip: function(e) {
var tooltip = this.$(".ui-tooltip-top");
var val = this.input.val();
tooltip.fadeOut();
if (this.tooltipTimeout) clearTimeout(this.tooltipTimeout);
if (val == '' || val == this.input.attr('placeholder')) return;
var show = function(){ tooltip.show().fadeIn(); };
this.tooltipTimeout = _.delay(show, 1000);
}
}); |
Finally, we kick things off by creating the App. | window.App = new AppView;
});
|