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. And why should you care?
    WHAT IS
    EMBER?

    View Slide

  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.

    View Slide

  3. A
    STANDARD
    LIBRARY

    View Slide

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

    View Slide

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

    View Slide

  6. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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)

    View Slide

  11. EMBER-METAL.
    EMBER-RUNTIME.

    View Slide

  12. EMBER'S
    BINDINGS

    View Slide

  13. BAKED INTO THE
    OBJECT MODEL.

    View Slide

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

    View Slide

  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.

    View Slide

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

    View Slide

  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.

    View Slide

  18. ALWAYS ASYNC.

    View Slide

  19. ALWAYS ASYNC.
    WHY?

    View Slide

  20. SIDE-EFFECTS.
    It guarantees you the ability
    to make all necessary
    changes before side-effects
    occur.

    View Slide

  21. yehuda.addObserver('spouseName',
    function() {
    $("#me div.spouse span.name")
    .html(yehuda.get('spouseName'));
    });
    BASIC EXAMPLE.
    spouseName changes
    recalculate the new name
    update the

    View Slide

  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.

    View Slide

  23. leah.set('firstName', "Léah")

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  27. leah.set('firstName', "Léah");
    // is this even a valid state?
    leah.set('lastName', "Silber-Katz");
    IN-BETWEEN STATE.

    View Slide

  28. REPEATED WORK.
    recalculate the name
    update the
    recalculate the name
    update the

    View Slide

  29. REPEATED WORK.
    recalculate the name
    update the
    recalculate the name
    update the
    2x

    View Slide

  30. initialize: function() {
    this.model.on('change', this.render, this);
    },
    render: function() {
    this.$el.html(this.model.fullName());
    }
    SAME THING.

    View Slide

  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.

    View Slide

  32. EMBER?
    leah.set('firstName', "Léah")

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  36. EMBER?
    leah. rstName changes
    yehuda.spouseName changes
    recalculate the name
    update the
    leah.lastName changes
    leah.fullName changes
    leah.set('firstName', "Léah") leah.set('lastName', "Silber-Katz")
    later...

    View Slide

  37. COLLECTIONS.

    View Slide

  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 = "" + list.remaining() + " remaining";
    var total = " and " + list.done() + " total";
    this.$el.html("" + remaining + done + "");
    },
    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.

    View Slide

  39. collection.forEach(function(todo) {
    todo.set('isDone', true);
    });
    MULTIPLE SETS.

    View Slide

  40. REPEATED WORK.

    View Slide

  41. REPEATED WORK.
    recalculate remaining
    update the
    recalculate total
    update the
    recalculate remaining
    update the
    recalculate total
    update the
    recalculate remaining
    update the
    recalculate total
    update the
    recalculate remaining
    update the
    recalculate total
    update the
    Nx

    View Slide

  42. EMBER.

    View Slide

  43. statistics.handlebars
    {{remaining}} remaining
    and {{done}} done
    TEMPLATE.

    View Slide

  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

    View Slide


  45. {{template "statistics"}}

    TODOS TEMPLATE.

    View Slide

  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.

    View Slide

  47. VIEW
    HIERARCHY

    View Slide


  48. AD-HOC.
    AppView
    TodoView
    TodoView
    TodoView



    TodoView
    TodoView
    TodoView
    Views DOM

    View Slide

  49. YOU END UP
    WANTING AN
    ENFORCED
    HIERARCHY.

    View Slide

  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.

    View Slide

  51. JUST THERE.

    View Slide

  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.

    View Slide

  53. ELIMINATION OF
    BUSYWORK.

    View Slide

  54. {{category}}

    {{#each}}

    {{name}}

    More details

    {{#unless readOnly}}
    {{view App.EditPerson}}
    {{/unless}}

    {{/each}}

    EXAMPLE TEMPLATE.

    View Slide

  55. {{category}}

    {{#each}}

    {{name}}

    More details

    {{#unless readOnly}}
    {{view App.EditPerson}}
    {{/unless}}

    {{/each}}

    EXAMPLE TEMPLATE.
    append controller.category
    and
    register observer on
    controller.category

    View Slide

  56. {{category}}

    {{#each}}

    {{name}}

    More details

    {{#unless readOnly}}
    {{view App.EditPerson}}
    {{/unless}}

    {{/each}}

    EXAMPLE TEMPLATE.
    iterate over people
    array changes rerender
    item added add to DOM
    item removed remove
    from DOM

    View Slide

  57. {{category}}

    {{#each}}

    {{name}}

    More details

    {{#unless readOnly}}
    {{view App.EditPerson}}
    {{/unless}}

    {{/each}}

    EXAMPLE TEMPLATE.
    people can be any object
    that mixes in Ember.Array

    View Slide

  58. {{category}}

    {{#each}}

    {{name}}

    More details

    {{#unless readOnly}}
    {{view App.EditPerson}}
    {{/unless}}

    {{/each}}

    EXAMPLE TEMPLATE.
    append person.name
    and
    register observer on
    person.name

    View Slide

  59. {{category}}

    {{#each}}

    {{name}}

    More details

    {{#unless readOnly}}
    {{view App.EditPerson}}
    {{/unless}}

    {{/each}}

    EXAMPLE TEMPLATE.
    when the link is clicked,
    trigger the showPerson
    event on the router
    also generate a URL

    View Slide

  60. {{category}}

    {{#each}}

    {{name}}

    More details

    {{#unless readOnly}}
    {{view App.EditPerson}}
    {{/unless}}

    {{/each}}

    EXAMPLE TEMPLATE.
    only render this area if
    person.readOnly is false.
    if that changes add or
    remove as appropriate

    View Slide

  61. {{category}}

    {{#each}}

    {{name}}

    More details

    {{#unless readOnly}}
    {{view App.EditPerson}}
    {{/unless}}

    {{/each}}

    EXAMPLE TEMPLATE.
    insert an App.EditPerson
    child view
    its context property will be
    the current person

    View Slide

  62. RESULT.
    App.ApplicationView
    {{controller.category}}
    {{#each}}
    {{person.name}}
    {{#unless person.readOnly}}
    App.EditPersonView

    View Slide

  63. {{unbound category}}

    {{#each}}

    {{name}}

    More details

    {{#unless readOnly}}
    {{view App.EditPerson}}
    {{/unless}}

    {{/each}}

    MORE CONTROL.

    View Slide

  64. {{category}}

    {{#each group=true}}

    {{name}}

    More details

    {{#unless readOnly}}
    {{view App.EditPerson}}
    {{/unless}}

    {{/each}}

    SOON.

    View Slide

  65. DOES THE RIGHT
    THING BY DEFAULT.

    View Slide

  66. ROUTING

    View Slide

  67. STATE
    MANAGEMENT.

    View Slide

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

    View Slide

  69. application.handlebars
    My Blog
    {{outlet}}
    posts.handlebars

    {{#each}}

    {{title}}

    {{/each}}

    post.handlebars
    {{title}}

    {{intro}}



    {{body}}

    TEMPLATES.

    View Slide

  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.

    View Slide

  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.

    {{#each}}

    {{title}}

    {{/each}}

    View Slide

  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

    View Slide

  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

    View Slide

  74. STATE MANAGER
    GOODIES.

    View Slide

  75. LOGGING.

    View Slide

  76. DEBUGGING
    PRODUCTION APPS.

    View Slide

  77. AVOID IMPOSSIBLE
    CASES.

    View Slide

  78. DEMO.

    View Slide