Single Page Web Apps with Backbone.js and Rails

13,850 views
13,722 views

Published on

My talk at RubyConf India 2011

Published in: Technology
1 Comment
26 Likes
Statistics
Notes
No Downloads
Views
Total views
13,850
On SlideShare
0
From Embeds
0
Number of Embeds
3,260
Actions
Shares
0
Downloads
214
Comments
1
Likes
26
Embeds 0
No embeds

No notes for slide
  • Prateek Dayal, 4 years with Ruby on Rails, CoFounder SupportBee, A help desk Software for SME, like the ones using Basecamp, Also did Muziboo and run HackerStreet .. Will be talking about ... how many of you love js?\n
  • As Douglas Crockford puts it, most widely installed language but most misunderstood or not understood at all. A lot of care for Rails but not so much for JS etc\n
  • How many of you have used jQuery?\n
  • If you have to select a few elements and show/hide them or apply a css property etc\n
  • Something like Gmail. Incoming Emails affect multiple items in the view etc. \n
  • \n
  • Choosing a framework is like choosing Ruby on Rails. It gives you a set of conventions and a place to put things. A lot of good choices have been already made for you. \n
  • Most of this talk is based on our experience of building SupportBee, The ticketing page is a single page app like gmail. The code is from SB\n
  • \n
  • Only communicate data with the server after being loaded. \n
  • Only communicate data with the server after being loaded. \n
  • Only communicate data with the server after being loaded. \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • Created by Charles Jolley, Used in Mobile Me, iWork.com, In Strobe Inc\n
  • Created by the founders of 280 North Inc. Used in GoMockingBird, Github Issues\n
  • \n
  • \n
  • Done by Jeremy Ashkenas. He has also created coffeescript and jammit. Jammit and Backbone is part of the document cloud code. Basecamp mobile is built using Backbone\n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • You get a json of tickets and display it. And on clicking the ticket the url should be changed and ticket should be displayed\n
  • \n
  • \n
  • \n
  • Unlike a Rails’ model, there is no inbuilt support for Associations yet\nYou can initialize values in the initialize method\nextend helps you setup the prototype chain so that you can again inherit from this model\n
  • \n
  • \n
  • \n
  • \n
  • This is a collection of model SB.Models.TicketSummary\n
  • \n
  • \n
  • Views also instantiate other views, which can render templates\n
  • Event binding for click happens here\nRenders a handlebars’ template\nThe model stores a reference to this view. This reference can be used to remove/update view on changes to the attribute\n
  • \n
  • Logicless template. Simple JSON parsing and if/else\nnameOrEmail is a helper function that you can register\n
  • \n
  • \n
  • Models inherit their urls from collections. For example\n
  • \n
  • \n
  • \n
  • The openTicket route will be fired when the ticket is clicked\n
  • \n
  • \n
  • \n
  • \n
  • \n
  • The controller has already appended the top level element to the list element on the page and has made it visible. So when tickets are populated, they show up on the page\n
  • The controller has already appended the top level element to the list element on the page and has made it visible. So when tickets are populated, they show up on the page\n
  • \n
  • \n
  • \n
  • \n
  • Though you can keep this and add some parsing code to parse the json out on client side\n
  • \n
  • \n
  • \n
  • Look at Rest in Practice or Roy Fielding’s blog for more info\n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • Single Page Web Apps with Backbone.js and Rails

    1. 1. SINGLE PAGE APPS WITHBACKBONE.JS & RAILS Prateek Dayal SupportBee.com
    2. 2. HOW I LEARNED TO STOP WORRYING AND LOVE JAVSCRIPT!
    3. 3. BUT I LOVE JQUERY!
    4. 4. VANILLA JQUERY IS GREAT FOR A SIMPLE WEBSITE
    5. 5. BUT YOU WANT TO BUILD ASINGLE PAGE APPLICATION
    6. 6. Unless you’re a really fastidious coder, some sort oflibrary to help structure large-scale JavaScriptapplications is important -- it’s far too easy todegenerate into nested piles of jQuery callbacks, alltied to concrete DOM elements. Jeremy Ashkenas
    7. 7. ;)
    8. 8. COMPLEX SINGLE PAGE APPS HAVE
    9. 9. • All of the client logic in Javascript
    10. 10. • All of the client logic in Javascript• Updates to many parts of the UI on changes to data
    11. 11. • All of the client logic in Javascript• Updates to many parts of the UI on changes to data• Templating on the client side
    12. 12. TO NOT GO CRAZYBUILDING IT, YOU NEED
    13. 13. •A MVC like pattern to keep the code clean
    14. 14. •A MVC like pattern to keep the code clean•Atemplating language like HAML/ERB to easilyrender view elements
    15. 15. •A MVC like pattern to keep the code clean•Atemplating language like HAML/ERB to easilyrender view elements•A better way to manage events and callbacks
    16. 16. •A MVC like pattern to keep the code clean•Atemplating language like HAML/ERB to easilyrender view elements•A better way to manage events and callbacks•A way to preserver browser’s back button
    17. 17. •A MVC like pattern to keep the code clean•A templating language like HAML/ERB to easily render view elements•A better way to manage events and callbacks•A way to preserver browser’s back button• Easy Testing :)
    18. 18. FORTUNATELY, THERE IS HELP!
    19. 19. SproutCore
    20. 20. Cappuccino
    21. 21. BUT THEY ARE BIG
    22. 22. AND HAVE ALEARNING CURVE
    23. 23. • 3.9 kb packed and gzipped
    24. 24. • 3.9 kb packed and gzipped• Only dependency is underscore.js
    25. 25. • 3.9 kb packed and gzipped• Only dependency is underscore.js• You need jQuery or Zepto for Ajax
    26. 26. • 3.9 kb packed and gzipped• Only dependency is underscore.js• You need jQuery or Zepto for Ajax• Annotated source code
    27. 27. MVC
    28. 28. MODELS, VIEWS & COLLECTIONS
    29. 29. MODELS
    30. 30. MODELS• Data is represented as Models
    31. 31. MODELS• Data is represented as Models• Can be Created, Validated, Destroyed & Persisted on Server
    32. 32. MODELS• Data is represented as Models• Can be Created, Validated, Destroyed & Persisted on Server• Attribute changes trigger a ‘change’ event
    33. 33. SB.Models.TicketSummary = Backbone.Model.extend({ id: null, subject: null, name: ticket,});
    34. 34. COLLECTIONS
    35. 35. COLLECTIONS•A collection of models
    36. 36. COLLECTIONS•A collection of models• Triggers events like add/remove/refresh
    37. 37. COLLECTIONS•A collection of models• Triggers events like add/remove/refresh• Can fetch models from a given url
    38. 38. COLLECTIONS•A collection of models• Triggers events like add/remove/refresh• Can fetch models from a given url• Can keep the models sorted if you define a comparator function
    39. 39. SB.Collections.TicketList = Backbone.Collection.extend({ model: SB.Models.TicketSummary, url: "/tickets", name: "tickets", initialize: function(models, options){ // Init stuff if you want }});
    40. 40. VIEWS
    41. 41. VIEWS• They are more like Rails’ Controllers
    42. 42. VIEWS• They are more like Rails’ Controllers• Responsiblefor instantiating Collections and binding events that update the UI
    43. 43. SB.Views.TicketListView = Backbone.View.extend({ tagName: ul, initialize: function(){ this.ticketList = new SB.Collections.TicketList; _.bindAll(this, addOne, addAll); this.ticketList.bind(add, this.addOne); this.ticketList.bind(refresh, this.addAll); this.ticketList.bind(all, this.render); this.ticketList.fetch(); }, addAll: function(){ this.ticketList.each(this.addOne); }, addOne: function(ticket){ var view = new SB.Views.TicketSummaryView({model:ticket}); $(this.el).append(view.render().el); }});
    44. 44. SB.Views.TicketSummary = Backbone.View.extend({ initialize: function(){ _.bind(this, render); if(this.model !== undefined){ this.model.view = this; } }, events: { click: openTicket }, openTicket: function(){ window.location = "#ticket/" + this.model.id; }, render: function(){ $(this.el).html(SB.Views.Templates[tickets/summary](this.model.toJSON())); return this; } });
    45. 45. HANDLEBARS!
    46. 46. {{#ticket}} <div class="listing-cell name"> <p>{{nameOrEmail requester}} ({{replies_count}})</p> </div> <div class="listing-cell subject"> <a href="#{{id}}"> {{#unread}} <b>{{subject}}</b> {{/unread}} {{^unread}} {{subject}} {{/unread}} </a> </div>{{/ticket}}
    47. 47. URLS
    48. 48. URLS• Collections can have a url
    49. 49. URLS• Collections can have a url• Models can have a url or they can derive it from collection’s url
    50. 50. ◦ create → POST   /collection◦ read → GET   /collection[/id]◦ update → PUT   /collection/id◦ delete → DELETE   /collection/id
    51. 51. FINALLY, CONTROLLERS
    52. 52. • Used for setting up the routes
    53. 53. • Used for setting up the routes• You can add new routes during runtime
    54. 54. SB.Controllers.AgentHome = Backbone.Controller.extend({ routes: { "": "dashboard", // http://app_url ":id": "openTicket" // http://app_url#id }, dashboard: function(){ var view = new SB.Views.TicketListView; $("#ticket").hide(); $("#list").html(view.el); $("#list").show(); }, openTicket: function(id){ var view = new SB.Views.TicketView({model : newSB.Models.Ticket({id : id})}); $("#list").hide(); $("#ticket").html(view.el); $("#ticket").show(); }});
    55. 55. window.controller = new SB.Controllers.AgentHome
    56. 56. • Initiates a new controller instance
    57. 57. • Initiates a new controller instance• which matches the dashboard route
    58. 58. • Initiates a new controller instance• which matches the dashboard route• and creates a instance of TicketListView
    59. 59. • Initiates a new controller instance• which matches the dashboard route• and creates a instance of TicketListView• and a TicketList collection is created
    60. 60. • TicketList collection fetches tickets and triggers the refresh event
    61. 61. • TicketList collection fetches tickets and triggers the refresh event• Refresh handler renders a TicketSummary view for each ticket and adds it to the top level element
    62. 62. • Every Ticket Summary row has an onclick handler
    63. 63. • Every Ticket Summary row has an onclick handler• which changes to url to #ticket_id
    64. 64. • Every Ticket Summary row has an onclick handler• which changes to url to #ticket_id• which matches the openTicket route
    65. 65. SERVER SIDE
    66. 66. ActiveRecord::Base.include_root_in_json = false
    67. 67. • Youcan use as_json to customize the json responses
    68. 68. • Youcan use as_json to customize the json responses• Or you can use Restfulie
    69. 69. • Youcan use as_json to customize the json responses• Or you can use Restfulie• Restfulie lets you easily put links in representations too
    70. 70. collection(@items, :root => "items") do |items| items.link "self", items_url items.members do |m, item| m.link :self, item_url(item) m.values { |values| values.name item.name } endend
    71. 71. NOT JUST FOR API BACKED MODELS
    72. 72. //window.AgentHomeController = new SB.Controllers.AgentHome;window.mainView = new SB.Views.Main;mainView.screens.add(new SB.Models.Screen({ title: Dashboard, href: #dashboard, listings: [ { title: New Tickets, url: /tickets?label=unanswered&replies=false }, { title: Ongoing Tickets, url: /tickets?label=unanswered&replies=true }, { title: Starred Tickets, url: /tickets?starred=true } ]}));
    73. 73. TESTING
    74. 74. JASMINE FTW!
    75. 75. beforeEach(function(){ var ticket = new SB.Models.TicketSummary({id: 1, subject: "A Ticket", replies_count: 0, requester: {id : 1, email: requester@example.com, name: Requester, thumbnail: thumbnail-url} }); this.view = new SB.Views.TicketSummary({model: ticket}); this.view.render();});it("should render a ticket summary correctly", function(){ expect($(this.view.el)).toHaveText(/A Ticket/); expect($(this.view.el)).toHaveText(/Requester/);});it("should change the url on click", function(){ var currentLoc = window.location.toString(); $(this.view.el).click(); expect(window.location.toString()).toBe(currentLoc + "ticket#1");});
    76. 76. SINON FOR MOCKS/STUBS
    77. 77. beforeEach(function(){ this.server = sinon.fakeServer.create(); this.server.respondWith("GET", "/tickets_url_mock_server", [200, { "Content-Type": "application/json" }, {"tickets":[{"id": 1, "subject": "Ticket 1"},{"id": 2, "subject":"Ticket 2"}]} ]);});it("should instantiate view when tickets are fetched", function(){ var viewMock = sinon.mock(SB.Views); viewMock.expects("TicketSummary").once().returns(new Backbone.View); viewMock.expects("TicketSummary").once().returns(new Backbone.View); var view = new SB.Views.TicketList({url: "/tickets_url_mock_server"}); this.server.respond(); viewMock.verify(); expect($(view.render().el).children().length).toBe(2);});
    78. 78. QUESTIONS?prateek@supportbee.com @prateekdayal http://supportbee.com

    ×