Blurb & Ember
Estella Madison
Front End Engineer
@chicagoing
Issues getting started with Ember
Performance
Best practices
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
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
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
Blurb & Ember
- 2,897