Blurb & Ember

Estella Madison
Front End Engineer
@chicagoing

Issues getting started with Ember

Our experience learning the framework


Performance

Tackling performance issues


Best practices

Taking advantage of built-in features early on

Blurb's Ebook Editor

  • HTML5 editing tool built with Ember
  • Print to ebook (iOS) converter
  • Fixed layouts
  • Enhanced ebooks (video & audio)
  • Started development in 2011 using SproutCore 2

Unique

  • Not a CRUD-style application
  • Heavy image manipulation with Javascript and CSS3
  • Still uses v1.0.0-pre.4


Our Application is BIG

Issues getting started with Ember

  • Documentation (early on)
  • The "Ember way"
  • Controller interdependencies
  • Too much logic in our views
  • Debugging
  • Bindings

Documentation

We used the online documentation but relied heavily on comments included with the unminified source file:

/**
  An `Ember.Binding` connects the properties of two objects so that whenever
  the value of one property changes, the other property will be changed also.

  ## Automatic Creation of Bindings with `/^*Binding/`-named Properties

  You do not usually create Binding objects directly but instead describe
  bindings in your class or object definition using automatic binding
  detection.

Managing controller
interdependencies


Earlier versions of Ember didn't automatically create your controllers. We started off with adding most of them to the global namespace.


App.bookController = App.BookController.create();App.editorController = App.EditorController.create();App.pageController = App.PageController.create();



Ember 1.0 Prerelease 2

introduced
connectControllers()

 YEAH! 

App.ApplicationRoute = Ember.Route.extend({
  setupController: function(controller) {
    var bookController = this.controllerFor('book'),
        pageController = this.controllerFor('page'),
        editorController = this.controllerFor('editor');
    bookController.connectControllers('editor', 'page');
  }
});

Then when we upgraded to Ember 1.0 Prerelease 4...

 No more connectControllers 
 NO! 


We noticed a needs property in Ember.ControllerMixin:

 Ember.ControllerMixin.reopen({
  concatenatedProperties: ['needs'],
  needs: [],  ...});

But no comments; no online documentation.

So we re-added
connectControllers ourselves:

var connectControllers = function() {
  var controllerName,
      controllerNames = Array.prototype.slice.apply(arguments);
  for (var i=0, l=controllerNames.length; i<l; i++) {
    controllerName = controllerNames[i];
    this.set(
      controllerName+'Controller',
      route.controllerFor(controllerName));
  }
};
Ember.Controller.reopen({ connectControllers: connectControllers });
Ember.ObjectController.reopen({ connectControllers: connectControllers });
Ember.ArrayController.reopen({ connectControllers: connectControllers });

This allowed us to focus on updates for
view context and the new router.

The right way to connect controllers


App.BookController = Ember.Controller.extend({
  needs: ['page', 'editor']
  ...
});

Too much logic in our views


We quickly realized that moving more of logic into our controllers made our app more maintainable.

A view should only maintain its styles, classes, and events


Source madhatted.com

The changes included with the

Ember 1.0 Prerelease

validated this approach


View context was changed to the supplied controller.

Debugging

(before we had Ember Inspector)


Chrome Dev Tools is the obvious debugging tool, but some of the vague errors thrown by Ember made tracing the source difficult.

Cannot perform operations on a Metamorph that is not in the DOM

We were like "what? where?"

  • Occurred on views surrounded by {{#if}} handlebar helpers.
  • Also, as implied, it occurred when manipulating a view before it was in the DOM or after it was destroyed (triggered by observers).
  • Resolved the issue by wrapping any DOM manipulation with:
if (this.get('state') === 'inDOM')

Other Debugging Tips


Ember.inspect()

Prints out a readable string for the object.


Ember.View.views["id"]

Given the ID of a view, you can get the view properties.


Tom Dale discusses more debugging tips

http://vimeo.com/37539737

Bindings & observers didn't always trigger in the ideal order

Sometimes Ember.beforeObserver() helped. Other times we added additional properties to cascade observers.

App.MockController = Ember.Controller.extend({
    property1: null,

    observer1: function() {
        ...
    }.observes('property1'),

    // We want this observer to trigger first
    observer2: function() {
        ...
    }.observes('property1')
});

A second property helped dictate the order in which these triggered

App.MockController = Ember.Controller.extend({
    property1: null,
    // We add a second property to trigger the rest of the observers
    observer2triggered: false,

    observer1: function() {
        ...
    }.observes('observer2triggered'),

    // We want this observer to trigger first
    observer2: function() {
        this.set('observer2triggered', true);
    }.observes('property1')
});

Performance

Big Ember.CollectionViews caused browsers to render slowly

Media panel

Display all the images and media files uploaded by the users.


Birds eye view

Gives an overview of the book, displaying a thumbnail size view of each page.

Media panel

We built a custom IncrementalCollectionView.


  • Extends from Ember.CollectionView.
  • Incrementally loads a certain number of items at a time.
  • Doesn't block page rendering.
  • Used event delegation to minimize the number of event listeners.

Birds eye view

Due to time constraints, we built the first version using existing views to render thumbnail-size versions of a page

Main problems

  • Books can have any number of pages.
  • Pages can be very complex, with any number of image and text containers.

Solution: <canvas>

  • Renders much faster than multiple, nested Ember views.
  • Drawing images was fairly simple with the API.
  • Since text isn't legible at the thumbnail size, we just draw lines to represent a text container.

Best practices

  • oneWay bindings
  • Avoiding jQuery
  • Setting properties

Use oneWay bindings

var obj = Ember.Object.create({
  valueBinding: Ember.Binding.oneWay('value')
});

According to the documentation:

One way bindings are almost twice as fast to setup and twice as fast to execute because the binding only has to worry about changes to one side.

Avoid using jQuery (for everything)

At first, we overused jQuery because it was what we knew best.


.$().fadeIn() & .$().fadeOut()

Replaced these with the CSS transition property.


.$().append() & .$().remove()

Instead of using jQuery to append/remove DOM elements, we use Ember.ContainerViews when possible.


Avoided jQuery plugins as much as possible.

Especially jQuery UI. We knew these would bloat our code.

Best practices


Setting properties


  • set/setProperties
  • Ember.beginPropertyChanges()/Ember.endPropertyChanges();
  • Ember.changeProperties()


Source: Stackoverflow.com, "EmberJS Set Multiple Properties At Once", Luke Melia's response

Set multiple properties at once


set/setProperties

Set a list of properties on an object. These properties are set inside a single `beginPropertyChanges` and `endPropertyChanges` batch, so observers will be buffered.

this.set({ property1: 'val1', property2: 'val2' });

Group property changes

... so that notifications will not be sent until the changes are finished. Useful for making a large number of changes at once.


  • Ember.beginPropertyChanges Call this at the beginning of the changes to begin deferring change notifications.
  • Ember.endPropertyChanges Delivers the deferred change notifications and ends deferring.
// Suspend observer triggers
Ember.beginPropertyChanges();
if (this.get('condition1')) { obj1.set('property1', 'val1'); }
if (this.get('condition2')) { obj2.set('property2', 'val2'); }
// Restore triggers and flush
Ember.endPropertyChanges();

Make a series of property changes together in an exception-safe way


Ember.changeProperties

Accepts a callback and calls Ember.beginPropertyChanges, then the callback, and then Ember.endPropertyChanges, even if your callback raised an exception.


Ember.changeProperties(function() {
  obj1.set('property1', 'changing this make things blow up');
  obj2.set('property2', 'val2');
});

Next for Blurb & Ember


Upgrade our ebook editor to Ember 1.0

Rebuild our checkout using Ember



Questions?


Make a book with Blurb

As a parting gift we're extending a coupon to attendees:

  • 15% off one order up to $150
  • Single use only
  • Promotion is valid for all books in the bookstore if you would rather buy a book versus make a book
  • Expires December 8, 2013


COUPON CODE: ember.js

Blurb & Ember

By Estella Gonzalez Madison