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

BackboneConf: Ember.js

BackboneConf: Ember.js

428167a3ec72235ba971162924492609?s=128

Yehuda Katz

May 31, 2012
Tweet

Transcript

  1. And why should you care? WHAT IS EMBER?

  2. ▪ A standard library for JavaScript ▪ Why Ember's bindings

    are special ▪ The view hierarchy ▪ Structure and Widgets ▪ Routing in Ember 1.0 ▪ Managing external data ▪ Bringing it all together with a demo EXPECTATIONS.
  3. A STANDARD LIBRARY

  4. JavaScript Language Standard Library (Date, RegExp, etc.) DOM, HTML5, WebGL,

    etc. jQuery
  5. JavaScript Language Standard Library (Date, RegExp, etc.) DOM, HTML5, WebGL,

    etc. jQuery Underscore
  6. None
  7. Ember.Object Events and Observability Inheritance: Mixins and super Getters and

    Setters
  8. Ember.Object Events and Observability Inheritance: Mixins and super Getters and

    Setters Proxies (Object and Array)
  9. Ember.Object Events and Observability Inheritance: Mixins and super Getters and

    Setters Proxies (Object and Array) Enumerables Map and Set
  10. Ember.Object Events and Observability Inheritance: Mixins and super Getters and

    Setters Proxies (Object and Array) Enumerables Map and Set Array-Likes Enumerable + Indexes (#each)
  11. EMBER-METAL. EMBER-RUNTIME.

  12. EMBER'S BINDINGS

  13. BAKED INTO THE OBJECT MODEL.

  14. Person = Ember.Object.extend({ fullName: function() { return this.get('firstName') + '

    ' + this.get('lastName'); }.property('firstName', 'lastName') }); BINDINGS.
  15. Person = Ember.Object.extend({ fullName: function() { return this.get('firstName') + '

    ' + this.get('lastName'); }.property('firstName', 'lastName') }); leah = Person.create({ firstName: "Leah", lastName: "Silber" }); BINDINGS.
  16. leah = Person.create({ firstName: "Leah", lastName: "Silber" }); yehuda =

    Person.create({ firstName: "Yehuda", lastName: "Katz", spouse: leah, spouseNameBinding: 'spouse.fullName' }); BINDINGS.
  17. leah = Person.create({ firstName: "Leah", lastName: "Silber" }); yehuda =

    Person.create({ firstName: "Yehuda", lastName: "Katz", spouse: leah, spouseNameBinding: 'spouse.fullName' }); leah.set('lastName', "Silber-Katz"); yehuda.get('spouseName'); // "Leah Silber-Katz" BINDINGS.
  18. ALWAYS ASYNC.

  19. ALWAYS ASYNC. WHY?

  20. SIDE-EFFECTS. It guarantees you the ability to make all necessary

    changes before side-effects occur.
  21. yehuda.addObserver('spouseName', function() { $("#me div.spouse span.name") .html(yehuda.get('spouseName')); }); BASIC EXAMPLE.

    spouseName changes recalculate the new name update the <span>
  22. yehuda.addObserver('spouseName', function() { $("#me div.spouse span.name") .html(yehuda.get('spouseName')); }); // later

    leah.set('firstName', "Léah"); leah.set('lastName', "Silber-Katz"); BASIC EXAMPLE.
  23. leah.set('firstName', "Léah")

  24. yehuda.spouseName changes recalculate the name update the <span> leah. rstName

    changes leah.fullName changes leah.set('firstName', "Léah")
  25. yehuda.spouseName changes recalculate the name update the <span> leah. rstName

    changes leah.fullName changes leah.set('firstName', "Léah") leah.set('lastName', "Silber-Katz")
  26. yehuda.spouseName changes recalculate the name update the <span> leah. rstName

    changes leah.fullName changes yehuda.spouseName changes recalculate the name update the <span> leah.lastName changes leah.fullName changes leah.set('firstName', "Léah") leah.set('lastName', "Silber-Katz")
  27. leah.set('firstName', "Léah"); // is this even a valid state? leah.set('lastName',

    "Silber-Katz"); IN-BETWEEN STATE.
  28. REPEATED WORK. recalculate the name update the <span> recalculate the

    name update the <span>
  29. REPEATED WORK. recalculate the name update the <span> recalculate the

    name update the <span> 2x
  30. initialize: function() { this.model.on('change', this.render, this); }, render: function() {

    this.$el.html(this.model.fullName()); } SAME THING.
  31. leah.set({ firstName: "Léah", lastName: "Silber-Katz" }); DO ONE SET? This

    solves this problem, but it's not always possible. The more you use events, the harder it is to enforce atomic changes.
  32. EMBER? leah.set('firstName', "Léah")

  33. EMBER? leah. rstName changes leah.set('firstName', "Léah")

  34. EMBER? leah. rstName changes leah.set('firstName', "Léah") leah.set('lastName', "Silber-Katz")

  35. EMBER? leah. rstName changes leah.lastName changes leah.set('firstName', "Léah") leah.set('lastName', "Silber-Katz")

  36. EMBER? leah. rstName changes yehuda.spouseName changes recalculate the name update

    the <span> leah.lastName changes leah.fullName changes leah.set('firstName', "Léah") leah.set('lastName', "Silber-Katz") later...
  37. COLLECTIONS.

  38. App.StatisticsView = Backbone.View.create({ initialize: function() { this.collection.on('all', this.updateRemaining, this); this.collection.on('all',

    this.updateTotal, this); } render: function() { var list = this.collection; var remaining = "<span class='remaining'>" + list.remaining() + "</span> remaining"; var total = " and <span class='total'>" + list.done() + "</span> total"; this.$el.html("<p>" + remaining + done + "</p>"); }, updateRemaining: function() { this.$el.find("span.remaining").html(this.collection.remaining()); }, updateTotal: function() { this.$el.find("span.done").html(this.collection.done()); } }); App.TodoCollection = Backbone.Collection.extend({ remaining: function() { return this.filter(function() { return this.get('isDone') === false; }).length; }, done: function() { return this.length - this.remaining(); } }); COLLECTION VIEW.
  39. collection.forEach(function(todo) { todo.set('isDone', true); }); MULTIPLE SETS.

  40. REPEATED WORK.

  41. REPEATED WORK. recalculate remaining update the <span> recalculate total update

    the <span> recalculate remaining update the <span> recalculate total update the <span> recalculate remaining update the <span> recalculate total update the <span> recalculate remaining update the <span> recalculate total update the <span> Nx
  42. EMBER.

  43. statistics.handlebars <p>{{remaining}} remaining and {{done}} done</p> TEMPLATE.

  44. App.TodosController = Ember.ArrayController.extend({ remaining: function() { return this .filterProperty('isDone', false)

    .get('length'); }.property('@each.isDone'), done: function() { return this.get('length') - this.get('remaining'); }.property('@each.isDone') }); CONTROLLER. An Ember ArrayController serves a similar purpose to a Backbone Collection
  45. <div class="stats"> {{template "statistics"}} </div> TODOS TEMPLATE.

  46. ▪ Automatic render method ▪ Always reuses compiled template ▪

    Makes the current controller the context ▪ Sets up observers on remaining and done properties on the controller ▪ Guarantees a single DOM update per turn in the browser's event loop ▪ Manages the view hierarchy... STATS TEMPLATE.
  47. VIEW HIERARCHY

  48. <ul id="todoapp"> AD-HOC. AppView TodoView TodoView TodoView <li> <li> <li>

    TodoView <li> TodoView <li> TodoView <li> Views DOM
  49. YOU END UP WANTING AN ENFORCED HIERARCHY.

  50. Support.CompositeView = function(options) { this.children = _([]); Backbone.View.apply(this, [options]); };

    _.extend(Support.CompositeView.prototype, Backbone.View.prototype, { leave: function() { this.unbind(); this.remove(); this._leaveChildren(); this._removeFromParent(); }, renderChild: function(view) { view.render(); this.children.push(view); view.parent = this; }, appendChild: function(view) { this.renderChild(view); $(this.el).append(view.el); }, renderChildInto: function(view, cont) { this.renderChild(view); $(cont).empty().append(view.el); }, _leaveChildren: function() { this.children.chain().clone() .each(function(view) { if (view.leave) view.leave(); }); }, _removeFromParent: function() { if (this.parent) this.parent._removeChild(this); }, _removeChild: function(view) { var index = this.children.indexOf(view); this.children.splice(index, 1); } }); Support.CompositeView.extend = Backbone.View.extend; COMPOSITEVIEW.
  51. JUST THERE.

  52. ▪ Cleanup on removal ▪ Unbinding all bindings and observers

    registered by child views ▪ Unbinding all bindings and observers registered on child views ▪ Remove all internal references to eliminate many sources of leaks ▪ Lifecycle events: willInsertElement, didInsertElement, willRerender, willRemoveElement, willDestroy WHAT IT BUYS YOU.
  53. ELIMINATION OF BUSYWORK.

  54. <h1>{{category}}</h1> <ul> {{#each}} <li> <p>{{name}} <a {{action showPerson href=true}}> More

    details</a> </p> {{#unless readOnly}} {{view App.EditPerson}} {{/unless}} </li> {{/each}} </ul> EXAMPLE TEMPLATE.
  55. <h1>{{category}}</h1> <ul> {{#each}} <li> <p>{{name}} <a {{action showPerson href=true}}> More

    details</a> </p> {{#unless readOnly}} {{view App.EditPerson}} {{/unless}} </li> {{/each}} </ul> EXAMPLE TEMPLATE. append controller.category and register observer on controller.category
  56. <h1>{{category}}</h1> <ul> {{#each}} <li> <p>{{name}} <a {{action showPerson href=true}}> More

    details</a> </p> {{#unless readOnly}} {{view App.EditPerson}} {{/unless}} </li> {{/each}} </ul> EXAMPLE TEMPLATE. iterate over people array changes rerender item added add to DOM item removed remove from DOM
  57. <h1>{{category}}</h1> <ul> {{#each}} <li> <p>{{name}} <a {{action showPerson href=true}}> More

    details</a> </p> {{#unless readOnly}} {{view App.EditPerson}} {{/unless}} </li> {{/each}} </ul> EXAMPLE TEMPLATE. people can be any object that mixes in Ember.Array
  58. <h1>{{category}}</h1> <ul> {{#each}} <li> <p>{{name}} <a {{action showPerson href=true}}> More

    details</a> </p> {{#unless readOnly}} {{view App.EditPerson}} {{/unless}} </li> {{/each}} </ul> EXAMPLE TEMPLATE. append person.name and register observer on person.name
  59. <h1>{{category}}</h1> <ul> {{#each}} <li> <p>{{name}} <a {{action showPerson href=true}}> More

    details</a> </p> {{#unless readOnly}} {{view App.EditPerson}} {{/unless}} </li> {{/each}} </ul> EXAMPLE TEMPLATE. when the link is clicked, trigger the showPerson event on the router also generate a URL
  60. <h1>{{category}}</h1> <ul> {{#each}} <li> <p>{{name}} <a {{action showPerson href=true}}> More

    details</a> </p> {{#unless readOnly}} {{view App.EditPerson}} {{/unless}} </li> {{/each}} </ul> EXAMPLE TEMPLATE. only render this area if person.readOnly is false. if that changes add or remove as appropriate
  61. <h1>{{category}}</h1> <ul> {{#each}} <li> <p>{{name}} <a {{action showPerson href=true}}> More

    details</a> </p> {{#unless readOnly}} {{view App.EditPerson}} {{/unless}} </li> {{/each}} </ul> EXAMPLE TEMPLATE. insert an App.EditPerson child view its context property will be the current person
  62. RESULT. App.ApplicationView {{controller.category}} {{#each}} {{person.name}} {{#unless person.readOnly}} App.EditPersonView

  63. <h1>{{unbound category}}</h1> <ul> {{#each}} <li> <p>{{name}} <a {{action showPerson href=true}}>

    More details</a> </p> {{#unless readOnly}} {{view App.EditPerson}} {{/unless}} </li> {{/each}} </ul> MORE CONTROL.
  64. <h1>{{category}}</h1> <ul> {{#each group=true}} <li> <p>{{name}} <a {{action showPerson href=true}}>

    More details</a> </p> {{#unless readOnly}} {{view App.EditPerson}} {{/unless}} </li> {{/each}} </ul> SOON.
  65. DOES THE RIGHT THING BY DEFAULT.

  66. ROUTING

  67. STATE MANAGEMENT.

  68. App.PostsController = Ember.ArrayController.extend(); App.PostController = Ember.ObjectController.extend(); App.PostsView = Ember.View.extend(); App.PostView

    = Ember.View.extend(); OBJECTS.
  69. application.handlebars <h1>My Blog</h1> {{outlet}} posts.handlebars <ul> {{#each}} <li><a {{action showPost

    href=true}}> {{title}} </a></li> {{/each}} </ul> post.handlebars <h1>{{title}}</h1> <div class="intro"> {{intro}} </div> <hr> <div class="body"> {{body}} </div> TEMPLATES.
  70. Ember.Router.extend({ root: Ember.State.extend({ posts: Ember.State.extend({ route: '/posts', index: Ember.State.extend({ route:

    '/', showPost: Ember.State.transitionTo('show'), connectOutlets: function(router, post) { router.get('applicationController') .connectOutlet(App.PostsView, App.Post.all()); } }), show: Ember.State.extend({ route: '/:post_id', connectOutlets: function(router, post) { router.get('applicationController') .connectOutlet(App.PostView, post); } }) }) }) }); ROUTER.
  71. Ember.Router.extend({ root: Ember.State.extend({ posts: Ember.State.extend({ route: '/posts', index: Ember.State.extend({ route:

    '/', showPost: Ember.State.transitionTo('show'), connectOutlets: function(router, post) { router.get('applicationController') .connectOutlet(App.PostsView, App.Post.all()); } }), show: Ember.State.extend({ route: '/:post_id', connectOutlets: function(router, post) { router.get('applicationController') .connectOutlet(App.PostView, post); } }) }) }) }); ROUTER. <ul> {{#each}} <li><a {{action showPost href=true}}> {{title}} </a></li> {{/each}} </ul>
  72. Ember.Router.extend({ root: Ember.State.extend({ posts: Ember.State.extend({ route: '/posts', index: Ember.State.extend({ route:

    '/', showPost: Ember.State.transitionTo('show'), connectOutlets: function(router, post) { router.get('applicationController') .connectOutlet(App.PostsView, App.Post.all()); } }), show: Ember.State.extend({ route: '/:post_id', connectOutlets: function(router, post) { router.get('applicationController') .connectOutlet(App.PostView, post); } }) }) }) }); ROUTER. 1. User clicks link 2. Transition to show state, passing current post 3. Serialize the post via .get('id'), set URL /posts/51 4. Call connectOutlets with the post 5. Replace the main outlet with the PostView, with its controller set to PostController with content of post
  73. Ember.Router.extend({ root: Ember.State.extend({ posts: Ember.State.extend({ route: '/posts', index: Ember.State.extend({ route:

    '/', showPost: Ember.State.transitionTo('show'), connectOutlets: function(router, post) { router.get('applicationController') .connectOutlet(App.PostsView, App.Post.all()); } }), show: Ember.State.extend({ route: '/:post_id', connectOutlets: function(router, post) { router.get('applicationController') .connectOutlet(App.PostView, post); } }) }) }) }); ROUTER. 1. User enters at /posts/51 2. The router transitions into root.posts.show 3. The router nds post = App.Post.find(51) 4. The router calls connectOutlets with post 5. Same as before
  74. STATE MANAGER GOODIES.

  75. LOGGING.

  76. DEBUGGING PRODUCTION APPS.

  77. AVOID IMPOSSIBLE CASES.

  78. DEMO.