Backbone.js: Run your Application Inside The Browser


Published on

As presented at No Fluff Just Stuff San Antonio, April 14th 2012.

Published in: Technology
  • Be the first to comment

No Downloads
Total views
On SlideShare
From Embeds
Number of Embeds
Embeds 0
No embeds

No notes for slide

Backbone.js: Run your Application Inside The Browser

  1. 1. Backbone.js: Run yourApplication Inside TheBrowserHoward M. Lewis ShipTWD © 2012 Howard M. Lewis Ship
  2. 2. Are We Doing it Wrong? GET / Server <html><head>… POST /addCustomer Server <html><head>…
  3. 3. Page Refresh vs. User Interaction
  4. 4. Challenges Statefulness Browser Vs. Server HTML POST key values vs. heirarchical data Avoid full page updates ❝Can it be as easy to use as an iPad app?❞
  5. 5. Servers Role Load the application: Static HTML, images, stylesheets JavaScript Source/Sink of data JSON is the easy way! Ties in with NoSQL In-place page updates
  6. 6. Whats new here? Isnt this just Ajax? Core structure vs. optional enhancements Isnt this just jQuery? jQuery is great w/ elements, events, effects More structure is needed
  7. 7.
  8. 8. Dependencies Ba ck 0.9 bon .2 e Backbone jQuery (or Zepto) Underscore JSON2 Not needed in modern browsers
  9. 9. Structure XHRServer Sync Collection 0..n Model Events DOM Events Model View Property Updates <div> <input ... DOM Updates
  10. 10. Underscore.js
  11. 11. Caution: CoffeeScript
  12. 12. Caution: CoffeeScript CoffeeScript ➠ ❝… a little language that compiles into JavaScript❞ Concise and readable Optional parenthesis Implicit returns Concise function definitions Fits nicely on slides!
  13. 13. CoffeeScript: Invoking Functions$(".x-cancel").tooltip "hide" $(".x-cancel").tooltip("hide")collection.add new Quiz(originalModel), at: 0 collection.add(new Quiz(originalModel), { at: 0 });
  14. 14. CoffeeScript: Defining Functions(x,y) -> x * y function (x, y) { return x * y; }isBlank = (str) -> function isBlank(str) { _.isNull(str) or return _.isNull(str) || _.isUndefined(str) or _.isUndefined(str) || str.trim() is "" str.trim() === ""; }@collection.each (round) -> this.collection.each(function(round) { round.set "index", index++ return round.set("index", index++); });
  15. 15.
  16. 16. underscore:load coffeescript:load⏎documentcloud/backbone/0.9.2/backbone.js
  17. 17. Backbone Events
  18. 18. Backbone.EventsMixin to be added to any objectAdd/remove listenersTrigger eventsAlternative to DOM events
  19. 19. on(eventName, listener, [context])dispatcher = _.extend {}, Backbone.Events➠ …dispatcher.on "gnip", (id) -> console.log "gnip: #{id}"➠ …dispatcher.trigger "gnip", 1234"gnip: 1234"➠ …dispatcher.on "all", (eventName, args...) -> console.log "Received #{eventName}"➠ …dispatcher.trigger "fnord""Received fnord"➠ …dispatcher.trigger "gnip", 54321"gnip: 54321""Received gnip"➠ …
  20. 20. context and this this is a side-effect of method invocation: anObject.aMethod() ➠ var fn = anObject["aMethod"]; Sets this for new stack frame DOM and JQuery manipulate this ➠ Usually the DOM element that triggered event this not relevant to HOFs Functions, parameters, local scope
  21. 21. Backbone.Eventson(), off(), trigger() return thistrigger()➠ arguments passed to listenersEvent name "all"➠ Listener sees all events, first parameter is event name
  22. 22. off([event], [callback], [context]) Removes matching listeners event ➠ event to remove, or null for all callback ➠ callback function to match context ➠ context to match off() ➠ remove all listeners off("gnip") ➠ remove all gnip listeners
  23. 23. Backbone Models
  24. 24. Backbone.ModelBase class via extend()Triggers events on property changesValidates changes for consistencyCRUD via Backbone.Sync
  25. 25. Quizzical Empire
  26. 26. Paperwork
  27. 27. HTTP / XHRBackbone ExpressUnderscore Jade jQuery Mongoose Mustache connect-assets Bootstrap Underscore
  28. 28. Backbone.Model id idAttribute cid extend() get() set() escape() fetch() save() destroy() defaults() Quiz Round QuestionenableSave() : boolean enableSave() : boolean enableSave() : boolean _id title kind text location title answer created questions value rounds
  29. 29. Models and Attributes Models contain attributes … data from server Nothing is pre-defined One attribute uniquely identifies the Model idAttribute property defines this idAttribute is "_id" for MongoDB id property shadows the id attribute cid property ➠ temporary unique id before saved to server
  30. 30. extend(properties, [class props]) Creates a new type of model Properties used to define / override new methods
  31. 31. extend(properties, [class props]) Model = Backbone.Model Quiz = Model.extend idAttribute: "_id" urlRoot: "/api/quiz" parse: (response) -> response.rounds = _(response.rounds).map (raw) -> new Round raw, { parse: true } return response default: -> rounds: [] enableSave: -> (not isBlank @get "title") and _(@get "rounds").all (round) -> round.enableSave()
  32. 32. Creating an InstanceQuiz = Backbone.Model.extend initialize: -> console.log "Quiz initialized with", @attributes➠ …new Quiz title: "New Quiz" location: "Portland, OR""Quiz initialized with"{"location": "Portland, OR", "title": "New Quiz"}➠ …
  33. 33. Simple Defaults Quiz = Backbone.Model.extend initialize: -> console.log "Quiz initialized with", @attributes defaults: location: "Default Location" ➠ … new Quiz title: "Simple Defaults Quiz" "Quiz initialized with" {"location": "Default Location", "title": "Simple Defaults Quiz"} ➠ …
  34. 34. Computed Defaults Quiz = Backbone.Model.extend initialize: -> console.log "Quiz initialized with", @attributes defaults: -> title: "New Quiz at #{new Date().toDateString()}" ➠ … new Quiz "Quiz initialized with" {"title": "New Quiz at Mon Apr 09 2012"} ➠ …
  35. 35. Reading Attributesquiz = new Quiz title: "<Script>ing Attack""Quiz initialized with" jsconsole bug!{"title": "➠ …quiz.get "title"➠ "<Script>ing Attack"quiz.escape "title"➠ "&lt;Script&gt;ing Attack"
  36. 36. Changing Attributesquiz.set "title", "New Title" attribute key and value➠ …quiz.attributes➠ {"title": "New Title"}quiz.set location: "New Location" keys and values➠ …quiz.attributes➠ {"location": "New Location", "title":"New Title"}
  37. 37. Attribute Change Notification quiz.on "all", (name) -> console.log "Event: #{name}" ➠ … quiz.set title: "Updated Title" "Event: change:title" "Event: change" quiz.set { location: "Updated Location"}, silent:true ➠ … quiz.change() ➠ … "Event: change:location" "Event: change" ➠ …
  38. 38. ValidationQuiz = Backbone.Model.extend validate: (attributes) -> return "Title may not be blank" if isBlank attributes.title➠ …quiz = new Quiz➠ …quiz.isValid()➠ falsequiz.set title:null➠ falsequiz.set title:"Valid Title"➠ …quiz.isValid()➠ true NOT recommended for user input validation
  39. 39. Backbone Views
  40. 40. Backbone ViewsAre Controllers not ViewsCreate the UI for the element Handle model events, update DOM Handle DOM events, update model Trigger own events
  41. 41. View.extend(properties, [class properties]) Properties are added to Standard properties: the View model Defaults: collection tagName ➠ "div" el id attributes className tagName
  42. 42. View and Element @el is CoffeeScript for this.el @el ➠ the element for this View passed as an option to the constructor or created from tagName, className, id, and attributes options Initially detached @render or @initialize inserts it into DOM
  43. 43. Constructor new MyView([options]) Merges options into @options Creates @el if not passed as an option Passes all constructor arguments to @initialize Delegates events to @el
  44. 44. Event Delegation Automatically dispatch events from inside @el QuizTableRowView = View.extend tagName: "tr" initialize: -> … event name and events: "click .x-delete": "deleteDialog" selector "click .x-edit": "editQuiz" render: -> … method name deleteDialog: -> … editQuiz: -> … .x-verb: Personal style
  45. 45. @render vs. @initialize() Default @render implementation is: return this Goal: minimize DOM reflow/recalc Sub-views attach their @el inside containers detached @el Top-most @el attached once Also OK to attach @el inside @initialize
  46. 46. Uh … attach? QuizEditorView = FormView.extend className: "tab-pane" initialize: -> … Text @$el.attr("id", tabId) .html(readTemplate "QuizEditorView") .appendTo("#top-level-tabs > .tab-content") … Detached @el attached to DOM
  47. 47. Elements and jQuery @$el ➠ jQuery reference to the element ➠ @$el.html "<div>This Views Content</div>" $ ➠ Equivalent to @$el.find setElement(element, delegate) Sets views @el Undelegates events from old @el Delegates events to new @el
  48. 48. Client-Side Templating id aligns with Viewlayout.jadescript#ConfirmDialog(type="text/template") .modal-header a.close(data-dismiss="modal") &times; h3 {{title}} .modal-body {{{body}}} .modal-footer button.btn.btn-danger.x-confirm(data-dismiss="modal") {{label}} button.btn(data-dismiss="modal") CancelRendered HTML<script id="ConfirmDialog" type="text/template"><div class="modal-header"><a class="close" … </script>
  49. 49. Mustache readTemplate = (scriptId) -> $("##{scriptId}").html() fromMustacheTemplate = (scriptId, attributes) -> Mustache.render readTemplate(scriptId), attributesMustache.render(string, attributes) Parses the string {{name}} inserts escaped attribute {{{name}}} inserts un-escaped attribute Result ready to insert into DOM
  50. 50. Putting It All Together – Reusable Confirmation Dialog
  51. 51. ConfirmDialog = View.extend className: "modal fade in" initialize: -> @$el.html fromMustacheTemplate "ConfirmDialog", title: @options.title body: @options.body label: @options.label or "Confirm" @$(".x-confirm").addClass @options.buttonClass or "btn-primary" $("body").append(@$el) @$el.modal().on "hidden", => @remove() doConfirm: -> @trigger "confirm" events: "click .x-confirm": "doConfirm"
  52. 52. Using ConfirmDialog deleteDialog: -> title = @model.escape "title" dialog = new ConfirmDialog title: "Really delete Quiz?" body: "<p>Deletion of quiz <strong>#{title}</strong> is immediate and can not be undone.</p>" label: "Delete Quiz" buttonClass: "btn-danger" dialog.on "confirm", => @model.destroy() If the user clicks Delete Quiz, then destroy model
  53. 53. ConfirmDialog = View.extend className: "modal fade in" initialize: -> @$el.html fromMustacheTemplate "ConfirmDialog", title: @options.title body: @options.body label: @options.label or "Confirm" @$(".x-confirm").addClass @options.buttonClass or "btn-primary" $("body").append(@$el) Update body of @el from template @$el.modal().on "hidden", => Modify the class of the button @remove() doConfirm: -> @trigger "confirm" events: "click .x-confirm": "doConfirm"
  54. 54. ConfirmDialog = View.extend className: "modal fade in" initialize: -> @$el.html fromMustacheTemplate "ConfirmDialog", title: @options.title body: @options.body label: @options.label or "Confirm" @$(".x-confirm").addClass @options.buttonClass or "btn-primary" $("body").append(@$el) Insert elements into the DOM @$el.modal().on "hidden", => @remove() doConfirm: -> @trigger "confirm" events: "click .x-confirm": "doConfirm"
  55. 55. ConfirmDialog = View.extend className: "modal fade in" initialize: -> @$el.html fromMustacheTemplate "ConfirmDialog", title: @options.title body: @options.body label: @options.label or "Confirm" @$(".x-confirm").addClass @options.buttonClass or "btn-primary" $("body").append(@$el) @$el.modal().on "hidden", => @remove() Remove the element from the DOM doConfirm: -> after hide animation completes @trigger "confirm" events: "click .x-confirm": "doConfirm"
  56. 56. ConfirmDialog = View.extend className: "modal fade in" initialize: -> @$el.html fromMustacheTemplate "ConfirmDialog", title: @options.title body: @options.body label: @options.label or "Confirm" @$(".x-confirm").addClass @options.buttonClass or "btn-primary" $("body").append(@$el) @$el.modal().on "hidden", => @remove() doConfirm: -> Delegate a listener to the main button @trigger "confirm" events: "click .x-confirm": "doConfirm"
  57. 57. Form Views
  58. 58. QuizFieldsEditorView QuizFieldsEditorView = FormView.extend initialize: -> @$el.html readTemplate "QuizFieldsEditorView" @linkField name for name in ["title", "location"] script#QuizFieldsEditorView(type="text/template") .control-group.x-title label Quiz Title .controls input.span4(type="text", required) Title or theme for the Quiz .control-group.x-location label Location input.span4(type="text") Location where Quiz will take place
  59. 59. FormView FormView = View.extend linkField: (name, selector = ".x-#{name}") -> $field = @$("#{selector} input") $field.val @model.get(name) $field.on "change", (event) => newValue = @model.set name, newValue linkElement: (name, selector = ".x-#{name}", defaultText) -> element = @$(selector) update = => element.html (@model.escape name) or defaultText update() @model.on "change:#{name}", update
  60. 60. Backbone Collections
  61. 61. Collection Contains Models Has its own URL for queries & updates May represent a subset of the data Fires own events Forwards events from Models Can create new Model instances
  62. 62. Collection Access Implements Underscore collection methods (pluck, map, etc.) get(id) ➠ Get by unique id getByCid(cid) ➠ Get by temporary client id at(index) ➠ Get by index in collection push(model) ➠ Add to end of collection unshift(model) ➠ Add to start of collection pop() ➠ Remove and return last model shift() ➠ Remove and return first model length ➠ Count of models in collection
  63. 63. Collection Events add ➠ Model added to Collection remove ➠ Model removed from Collection reset ➠ Collections entire contents updated change ➠ Models attributes have changed destroy ➠ Model is destroyed (deleted)
  64. 64. QuizTableRowView QuizTableView
  65. 65. QuizListQuizList = Collection.extend model: Quiz Server returns just _id, title, location, url: "/api/quizzes" created in descending order
  66. 66. QuizTableView Alert visible when the QuizList is empty.alert strong. No Quizzes have been created yet ... feel free to start creating some!.limit-height table.table.table-bordered.table-striped.table-condensed thead tr th Title th.span3 Location th.span3 Created th.span3 tbody QuizTableRowView added as children.well button.btn.btn-primary.x-create-new Create New Quiz &nbsp; button.btn.x-create-test-data(data-loading-text="Loading ..."). Create Test Quizzes
  67. 67. QuizTableViewQuizTableView = View.extend initialize: -> @quizzes = new QuizList @$el.html readTemplate "QuizTableView" @$(".alert, table").hide() @quizzes.on "reset", @addAll, this @quizzes.on "add", @addOne, this @quizzes.on "destroy", @render, this @quizzes.fetch() @$(".x-create-test-data").popover title: "For Testing" content: "Creates many Quizzes with random text, for testing purposes. This will be removed in the final application." …
  68. 68. QuizTableView.render() render: -> if @quizzes.length is 0 @$(".alert").show() @$("table").hide() else @$(".alert").hide() @$("table").show() this
  69. 69. QuizTableView – Adding Models @quizzes.on "reset", @addAll, this @quizzes.on "add", @addOne, thisaddOne: (quiz, collection, options) -> view = new QuizTableRowView model: quiz collection: @quizzes # Special case for inserting at index 0, which happens when a new quiz # is added in the UI. fname = if options? and options.index is 0 then "prepend" else "append" @$("tbody")[fname] view.render().eladdAll: (quizzes) -> @$("tbody").empty() quizzes.each (quiz) => @addOne quiz @render()
  70. 70. QuizTableRowView view = new QuizTableRowView model: quiz collection: @quizzes QuizTableRowView = View.extend tagName: "tr" initialize: -> @model.on "change", @render, this @model.on "destroy", @remove, this @template = readTemplate "QuizTableRowView" render: -> @$el.html Mustache.render @template, title: @model.escape "title" or "<em>No Title</em>" location: @model.escape "location" created: @model.get "created" …
  71. 71. Dealing With Relationships
  72. 72. QuizenableSave() : boolean _id title location created 0..n rounds Round enableSave() : boolean kind title 0..n questions Question enableSave() : boolean text answer value
  73. 73. ❝Backbone doesnt include direct support for nested models and collections or "has many" associations because there are a numberof good patterns for modeling structured data on the client side, and Backbone should provide the foundation for implementing any of them.❞
  74. 74. Quiz inside MongoDB { "title" : "NFJS", "location" : "San Antonio", "_id" : ObjectId("4f835669c34c2a9c6f000003"), "rounds" : [ # { "kind" : "normal", # "title" : "Server-Side Java", # "_id" : ObjectId("4f835669c34c2a9c6f000004"), # "questions" : [ ] } ], "created" : ISODate("2012-04-09T21:36:41.726Z") }
  75. 75. Quiz.parse() Quiz = Model.extend idAttribute: "_id" urlRoot: "/api/quiz" parse: (response) -> response.rounds = _(response.rounds).map (raw) -> new Round raw, { parse: true } return response default: -> rounds: [] # of Round enableSave: -> (not isBlank @get "title") and _(@get "rounds").all (round) -> round.enableSave()
  76. 76. Round.parse() Round = Model.extend default: -> questions: [] # of Question parse: (response) -> response.questions = _(response.questions).map (raw) -> new Question raw, { parse: true } return response enableSave: -> return false if isBlank @get "title" questions = @get "questions" _.all questions, (q) -> q.enableSave()
  77. 77. Managing RelationshipsQuizRoundsEditorView = View.extend initialize: -> Build collection from simple array … @collection = new RoundCollection @model.get "rounds" @collection.on "all", => @model.set "rounds", @collection.toArray() @model.trigger "childChange" Update array from collection on any change RoundCollection = Collection.extend model: Round
  78. 78. Wrap Up
  79. 79. BackboneClient-centered design
  80. 80. Not Covered Lots! Details of Sync Router ➠ encode state into URL Server-side details Lots of libraries on top of Backbone
  81. 81.
  82. 82.
  83. 83.
  84. 84. Q&A