From 3cf1bb4ed51a47a87c50a56896a10e2fe597172a Mon Sep 17 00:00:00 2001 From: addyosmani Date: Mon, 27 Feb 2012 21:41:27 +0000 Subject: [PATCH] Updating with latest version from TodoMVC As requested in #1044, here's the latest version of the Backbone.js Todo app as rewritten by our project. We started out with the 0.5 base and re-wrote it to cover some subtle best practices we thought were important. Ours, like the current one also uses the latest Backbone and jQuery 1.7.1. As part of the changes, we also introduced two differences in the UX: * When in edit mode, if a todo item is emptied and then blurred, the item is removed. This contrasts with the current behaviour of the app in the official repo at the moment which maintains the empty item in place (albeit looking a little broken http://addyosmani.com/gyazo/bbd4cd.png) * We removed the tooltip occasionally seen when a user was trying to add a new item. Having discussed this with developers frequently using the Todo app as an initial point of reference, it was a consensus that the notification didn't really offer that much value nor did it really show anything that Backbone-specific worth keeping it in for. We usually enforce examples separate concerns (Models, Views etc.) into their own directories pre-build, but I've reformatted it to match the structure your current app takes so that it can be more easily diffed. I hope it's worth considering our version for a merge. We're happy to take on any feedback needed to update it to address concerns you might have. --- examples/todos/index.html | 114 ++++------ examples/todos/todos.css | 466 +++++++++++++++----------------------- examples/todos/todos.js | 159 +++++++------ 3 files changed, 321 insertions(+), 418 deletions(-) diff --git a/examples/todos/index.html b/examples/todos/index.html index ec93e68fd..d2eba9fe0 100644 --- a/examples/todos/index.html +++ b/examples/todos/index.html @@ -1,87 +1,63 @@ - - + + + + + Backbone.js Todos + + + +
+
+

Todos

+ +
+ +
+ + +
    +
+
+ + +
+
+ Double-click to edit a todo. +
+
+ Created by +
+ Jérôme Gravel-Niquet. +
Rewritten by: TodoMVC. +
- - Backbone Demo: Todos - - - - - - - -
- -
-

Todos

