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

BackboneConf: Ember.js

BackboneConf: Ember.js

Yehuda Katz

May 31, 2012
Tweet

More Decks by Yehuda Katz

Other Decks in Technology

Transcript

  1. ▪ 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.
  2. Ember.Object Events and Observability Inheritance: Mixins and super Getters and

    Setters Proxies (Object and Array) Enumerables Map and Set
  3. 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)
  4. Person = Ember.Object.extend({ fullName: function() { return this.get('firstName') + '

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

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

    Person.create({ firstName: "Yehuda", lastName: "Katz", spouse: leah, spouseNameBinding: 'spouse.fullName' }); BINDINGS.
  7. 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.
  8. 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.
  9. yehuda.spouseName changes recalculate the name update the <span> leah. rstName

    changes leah.fullName changes leah.set('firstName', "Léah")
  10. 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")
  11. 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")
  12. 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.
  13. 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...
  14. 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.
  15. 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
  16. 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
  17. ▪ 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.
  18. <ul id="todoapp"> AD-HOC. AppView TodoView TodoView TodoView <li> <li> <li>

    TodoView <li> TodoView <li> TodoView <li> Views DOM
  19. 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.
  20. ▪ 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.
  21. <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.
  22. <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
  23. <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
  24. <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
  25. <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
  26. <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
  27. <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
  28. <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
  29. <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.
  30. <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.
  31. 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.
  32. 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.
  33. 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>
  34. 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
  35. 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