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

Javascript MVC & Backbone.js Tips & Tricks

Javascript MVC & Backbone.js Tips & Tricks

Presentation Agenda:

- Why MVC in Javascript ?
- Backbone & Spine
- Backbone fundamentals
- Backbone Tips & Tricks

Hjörtur Hilmarsson

May 09, 2012
Tweet

Other Decks in Programming

Transcript

  1. Agenda • Why MVC in Javascript ? • Backbone &

    Spine • Backbone fundamentals • Backbone Tips & Tricks
  2. Markup <form> ! <!-- Name input --> ! <input id="name"

    name="name" type="text" placeholder="What is your name?" required /> ! <!-- Email input --> ! <input id="email" name="email" type="email" placeholder="What is your email?" required /> ! <!-- Message input --> ! <textarea id="message" name="message" placeholder="Hello!" required ></textarea> ! <!--Send button --> ! <input id="submit" name="submit" type="submit" value="Send" /> ! <!-- Message label --> ! <span id="message" ></span> </form>
  3. Javascript - Old style $("form").submit(function( e ) { ! !

    ! ! ! e.preventDefault(); ! // get values var $form = $(this); var data = { name: $form.find("[name=name]").val(), email: $form.find("[name=email]").val(), message: $form.find("[name=message]").val() }; // ajax request $.ajax({ type: "post", url: "/enquiry", contentType: "application/json", dataType: "json", data: data, success: function() { $form.find("#message").text("Message posted").fadeIn(); }, error: function() { $form.find("#message").text("Sorry, there was an error").fadeIn(); } }); });
  4. Controller - MVC style $("form").submit(function( e ) { ! !

    ! ! ! e.preventDefault(); ! // get values ! var $form = $(this); ! var data = { ! ! name: $form.find("[name=name]").val(), ! ! email: $form.find("[name=email]").val(), ! ! message: $form.find("[name=message]").val() ! }; ! // model ! var enquiry = new Enquiry( data ); ! ! enquiry.save( ! ! function() { ! ! ! $form.find("#message").text("Message posted"); ! ! }, ! ! function() { ! ! ! $form.find("#message").text("Sorry, there was an error"); ! ! } ! ); });
  5. Model - MVC style // constructor var Enquiry = function(

    data ) { ! this.data = data; }; // save method Enquiry.prototype.save = function( success, error ) { ! // ajax request ! $.ajax({ ! ! type: "post", ! ! url: "/enquiry", ! ! contentType: "application/json", ! ! dataType: "json", ! ! data: this.data, ! ! success: success, ! ! error: error ! }); };
  6. Backbone.js controller view var ContactUs = Backbone.View.extend({ ! ! //

    local variables ! el: $("form").get(0), ! events: { "submit": "submit" } ! model: new Enquiry, ! // constructor ! initialize: function() { ! ! this.model.bind("create", create, this );! ! ! this.model.bind("error", error, this );! ! }, ! // submit event ! submit: function( e ) { ! ! e.preventDefault(); ! ! ! ! var data = { ! ! ! name: this.$("[name=name]").val(), ! ! ! email: this.$("[name=email]").val(), ! ! ! message: this.$("[name=message]").val() ! ! }; ! ! this.model.save(); ! }, ! // success callback ! create: function() { ! ! this.$("#message").text("Message posted"); ! }, ! // error callback ! error: function() { ! ! this.$("#message").text("Sorry, there was an error"); ! } });
  7. MVC Benefits Structure Classes, inheritance, common patterns. Modular Communication via

    events, lousily coupled & testable components. Common services Back and forward history, clients-side url resources, utilities. Persistence layers RESTful sync, local storage, web sockets and more. Community Patterns, mixins, conferences and more.
  8. Challenges • Going out of the box • Nested models

    • Complex ajax requests • Understanding the limitations • Its still hard
  9. To mvc, or not to mvc ? Use for one

    page apps Use for complex client-side UIs & crud Use not only for UI sugar Use not for just rendering HTML Use not for inflexible backends
  10. • Created 2010 by Jeremy Ashkenas • File size 5.4k

    • Depends on Underscore.js ( 4k ) • Very popular
  11. • Inspired by Backbone • Written in CoffeeScript by Alex

    McCaw • File size 7k • Introduced async UI concept Spine
  12. Events • Consists of on, off & trigger methods •

    All Backbone modules can trigger events • All Javascript object can be extended with the Backbone events module
  13. Event example user.on("change:name", function( name ) { ! alert( "Name

    changed to " + name ); }); Bind to a name change event Event triggered inside User class when name is changed this.trigger("change:name", "Mr Hilmarsson");
  14. Models • Wrapper for JSON & syncing via JSON •

    RESTful by default. Overwrite sync method to change persistence logic. • Communicates via events ( create, change, destroy, sync, error, add , remove ) • Can handle validation
  15. Model var Todo = Backbone.Model.extend({ defaults: { done: false },

    toggle: function() { this.save({done: !this.get("done")}); }, clear: function() { this.destroy(); } });
  16. Collections • List of models • Fires events for collection

    and the models • Keeps models sorted • Includes many utility methods
  17. Collection var TodoList = Backbone.Collection.extend({ model: Todo, done: function() {

    return this.filter(function(todo){ return todo.get('done'); }); }, remaining: function() { return this.without.apply(this, this.done() ); }, comparator: function(todo) { return todo.get('order'); } });
  18. Views • Bridge the gap between the HTML and models

    • DOM element ( this.el ) represents the context • Uses jQuery / Zepto / ender for DOM manipulation • Listens for UI events & model events • Use render method to create view
  19. Todo view var TodoView = Backbone.View.extend({ tagName: "li", template: _.template($('#item-template').html()),

    events: { "click .check" : "toggleDone" }, initialize: function() { _.bindAll(this, 'render' ); this.model.bind('change', this.render ); }, render: function() { $(this.el).html(this.template(this.model.toJSON())); return this; }, toggleDone: function() { this.model.toggle(); } ... }
  20. Template <script type="text/template" id="item-template"> <div class="todo <%= done ? 'done'

    : '' %>"> <div class="display"> <input class="check" type="checkbox" <%= done ? 'checked="checked"' : '' %> /> <label class="todo-content"><%= content %></label> <span class="todo-destroy"></span> </div> <div class="edit"> <input class="todo-input" type="text" value="<%= content %>" /> </div> </div> </script>
  21. App view var AppView = Backbone.View.extend({ ! el: $("#todoapp"), !

    ! initialize: function() { ! ! _.bindAll(this, 'addOne', 'addAll', 'render' ); ! ! Todos.on('add', this.addOne); ! ! Todos.on('reset', this.addAll); ! ! Todos.fetch(); ! }, ! addOne: function(todo) { ! ! var view = new TodoView({model: todo}); ! ! this.$("#todo-list").append(view.render().el); ! }, ! addAll: function() { ! ! Todos.each(this.addOne); ! } ! ... }
  22. Router & History • Provides a way to map URL

    resources • Enables client-side back & forward navigation • Use Hash-change by default. Supports push state ( History API )
  23. Be Careful! • Its stateful ! • Its not easy

    • Don’t set navigate trigger to true
  24. Router APP.Router = Backbone.Router.extend({ routes: { "new": "newNote", ":id": "editNote",

    "": "home" }, home: function() { APP.appView.home(); }, newNote: function() { APP.appView.newNote(); }, editNote: function( id ) { APP.appView.editNote( id ); } });
  25. History - example // Start the history Backbone.history.start(); // Start

    the history Backbone.history.start({pushState: true}); Use html5 history API Start listening for hash-change events
  26. Tips & Tricks • Tip #1 - Bootstrapping data •

    Tip #2 - Async user interfaces • Tip #3 - Nested models • Tip #4 - Custom ajax requests • Tip #5 - Zombies to heaven • Tip #6 - The toolbox • Tip #7 - Test, test, test • Tip #8 - CoffeeScript • Tip #9 - Remember the basics • Tip #10 - Bonus points
  27. Bootstrapping data • Using fetch extends waiting time • Possible

    to bootstrap the most important data when the page is rendered • No loading spinners !
  28. Bootstrapping Data // Current user APP.currentUser = new APP.Models.User(<%= @current_user.to_json.html_safe

    %>); // Notes APP.notes.reset(<%= @notes.to_json.html_safe %>); The code After render // Current user APP.currentUser = new APP.Models.User({ id: 1, username: "hjortureh", name: "Hjortur Hilmarsson", avatar: "avatar.gif" }); // Notes APP.notes.reset([ { id: 1, text: "Note 1" }, { id: 1, text: "Note 2" }, { id: 1, text: "Note 3" } ]);
  29. Importance of speed Amazon 100 ms of extra load time

    caused a 1% drop in sales (source: Greg Linden, Amazon). Google 500 ms of extra load time caused 20% fewer searches (source: Marrissa Mayer, Google). Yahoo! 400 ms of extra load time caused a 5–9% increase in the number of people who clicked “back” before the page even loaded (source: Nicole Sullivan, Yahoo!). 37 Signals - Basecamp 500 ms increase in speed on basecamp.com resulted in 5% improvement in conversion rate.
  30. Async user interfaces • Models are optimistic by default •

    UI is updated before server response • Use cid as a unique identifier on the client • No loading spinners !
  31. Nested models • Nested models are common • No official

    way of doing it • Overwrite parse after ajax request • Overwrite toJSON before ajax request • Backbone-relational mixin could help
  32. Nested models var Question = Backbone.Model.extend({ initialize: function() { //

    collection instance this.answers = new Answers; }, parse: function(resp, xhr) { // fill nested model if( _.isArray( resp.answers ) ) { this.answers.reset( resp.answers ); } return resp; }, toJSON: function() { // send nested models return $.extend( this.attributes(), { answers: this.answers.toJSON() } ); } });
  33. Custom ajax request • Sometimes RESTful methods are not enough

    • Example: Sorting tasks in to-do list
  34. Sorting - Custom request saveOrder: function() { ! ! var

    ids = this.pluck("id"); ! ! window.$.ajax({ ! ! url: "/tasks/reorder", ! ! data: { ! ! ! ids: ids ! ! }, ! ! type: "POST", ! ! dataType: "json", ! ! complete: function() { ! ! ! // Handle response ! ! } ! }); ! }
  35. Zombies to heaven • Its not enough to remove views

    from the DOM • Events must be released so you don’t have zombies walking around
  36. Zombies to heaven // same as this.$el.remove(); this.remove(); // remove

    all models bindings // made by this view this.model.off( null, null, this ); // unbind events that are // set on this view this.off();
  37. Use the toolbox • Underscore has some wonderful methods •

    isFunction, isObject, isString, isNumber, isDate & more. • Underscore: http:// documentcloud.github.com/underscore
  38. Underscore // Underscore methods that we want to implement on

    the Collection. var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy']; // Mix in each Underscore method as a proxy to `Collection#models`. _.each(methods, function(method) { Collection.prototype[method] = function() { return _[method].apply(_, [this.models].concat(_.toArray(arguments))); }; }); Line 865 from the Backbone.js code.
  39. Testing • Recommend Jasmine for testing • Recommend Sinon to

    fake the server • jQuery-jasmine to test views • Use setDomLibrary method to fake jQuery
  40. Jasmine with fake server & spy it('Should sync correctly', function

    () { // mockup data var note = new APP.Models.Note({ text: "Buy some eggs" }); // fake server this.server = sinon.fakeServer.create(); // fake response this.server.respondWith( "POST", "/notes", [ 200, {"Content-Type": "application/json"}, '{ "id": 1, "text": "Remember the milk" }' ] ); // spy on sync event var spy = sinon.spy(); note.on("sync", spy ); // save model note.save(); // server repsonse this.server.respond(); // assert expect( spy ).toHaveBeenCalledOnce(); expect( spy ).toHaveBeenCalledWith( note ); expect( note.get("text") ).toEqual( "Remember the milk" ); // restore fake server this.server.restore(); });
  41. CoffeeScript • Advanced programing language • Compiles to javascript •

    Same creator of Backbone and CoffeeScript • Integrates well with Backbone
  42. Coffee Script example class TodoList extends Backbone.View _.bindAll( this, 'render'

    ) render: => @$el.html( @template( @.model.toJSON() )) @ initialize: -> ! super Extending Backbone module Double arrow to bind to the context Use @ instead of this Last line is the return value, returns this Need to call super on parent constructors
  43. The basics • The basics still apply with MVC in

    place • Minimize ajax requests • Keep your views thin & models fat • Understanding Javascript is the key