-
- -
- -
- - -
- -
-
    -
    - -
    - -
    - -
    - - - -
    - Created by -
    - Jérôme Gravel-Niquet -
    - - + diff --git a/examples/todos/todos.css b/examples/todos/todos.css index 61ab6cffd..35bdb056f 100644 --- a/examples/todos/todos.css +++ b/examples/todos/todos.css @@ -1,311 +1,211 @@ -html, body, div, span, applet, object, iframe, -h1, h2, h3, h4, h5, h6, p, blockquote, pre, -a, abbr, acronym, address, big, cite, code, -del, dfn, em, font, img, ins, kbd, q, s, samp, -small, strike, strong, sub, sup, tt, var, -dl, dt, dd, ol, ul, li, -fieldset, form, label, legend, -table, caption, tbody, tfoot, thead, tr, th, td { - margin: 0; - padding: 0; - border: 0; - outline: 0; - font-weight: inherit; - font-style: inherit; - font-size: 100%; - font-family: inherit; - vertical-align: baseline; +html, +body { + margin: 0; + padding: 0; } + body { - line-height: 1; - color: black; - background: white; + font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #eeeeee; + color: #333333; + width: 520px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; } -ol, ul { - list-style: none; + +#todoapp { + background: #fff; + padding: 20px; + margin-bottom: 40px; + -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0; + -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0; + -ms-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0; + -o-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0; + box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0; + -webkit-border-radius: 0 0 5px 5px; + -moz-border-radius: 0 0 5px 5px; + -ms-border-radius: 0 0 5px 5px; + -o-border-radius: 0 0 5px 5px; + border-radius: 0 0 5px 5px; } -a img { - border: none; + +#todoapp h1 { + font-size: 36px; + font-weight: bold; + text-align: center; + padding: 0 0 10px 0; } -html { - background: #eeeeee; +#todoapp input[type="text"] { + width: 466px; + font-size: 24px; + font-family: inherit; + line-height: 1.4em; + border: 0; + outline: none; + padding: 6px; + border: 1px solid #999999; + -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; + -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; + -ms-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; + -o-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; + box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; } - body { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 14px; - line-height: 1.4em; - background: #eeeeee; - color: #333333; - } -#todoapp { - width: 480px; - margin: 0 auto 40px; - background: white; - padding: 20px; - -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 5px 6px 0; - -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 5px 6px 0; - -o-box-shadow: rgba(0, 0, 0, 0.2) 0 5px 6px 0; - box-shadow: rgba(0, 0, 0, 0.2) 0 5px 6px 0; +#todoapp input::-webkit-input-placeholder { + font-style: italic; } - #todoapp h1 { - font-size: 36px; - font-weight: bold; - text-align: center; - padding: 20px 0 30px 0; - line-height: 1; - } -#create-todo { - position: relative; +#main { + display: none; } - #create-todo input { - width: 466px; - font-size: 24px; - font-family: inherit; - line-height: 1.4em; - border: 0; - outline: none; - padding: 6px; - border: 1px solid #999999; - -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; - -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; - -o-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; - box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; - } - #create-todo input::-webkit-input-placeholder { - font-style: italic; - } - #create-todo span { - position: absolute; - z-index: 999; - width: 170px; - left: 50%; - margin-left: -85px; - } #todo-list { - margin-top: 10px; + margin: 10px 0; + padding: 0; + list-style: none; } - #todo-list li { - padding: 12px 20px 11px 0; - position: relative; - font-size: 24px; - line-height: 1.1em; - border-bottom: 1px solid #cccccc; - } - #todo-list li:after { - content: "\0020"; - display: block; - height: 0; - clear: both; - overflow: hidden; - visibility: hidden; - } - #todo-list li.editing { - padding: 0; - border-bottom: 0; - } - #todo-list .editing .display, - #todo-list .edit { - display: none; - } - #todo-list .editing .edit { - display: block; - } - #todo-list .editing input { - width: 444px; - font-size: 24px; - font-family: inherit; - margin: 0; - line-height: 1.6em; - border: 0; - outline: none; - padding: 10px 7px 0px 27px; - border: 1px solid #999999; - -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; - -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; - -o-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; - box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset; - } - #todo-list .check { - position: relative; - top: 9px; - margin: 0 10px 0 7px; - float: left; - } - #todo-list .done .todo-text { - text-decoration: line-through; - color: #777777; - } - #todo-list .todo-destroy { - position: absolute; - right: 5px; - top: 14px; - display: none; - cursor: pointer; - width: 20px; - height: 20px; - background: url(destroy.png) no-repeat 0 0; - } - #todo-list li:hover .todo-destroy { - display: block; - } - #todo-list .todo-destroy:hover { - background-position: 0 -20px; - } -#todo-stats { - *zoom: 1; - margin-top: 10px; - color: #777777; +#todo-list li { + padding: 18px 20px 18px 0; + position: relative; + font-size: 24px; + border-bottom: 1px solid #cccccc; } - #todo-stats:after { - content: "\0020"; - display: block; - height: 0; - clear: both; - overflow: hidden; - visibility: hidden; - } - #todo-stats .todo-count { - float: left; - } - #todo-stats .todo-count .number { - font-weight: bold; - color: #333333; - } - #todo-stats .todo-clear { - float: right; - } - #todo-stats .todo-clear a { - color: #777777; - font-size: 12px; - } - #todo-stats .todo-clear a:visited { - color: #777777; - } - #todo-stats .todo-clear a:hover { - color: #336699; - } -#instructions { - width: 520px; - margin: 10px auto; - color: #777777; - text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0; - text-align: center; +#todo-list li:last-child { + border-bottom: none; } - #instructions a { - color: #336699; - } -#credits { - width: 520px; - margin: 30px auto; - color: #999; - text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0; - text-align: center; +#todo-list li.done label { + color: #777777; + text-decoration: line-through; +} + +#todo-list .destroy { + position: absolute; + right: 5px; + top: 20px; + display: none; + cursor: pointer; + width: 20px; + height: 20px; + background: url(destroy.png) no-repeat; +} + +#todo-list li:hover .destroy { + display: block; +} + +#todo-list .destroy:hover { + background-position: 0 -20px; +} + +#todo-list li.editing { + border-bottom: none; + margin-top: -1px; + padding: 0; +} + +#todo-list li.editing:last-child { + margin-bottom: -1px; +} + +#todo-list li.editing .edit { + display: block; + width: 444px; + padding: 13px 15px 14px 20px; + margin: 0; +} + +#todo-list li.editing .view { + display: none; +} + +#todo-list li .view label { + word-break: break-word; } - #credits a { - color: #888; - } +#todo-list li .edit { + display: none; +} -/* - * François 'cahnory' Germain - */ -.ui-tooltip, .ui-tooltip-top, .ui-tooltip-right, .ui-tooltip-bottom, .ui-tooltip-left { - color:#ffffff; - cursor:normal; - display:-moz-inline-stack; - display:inline-block; - font-size:12px; - font-family:arial; - padding:.5em 1em; - position:relative; - text-align:center; - text-shadow:0 -1px 1px #111111; - -webkit-border-top-left-radius:4px ; - -webkit-border-top-right-radius:4px ; - -webkit-border-bottom-right-radius:4px ; - -webkit-border-bottom-left-radius:4px ; - -khtml-border-top-left-radius:4px ; - -khtml-border-top-right-radius:4px ; - -khtml-border-bottom-right-radius:4px ; - -khtml-border-bottom-left-radius:4px ; - -moz-border-radius-topleft:4px ; - -moz-border-radius-topright:4px ; - -moz-border-radius-bottomright:4px ; - -moz-border-radius-bottomleft:4px ; - border-top-left-radius:4px ; - border-top-right-radius:4px ; - border-bottom-right-radius:4px ; - border-bottom-left-radius:4px ; - -o-box-shadow:0 1px 2px #000000, inset 0 0 0 1px #222222, inset 0 2px #666666, inset 0 -2px 2px #444444; - -moz-box-shadow:0 1px 2px #000000, inset 0 0 0 1px #222222, inset 0 2px #666666, inset 0 -2px 2px #444444; - -khtml-box-shadow:0 1px 2px #000000, inset 0 0 0 1px #222222, inset 0 2px #666666, inset 0 -2px 2px #444444; - -webkit-box-shadow:0 1px 2px #000000, inset 0 0 0 1px #222222, inset 0 2px #666666, inset 0 -2px 2px #444444; - box-shadow:0 1px 2px #000000, inset 0 0 0 1px #222222, inset 0 2px #666666, inset 0 -2px 2px #444444; - background-color:#3b3b3b; - background-image:-moz-linear-gradient(top,#555555,#222222); - background-image:-webkit-gradient(linear,left top,left bottom,color-stop(0,#555555),color-stop(1,#222222)); - filter:progid:DXImageTransform.Microsoft.gradient(startColorStr=#555555,EndColorStr=#222222); - -ms-filter:progid:DXImageTransform.Microsoft.gradient(startColorStr=#555555,EndColorStr=#222222); +#todoapp footer { + display: none; + margin: 0 -20px -20px -20px; + overflow: hidden; + color: #555555; + background: #f4fce8; + border-top: 1px solid #ededed; + padding: 0 20px; + line-height: 37px; + -webkit-border-radius: 0 0 5px 5px; + -moz-border-radius: 0 0 5px 5px; + -ms-border-radius: 0 0 5px 5px; + -o-border-radius: 0 0 5px 5px; + border-radius: 0 0 5px 5px; } -.ui-tooltip:after, .ui-tooltip-top:after, .ui-tooltip-right:after, .ui-tooltip-bottom:after, .ui-tooltip-left:after { - content:"\25B8"; - display:block; - font-size:2em; - height:0; - line-height:0; - position:absolute; + +#clear-completed { + float: right; + line-height: 20px; + text-decoration: none; + background: rgba(0, 0, 0, 0.1); + color: #555555; + font-size: 11px; + margin-top: 8px; + margin-bottom: 8px; + padding: 0 10px 1px; + cursor: pointer; + -webkit-border-radius: 12px; + -moz-border-radius: 12px; + -ms-border-radius: 12px; + -o-border-radius: 12px; + border-radius: 12px; + -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0; + -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0; + -ms-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0; + -o-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0; + box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0; } -.ui-tooltip:after, .ui-tooltip-bottom:after { - color:#2a2a2a; - bottom:0; - left:1px; - text-align:center; - text-shadow:1px 0 2px #000000; - -o-transform:rotate(90deg); - -moz-transform:rotate(90deg); - -khtml-transform:rotate(90deg); - -webkit-transform:rotate(90deg); - width:100%; + +#clear-completed:hover { + background: rgba(0, 0, 0, 0.15); + -webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0; + -moz-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0; + -ms-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0; + -o-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0; + box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0; } -.ui-tooltip-top:after { - bottom:auto; - color:#4f4f4f; - left:-2px; - top:0; - text-align:center; - text-shadow:none; - -o-transform:rotate(-90deg); - -moz-transform:rotate(-90deg); - -khtml-transform:rotate(-90deg); - -webkit-transform:rotate(-90deg); - width:100%; + +#clear-completed:active { + position: relative; + top: 1px; } -.ui-tooltip-right:after { - color:#222222; - right:-0.375em; - top:50%; - margin-top:-.05em; - text-shadow:0 1px 2px #000000; - -o-transform:rotate(0); - -moz-transform:rotate(0); - -khtml-transform:rotate(0); - -webkit-transform:rotate(0); + +#todo-count span { + font-weight: bold; } -.ui-tooltip-left:after { - color:#222222; - left:-0.375em; - top:50%; - margin-top:.1em; - text-shadow:0 -1px 2px #000000; - -o-transform:rotate(180deg); - -moz-transform:rotate(180deg); - -khtml-transform:rotate(180deg); - -webkit-transform:rotate(180deg); + +#instructions { + margin: 10px auto; + color: #777777; + text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0; + text-align: center; +} + +#instructions a { + color: #336699; +} + +#credits { + margin: 30px auto; + color: #999; + text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0; + text-align: center; +} + +#credits a { + color: #888; } diff --git a/examples/todos/todos.js b/examples/todos/todos.js index 5b58fae72..895d3410b 100644 --- a/examples/todos/todos.js +++ b/examples/todos/todos.js @@ -1,6 +1,6 @@ // An example Backbone application contributed by // [Jérôme Gravel-Niquet](http://jgn.me/). This demo uses a simple -// [LocalStorage adapter](backbone-localstorage.html) +// [LocalStorage adapter](backbone-localstorage.js) // to persist Backbone models within your browser. // Load the application once the DOM is ready, using `jQuery.ready`: @@ -9,20 +9,30 @@ $(function(){ // Todo Model // ---------- - // Our basic **Todo** model has `text`, `order`, and `done` attributes. - window.Todo = Backbone.Model.extend({ + // Our basic **Todo** model has `title`, `order`, and `done` attributes. + var Todo = Backbone.Model.extend({ - // Default attributes for a todo item. - defaults: function() { - return { - done: false, - order: Todos.nextOrder() - }; + // Default attributes for the todo. + defaults: { + title: "empty todo...", + done: false + }, + + // Ensure that each todo created has `title`. + initialize: function() { + if (!this.get("title")) { + this.set({"title": this.defaults.title}); + } }, // Toggle the `done` state of this todo item. toggle: function() { this.save({done: !this.get("done")}); + }, + + // Remove this Todo from *localStorage* and delete its view. + clear: function() { + this.destroy(); } }); @@ -32,13 +42,13 @@ $(function(){ // The collection of todos is backed by *localStorage* instead of a remote // server. - window.TodoList = Backbone.Collection.extend({ + 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"), + localStorage: new Store("todos-backbone"), // Filter down the list of all todo items that are finished. done: function() { @@ -65,13 +75,13 @@ $(function(){ }); // Create our global collection of **Todos**. - window.Todos = new TodoList; + var Todos = new TodoList; // Todo Item View // -------------- // The DOM element for a todo item... - window.TodoView = Backbone.View.extend({ + var TodoView = Backbone.View.extend({ //... is a list tag. tagName: "li", @@ -81,32 +91,29 @@ $(function(){ // The DOM events specific to an item. events: { - "click .check" : "toggleDone", - "dblclick div.todo-text" : "edit", - "click span.todo-destroy" : "clear", - "keypress .todo-input" : "updateOnEnter" + "click .toggle" : "toggleDone", + "dblclick .view" : "edit", + "click a.destroy" : "clear", + "keypress .edit" : "updateOnEnter", + "blur .edit" : "close" }, - // The TodoView listens for changes to its model, re-rendering. + // 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.bind('destroy', this.remove, this); }, - // Re-render the contents of the todo item. + // Re-render the titles of the todo item. render: function() { - $(this.el).html(this.template(this.model.toJSON())); - this.setText(); - return this; - }, + var $el = $(this.el); + $el.html(this.template(this.model.toJSON())); + $el.toggleClass('done', this.model.get('done')); - // To avoid XSS (not that it would be harmful in this particular app), - // we use `jQuery.text` to set the contents of the todo item. - setText: function() { - var text = this.model.get('text'); - this.$('.todo-text').text(text); - this.input = this.$('.todo-input'); - this.input.bind('blur', _.bind(this.close, this)).val(text); + this.input = this.$('.edit'); + return this; }, // Toggle the `"done"` state of the model. @@ -122,7 +129,12 @@ $(function(){ // Close the `"editing"` mode, saving changes to the todo. close: function() { - this.model.save({text: this.input.val()}); + var value = this.input.val().trim(); + + if (!value) + this.clear(); + + this.model.save({title: value}); $(this.el).removeClass("editing"); }, @@ -131,14 +143,9 @@ $(function(){ 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.destroy(); + this.model.clear(); } }); @@ -147,7 +154,7 @@ $(function(){ // --------------- // Our overall **AppView** is the top-level piece of UI. - window.AppView = Backbone.View.extend({ + var AppView = Backbone.View.extend({ // Instead of generating a new element, bind to the existing skeleton of // the App already present in the HTML. @@ -159,19 +166,24 @@ $(function(){ // Delegated events for creating new items, and clearing completed ones. events: { "keypress #new-todo": "createOnEnter", - "keyup #new-todo": "showTooltip", - "click .todo-clear a": "clearCompleted" + "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"); - Todos.bind('add', this.addOne, this); + this.input = this.$("#new-todo"); + this.allCheckbox = this.$("#toggle-all")[0]; + + Todos.bind('add', this.addOne, this); Todos.bind('reset', this.addAll, this); - Todos.bind('all', this.render, this); + Todos.bind('all', this.render, this); + + this.$footer = this.$('footer'); + this.$main = $('#main'); Todos.fetch(); }, @@ -179,18 +191,30 @@ $(function(){ // 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 - })); + var done = Todos.done().length; + var remaining = Todos.remaining().length; + + if (Todos.length) { + this.$main.show(); + this.$footer.show(); + + this.$footer.html(this.statsTemplate({ + done: done, + remaining: remaining + })); + } else { + this.$main.hide(); + this.$footer.hide(); + } + + this.allCheckbox.checked = !remaining; }, // Add a single todo item to the list by creating a view for it, and // appending its element to the `