Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Backbone.JS: Light-weight MVC for JavaScript

Leif Singer
November 24, 2011

Backbone.JS: Light-weight MVC for JavaScript

The slides for my talk at the November 2011 @HannoverJS meet-up.

"Writing Javascript-heavy client applications requires new ways of organizing your code. MVC has worked great in other domains — see how Backbone.JS translates this 32-year-old design pattern for today's web development practices."

Leif Singer

November 24, 2011
Tweet

More Decks by Leif Singer

Other Decks in Technology

Transcript

  1. LEIF SINGER Software Engineering Group, Leibniz Universität Hannover –Teaching: Projects,

    Seminars –Research: Supporting SE using Social Software 4
  2. LEIF SINGER Software Engineering Group, Leibniz Universität Hannover –Teaching: Projects,

    Seminars –Research: Supporting SE using Social Software 4 Web Development since 1996 –private, start-up, employee, freelancer –HTML, JavaScript, Perl, CSS, PHP, Java, Groovy, ... –WebObjects, CakePHP, jQuery, Cappuccino, Grails, Play!, ...
  3. WHAT IS MVC? DESIGN PATTERN TRYGVE REENSKAUG 1979, XEROX PARC

    DECOUPLES YOUR CODE INTO MODEL VIEW CONTROLLER
  4. 6 MODEL VIEW CONTROLLER READ CHANGE SELECT USER EVENTS CHANGE

    EVENTS MVC GREAT BENEFITS: • MODEL INDEPENDENT FROM CONTROLLER & VIEW • VIEW KNOWS ONLY THE MODEL IT DISPLAYS • CONTROLLER IS GLUE • ... STRUCTURE!
  5. 8 BROWSER WEB SERVER REQUEST GET /file.html RESPONSE 200 OK

    THE WEB’S ARCHITECTURE IS A SUCCESS! STATELESS: JUST ADD SERVERS PROXIES, CACHING UNIFORM INTERFACE EASY TO INTEGRATE NEW CLIENTS & SERVERS SAFE, IDEMPOTENT GET SERVER FORGETS CLIENT.
  6. 8 BROWSER WEB SERVER REQUEST GET /file.html RESPONSE 200 OK

    THE WEB’S ARCHITECTURE IS A SUCCESS! STATELESS: JUST ADD SERVERS PROXIES, CACHING UNIFORM INTERFACE EASY TO INTEGRATE NEW CLIENTS & SERVERS SAFE, IDEMPOTENT GET SERVER FORGETS CLIENT. BUT: OBSERVERS? USER EVENTS? CHANGE EVENTS? VIEW? MODEL!? CONTROLLER!?! AN ADAPTATION OF MVC IS NEEDED.
  7. 9 MVC MODEL 2 / “WEB MVC” BROWSER WEB APPLICATION

    FRONTCONTROLLER OR ROUTER CONTROLLER
  8. 9 MVC MODEL 2 / “WEB MVC” BROWSER WEB APPLICATION

    FRONTCONTROLLER OR ROUTER CONTROLLER MODEL STORAGE
  9. 9 MVC MODEL 2 / “WEB MVC” BROWSER WEB APPLICATION

    FRONTCONTROLLER OR ROUTER CONTROLLER MODEL STORAGE
  10. 9 MVC MODEL 2 / “WEB MVC” BROWSER WEB APPLICATION

    FRONTCONTROLLER OR ROUTER CONTROLLER VIEW MODEL STORAGE
  11. 11 BROWSER WEB SERVER REQUEST GET /file.html RESPONSE 200 OK

    JSON DATA P A G E FR A G M EN T SEQUENCE SHORTENED :)
  12. 11 BROWSER WEB SERVER REQUEST GET /file.html RESPONSE 200 OK

    JSON DATA P A G E FR A G M EN T SEQUENCE SHORTENED :) HORRIBLE MESS!
  13. 11 BROWSER WEB SERVER REQUEST GET /file.html RESPONSE 200 OK

    JSON DATA P A G E FR A G M EN T SEQUENCE SHORTENED :) HORRIBLE MESS! MVC FOR JAVASCRIPT APPLICATIONS?
  14. 11 BROWSER WEB SERVER REQUEST GET /file.html RESPONSE 200 OK

    JSON DATA P A G E FR A G M EN T SEQUENCE SHORTENED :) HORRIBLE MESS! MVC FOR JAVASCRIPT APPLICATIONS? YES!
  15. 13 LINKEDIN 37SIGNALS FOURSQUARE SOUNDCLOUD GROUPON PANDORA FLOW AUDIOVROOM SALESFORCE

    DO TRAJECTORY CLOUDAPP BATTLEFIELD PLAY4FREE BLOSSOM.IO BITTORRENT BACKBONE.JS IS “MVC FOR JAVASCRIPT-HEAVY APPLICATIONS” CREATED BY DOCUMENTCLOUD.ORG HTTP://DOCUMENTCLOUD.GITHUB.COM/BACKBONE/
  16. 15 MODEL VIEW CONTROLLER READ CHANGE SELECT USER EVENTS CHANGE

    EVENTS BACKBONE.MODEL BACKBONE.COLLECTION
  17. 15 MODEL VIEW CONTROLLER READ CHANGE SELECT USER EVENTS CHANGE

    EVENTS BACKBONE.MODEL BACKBONE.COLLECTION TEMPLATES HTML
  18. 15 MODEL VIEW CONTROLLER READ CHANGE SELECT USER EVENTS CHANGE

    EVENTS BACKBONE.MODEL BACKBONE.COLLECTION BACKBONE.VIEW BACKBONE.ROUTER TEMPLATES HTML
  19. 15 MODEL VIEW CONTROLLER READ CHANGE SELECT USER EVENTS CHANGE

    EVENTS BACKBONE.MODEL BACKBONE.COLLECTION BACKBONE.EVENTS BACKBONE.EVENTS BACKBONE.VIEW BACKBONE.ROUTER TEMPLATES HTML
  20. 15 MODEL VIEW CONTROLLER READ CHANGE SELECT USER EVENTS CHANGE

    EVENTS BACKBONE.MODEL BACKBONE.COLLECTION BACKBONE.EVENTS BACKBONE.EVENTS STORAGE BACKBONE.SYNC BACKBONE.VIEW BACKBONE.ROUTER TEMPLATES HTML
  21. 16 OUR DOMAIN OBJECTS THE “THINGS” OUR APPLICATION IS ABOUT

    BACKBONE.MODEL MyApp.User = Backbone.Model.extend({});
  22. 17 OUR DOMAIN OBJECTS THE “THINGS” OUR APPLICATION IS ABOUT

    BACKBONE.MODEL MyApp.User = Backbone.Model.extend({ defaults: function() { return { username: "anonymous", thumbnail: "default.png" }; } });
  23. 18 OUR DOMAIN OBJECTS THE “THINGS” OUR APPLICATION IS ABOUT

    BACKBONE.MODEL MyApp.User = Backbone.Model.extend({ defaults: function() { return { username: "anonymous", thumbnail: "default.png" }; }, hasUsername: function() { return this.get("username") !== "anonymous"; } });
  24. 19 COLLECTIONS OF OUR DOMAIN OBJECTS SORT THEM, FILTER THEM,

    ADD, REMOVE, SYNC, ... BACKBONE.COLLECTION MyApp.UserList = Backbone.Collection.extend({}); MyApp.Users = new MyApp.UserList;
  25. 20 COLLECTIONS OF OUR DOMAIN OBJECTS SORT THEM, FILTER THEM,

    ADD, REMOVE, SYNC, ... BACKBONE.COLLECTION MyApp.UserList = Backbone.Collection.extend({ model: MyApp.User, url: "/users" }); MyApp.Users = new MyApp.UserList;
  26. 21 COLLECTIONS OF OUR DOMAIN OBJECTS SORT THEM, FILTER THEM,

    ADD, REMOVE, SYNC, ... BACKBONE.COLLECTION MyApp.UserList = Backbone.Collection.extend({ model: MyApp.User, url: "/users", withUsername: function() { return this.filter(function(user) { return user.hasUsername(); }); }, comparator: function(user) { return user.get("username"); } }); MyApp.Users = new MyApp.UserList;
  27. 22 COLLECTIONS OF OUR DOMAIN OBJECTS NEAT FUNCTIONS FROM UNDERSCORE.JS!

    BACKBONE.COLLECTION // each: calls function for each element collection.each(function(element) {element.save();});
  28. 22 COLLECTIONS OF OUR DOMAIN OBJECTS NEAT FUNCTIONS FROM UNDERSCORE.JS!

    BACKBONE.COLLECTION // each: calls function for each element collection.each(function(element) {element.save();}); // filter: all those elements passing the test var goodOnes = collection.filter(function(element) {element.isGood});
  29. 22 COLLECTIONS OF OUR DOMAIN OBJECTS NEAT FUNCTIONS FROM UNDERSCORE.JS!

    BACKBONE.COLLECTION // each: calls function for each element collection.each(function(element) {element.save();}); // filter: all those elements passing the test var goodOnes = collection.filter(function(element) {element.isGood}); // reject: all those elements *failing* the test var badOnes = collection.reject(function(element) {element.isGood});
  30. 22 COLLECTIONS OF OUR DOMAIN OBJECTS NEAT FUNCTIONS FROM UNDERSCORE.JS!

    BACKBONE.COLLECTION // each: calls function for each element collection.each(function(element) {element.save();}); // filter: all those elements passing the test var goodOnes = collection.filter(function(element) {element.isGood}); // reject: all those elements *failing* the test var badOnes = collection.reject(function(element) {element.isGood}); // find: returns first match var mm = collection.find(function(element) {element.username === "mmuster"});
  31. 23 STORAGE BACKBONE.SYNC MANAGES PERSISTENCE FOR OUR MODELS DEFAULT: RESTFUL*

    HTTP INTERFACE REPLACEABLE! E.G. WITH LOCALSTORAGE * WELL, KINDA, SORTA. REST IS MORE THAN CRUD.
  32. 23 STORAGE BACKBONE.SYNC MANAGES PERSISTENCE FOR OUR MODELS DEFAULT: RESTFUL*

    HTTP INTERFACE REPLACEABLE! E.G. WITH LOCALSTORAGE * WELL, KINDA, SORTA. REST IS MORE THAN CRUD. var user = new MyApp.User({ username: "lsinger" }); // "create" event => POST /users user.save(); // "update" event => PUT /users/17 user.save({thumbnail: "lsinger.png"}); // "destroy" event => DELETE /users/17 user.destroy();
  33. 24 STORAGE BACKBONE.SYNC USER VIEW CONTROLLER MODEL SERVER BUY! SUBMIT()

    SAVE() POST /ORDERS 200 OK TRUE SUCCESS.SHOW() SUCCESS!
  34. 25 STATIC APPLICATION SKELETON INSERTION POINTS FOR TEMPLATES HTML <div

    id="user-profile"> <h1>Profile</h1> <div id="user-details"> <!-- we'll insert details about the user here --> </div> </div>
  35. 26 DISPLAYS MODEL OBJECTS DEFAULT: UNDERSCORE’S TEMPLATE ENGINE REPLACEABLE WITH

    MUSTACHE, HAML-JS, ... TEMPLATES <script type="text/template" id="user-template"> <div class="user"> <div class="thumbnail"> <img src="<%= thumbnail %>" title="<%= username %>"/> </div> <span class="username"> <% if (username) { %> <%= username %> <% } else { %> anonymous <% } %> </span> </div> </script>
  36. 27 ACTUALLY, IT’S THE CONTROLLER GLUES TOGETHER VIEWS AND MODELS

    VIA EVENTS INTERPRETS VIEW EVENTS (SUBMIT MEANS BUY) BACKBONE.VIEW MyApp.UserView = Backbone.View.extend({});
  37. 28 BACKBONE.VIEW MyApp.UserView = Backbone.View.extend({ el: $("#user-details"), template: _.template($("#user-template").html()) });

    ACTUALLY, IT’S THE CONTROLLER GLUES TOGETHER VIEWS AND MODELS VIA EVENTS INTERPRETS VIEW EVENTS (SUBMIT MEANS BUY)
  38. 29 BACKBONE.VIEW MyApp.UserView = Backbone.View.extend({ el: $("#user-details"), template: _.template($("#user-template").html()), events:

    { "click div.thumbnail" : "zoomImage" }, zoomImage: function(e) { // e: event object /* ... zoom image here ... */ } }); ACTUALLY, IT’S THE CONTROLLER GLUES TOGETHER VIEWS AND MODELS VIA EVENTS INTERPRETS VIEW EVENTS (SUBMIT MEANS BUY)
  39. 30 BACKBONE.VIEW MyApp.UserView = Backbone.View.extend({ el: $("#user-details"), template: _.template($("#user-template").html()), events:

    { "click div.thumbnail" : "zoomImage" }, zoomImage: function(e) { // e: event object /* ... zoom image here ... */ }, initialize: function() { this.model.bind('change', this.render, this); } }); ACTUALLY, IT’S THE CONTROLLER GLUES TOGETHER VIEWS AND MODELS VIA EVENTS INTERPRETS VIEW EVENTS (SUBMIT MEANS BUY)
  40. 31 BACKBONE.VIEW MyApp.UserView = Backbone.View.extend({ el: $("#user-details"), template: _.template($("#user-template").html()), events:

    { "click div.thumbnail" : "zoomImage" }, zoomImage: function(e) { // e: event object /* ... zoom image here ... */ }, initialize: function() { this.model.bind('change', this.render, this); }, render: function() { $(this.el).html(this.template(this.model.toJSON())); return this; // allow chaining } }); ACTUALLY, IT’S THE CONTROLLER GLUES TOGETHER VIEWS AND MODELS VIA EVENTS INTERPRETS VIEW EVENTS (SUBMIT MEANS BUY)
  41. 32 A FRONTCONTROLLER MAPS URLS TO APPLICATION STATE STATE BECOMES

    BOOKMARKABLE! HISTORY API WITH HASHBANG FALLBACK BACKBONE.ROUTER MyApp.Router = Backbone.Router.extend({ routes: { "user/:username": "profile", "user/:username/posts": "posts" } });
  42. 33 A FRONTCONTROLLER MAPS URLS TO APPLICATION STATE STATE BECOMES

    BOOKMARKABLE! HISTORY API WITH HASHBANG FALLBACK BACKBONE.ROUTER MyApp.Router = Backbone.Router.extend({ routes: { "user/:username": "profile", "user/:username/posts": "posts" } }); MyApp.Router.bind("route:profile", function(username) { // recreate application state here }); MyApp.Router.bind("route:posts", function(username) { // recreate application state here });
  43. 34 USED THROUGHOUT BACKBONE TO CONNECT THINGS BACKBONE.EVENTS // observable

    emits "something:happened" events var observable = {}; _.extend(observable, Backbone.Events); observable.change = function() { observable.trigger("something:happened", "I changed!"); };
  44. 34 USED THROUGHOUT BACKBONE TO CONNECT THINGS BACKBONE.EVENTS // observable

    emits "something:happened" events var observable = {}; _.extend(observable, Backbone.Events); observable.change = function() { observable.trigger("something:happened", "I changed!"); }; // somewhere else, a listener wants to listen var listener = function(msg) {console.log("Something happened: " + msg);}; observable.bind("something:happened", listener);
  45. 34 USED THROUGHOUT BACKBONE TO CONNECT THINGS BACKBONE.EVENTS // observable

    emits "something:happened" events var observable = {}; _.extend(observable, Backbone.Events); observable.change = function() { observable.trigger("something:happened", "I changed!"); }; // somewhere else, a listener wants to listen var listener = function(msg) {console.log("Something happened: " + msg);}; observable.bind("something:happened", listener); // somewhere else still, something actually happens observable.change();
  46. 34 USED THROUGHOUT BACKBONE TO CONNECT THINGS BACKBONE.EVENTS // observable

    emits "something:happened" events var observable = {}; _.extend(observable, Backbone.Events); observable.change = function() { observable.trigger("something:happened", "I changed!"); }; // somewhere else, a listener wants to listen var listener = function(msg) {console.log("Something happened: " + msg);}; observable.bind("something:happened", listener); // somewhere else still, something actually happens observable.change();
  47. 34 USED THROUGHOUT BACKBONE TO CONNECT THINGS BACKBONE.EVENTS // observable

    emits "something:happened" events var observable = {}; _.extend(observable, Backbone.Events); observable.change = function() { observable.trigger("something:happened", "I changed!"); }; // somewhere else, a listener wants to listen var listener = function(msg) {console.log("Something happened: " + msg);}; observable.bind("something:happened", listener); // somewhere else still, something actually happens observable.change();
  48. 35 // use of events in a Backbone.View's initialize: initialize:

    function() { this.model.bind('change', this.render, this); this.model.bind('destroy', this.remove, this); } BACKBONE.EVENTS USED THROUGHOUT BACKBONE TO CONNECT THINGS
  49. 36

  50. 37

  51. 38

  52. 39

  53. 40

  54. 41

  55. WHAT IS A TODO? 42 TodoApp.Todo = Backbone.Model.extend({ defaults: function()

    { return { done: false, text: "", order: TodoApp.Todos.nextOrder() }; }, toggle: function() { this.save({done: !this.get("done")}); } });
  56. WHAT ABOUT MANY TODOS? 43 TodoApp.TodoList = Backbone.Collection.extend({ model: TodoApp.Todo,

    localStorage: new Store("todos"), done: function() { return this.filter(function(todo){ return todo.get('done'); }); }, remaining: function() { return this.without.apply(this, this.done()); }, nextOrder: function() { if (!this.length) return 1; return this.last().get('order') + 1; }, comparator: function(todo) { return todo.get('order'); } }); TodoApp.Todos = new TodoApp.TodoList;
  57. WHAT SHOULD A TODO LOOK LIKE? 44 <script type="text/template" id="item-template">

    <div class="todo <%= done ? 'done' : '' %>"> <div class="display"> <input class="check" type="checkbox" <%= done ? 'checked="checked"' : '' %> /> <div class="todo-text"></div> <span class="todo-destroy"></span> </div> <div class="edit"> <input class="todo-input" type="text" value="" /> </div> </div> </script>
  58. DISPLAY STATISTICS & CLEAR BUTTON 45 <script type="text/template" id="stats-template"> <%

    if (total) { %> <span class="todo-count"> <span class="number"><%= remaining %></span> <span class="word"><%= remaining == 1 ? 'item' : 'items' %></span> left. </span> <% } %> <% if (done) { %> <span class="todo-clear"> <a href="#"> Clear <span class="number-done"><%= done %></span> completed <span class="word-done"><%= done == 1 ? 'item' : 'items' %></span> </a> </span> <% } %> </script>
  59. PUT IT ALL INTO A STATIC SKELETON 46 <div id="todoapp">

    <div class="title"> <h1>Todos</h1> </div> <div class="content"> <div id="create-todo"> <input id="new-todo" placeholder="What needs to be done?" type="text" /> </div> <div id="todos"> <ul id="todo-list"></ul> </div> <div id="todo-stats"></div> </div> </div>
  60. 47

  61. THE TODO CONTROLLER 48 TodoApp.TodoView = Backbone.View.extend({ tagName: "li", template:

    _.template($('#item-template').html()), events: { "click .check" : "toggleDone", "dblclick div.todo-text" : "edit", "click span.todo-destroy" : "clear", "keypress .todo-input" : "updateOnEnter" }, initialize: function() { this.model.bind('change', this.render, this); this.model.bind('destroy', this.remove, this); }, render: function() { $(this.el).html(this.template(this.model.toJSON())); this.setText(); return this; }, 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); }, /* continued ... */
  62. EVENT CALLBACKS FOR THE TODO CONTROLLER 49 toggleDone: function() {

    this.model.toggle(); }, edit: function() { $(this.el).addClass("editing"); this.input.focus(); }, close: function() { this.model.save({text: this.input.val()}); $(this.el).removeClass("editing"); }, updateOnEnter: function(e) { if (e.keyCode == 13) this.close(); }, remove: function() { $(this.el).remove(); }, clear: function() { this.model.destroy(); }
  63. THE APPLICATION CONTROLLER 50 TodoApp.AppView = Backbone.View.extend({ el: $("#todoapp"), statsTemplate:

    _.template($('#stats-template').html()), events: { "keypress #new-todo": "createOnEnter", "click .todo-clear a": "clearCompleted" }, initialize: function() { this.input = this.$("#new-todo"); TodoApp.Todos.bind('add', this.addOne, this); TodoApp.Todos.bind('reset', this.addAll, this); TodoApp.Todos.bind('all', this.render, this); TodoApp.Todos.fetch(); }, render: function() { this.$('#todo-stats').html(this.statsTemplate({ total: TodoApp.Todos.length, done: TodoApp.Todos.done().length, remaining: TodoApp.Todos.remaining().length })); }, /* continued ... */
  64. EVENT CALLBACKS FOR THE APPLICATION CONTROLLER 51 addOne: function(todo) {

    var view = new TodoApp.TodoView({model: todo}); this.$("#todo-list").append(view.render().el); }, addAll: function() { // first, clear the existing views this.$("#todo-list").empty(); TodoApp.Todos.each(this.addOne); }, createOnEnter: function(e) { var text = this.input.val(); if (!text || e.keyCode != 13) return; TodoApp.Todos.create({text: text}); this.input.val(''); }, clearCompleted: function() { _.each(TodoApp.Todos.done(), function(todo){ todo.destroy(); }); return false; }
  65. REALTIME & BACKBONE.JS 55 MADE FOR “REST” INTERFACES COLLECTIONS ALWAYS

    FETCH ALL ITEMS CAN BE MODIFIED FOR USE WITH SOCKET.IO + NODE.JS JUGGERNAUT + NODE.JS (SPINE.JS!) ... NO TECH-NEUTRAL SOLUTION THERE YET.
  66. REALTIME & BACKBONE.JS 55 MADE FOR “REST” INTERFACES COLLECTIONS ALWAYS

    FETCH ALL ITEMS CAN BE MODIFIED FOR USE WITH SOCKET.IO + NODE.JS JUGGERNAUT + NODE.JS (SPINE.JS!) ... NO TECH-NEUTRAL SOLUTION THERE YET. CONVENTIONS & PATTERNS STILL IN FLUX
  67. 56 HTTP://TODOS.HANNOVERJS.DE/ BUT: A DIRTY LITTLE HACK ... TOO MUCH

    TRAFFIC EDITING IMPOSSIBLE VIEW GETS REPLACED BY UPDATES NO CONFLICT HANDLING
  68. 58 THE SPECTRUM THAT IS THE WEB * INDEED, SEVERAL

    OF THE FRAMEWORKS LISTED CAN BE USED FOR REALTIME, E.G. SPINE + JUGGERNAUT. HOWEVER, THESE SOLUTIONS TYPICALLY DEPEND ON CERTAIN SERVER-TECHNOLOGIES, SUCH AS NODE.JS. I DON’T SEE A TECH-NEUTRAL PATTERN YET – OR RATHER, I SEE MANY COMPETING ONES. TIME WILL TELL WHICH ONE(S) WILL PREVAIL.
  69. 58 THE SPECTRUM THAT IS THE WEB STATIC PAGES ENRICHED

    SOME AJAX DESKTOP-LIKE REALTIME* JS APPS ? * INDEED, SEVERAL OF THE FRAMEWORKS LISTED CAN BE USED FOR REALTIME, E.G. SPINE + JUGGERNAUT. HOWEVER, THESE SOLUTIONS TYPICALLY DEPEND ON CERTAIN SERVER-TECHNOLOGIES, SUCH AS NODE.JS. I DON’T SEE A TECH-NEUTRAL PATTERN YET – OR RATHER, I SEE MANY COMPETING ONES. TIME WILL TELL WHICH ONE(S) WILL PREVAIL.
  70. 58 THE SPECTRUM THAT IS THE WEB STATIC PAGES ENRICHED

    SOME AJAX DESKTOP-LIKE REALTIME* BACKBONE.JS ANGULAR SPINE ... CAPPUCCINO SPROUTCORE ... JS APPS ? JQUERY UI ... ? JQUERY PROTOTYPE ... * INDEED, SEVERAL OF THE FRAMEWORKS LISTED CAN BE USED FOR REALTIME, E.G. SPINE + JUGGERNAUT. HOWEVER, THESE SOLUTIONS TYPICALLY DEPEND ON CERTAIN SERVER-TECHNOLOGIES, SUCH AS NODE.JS. I DON’T SEE A TECH-NEUTRAL PATTERN YET – OR RATHER, I SEE MANY COMPETING ONES. TIME WILL TELL WHICH ONE(S) WILL PREVAIL.
  71. 59

  72. 60

  73. 61

  74. ADVERTISEMENT! 62 LOOKING FOR COMPANIES WITH SOFTWARE DEVELOPERS! TAKE PART

    IN RESEARCH! HOW TO CREATE SOFTWARE FASTER & BETTER? TALK TO ME || HTTP://LEIF.ME
  75. CREDIT WHERE CREDIT IS DUE 63 • Backbone.JS logo: http://documentcloud.github.com/backbone/

    • MVC gang: http://youtube.com/watch?v=91C7ax0UAAc • spider web: http://www.flickr.com/photos/zzathras777/1546040168/ • server: http://www.iconshock.com/ • HTML, JS, CSS icons: http://www.drewwilson.com/ • The Absolute Silence: http://www.flickr.com/photos/fabbriciuse/438451967/ • Facepalm: http://www.flickr.com/photos/santiagogprg/5023648200/ • Colorful World: http://mediablade.deviantart.com/art/Colorful-World-01-89864194 • Todo List Application by Jérôme Gravel-Niquet: http://documentcloud.github.com/backbone/ examples/todos/index.html