Lifecycle Hooks in Ember.js Views

Wow! This takes me back! Please check the date this post was authored, as it may no longer be relevant in a modern context.

Back in the days of spaghetti code, you could modify the DOM at any old time and in any old way. This kind of callback was quite common:

$(function(){
  var element = $('.someClass');
  element.addClass('loaded')

  element.on('click', function(){
    element.css('text-decoration', 'strikethrough');
  });
})

Ember discourages you from interacting with the DOM in this ad-hoc way, and instead provides you with a runloop and lifecycle hooks to aid in batching your changes. Making your changes at the same time means the browser needs to paint the screen fewer times, resulting in more consistent performance for well written apps.

View Lifecycle Hooks

Each view has several lifecycle hooks.

willInsertElement and didInsertElement will be called before and after a view is appended to the DOM. The hooks are triggered regardless of if a view is rendered or rerendered. Often didInsertElement is used for triggering a jQuery plugin:

App.SomeView = Em.View.extend({
  template: '{{input type="color" valueBinding="color"}}',
  didInsertElement: function(){
    // Polyfill that color picker with http://bgrins.github.io/spectrum/
    this.$('[type=color]').spectrum();
  }
});

More on this will be explained later, but always keep in mind that didInsertElement will be called during the render queue of the runloop. In practical terms, it is important to understand that only the template immediately associated with this view is assured to be in the DOM. It cannot be assumed that any view or template rendered inside this view has been appended to the DOM.

Ember cleans up it’s own event handlers, but if you add custom event handlers in a view they will need to be removed by hand (or leak memory). The willClearRender hook is ideal for this. It runs before a view is about to be destroyed, or about to be rerendered.

App.SomeView = Em.View.extend({
  template: '<video width="320" height="240" controls><source src="movie.mp4" type="video/mp4"></video>',
  didInsertElement: function(){
    this.$('video').on('play', this, this.play);
  },
  willClearRender: function(){
    this.$('video').off('play', this, this.play);
  },
  play: function(){
    // When you handle an event on your own, you
    // almost always wrap it in a runloop.
    Em.run(this, function(){
      this.get('controller').didStartPlaying();
    });
  }
});

Additionally there is a willDestroyElement callback triggered only when a view is actually destroyed, and not when it is rerendered. Note: Word on the street is that this may be a bug. willDestroyElement should be called when a view is rerendered. I’ve confirmed that from rc3-rc5 this has not been the case, and possibly longer.

The render & afterRender Queues

Ember uses the pattern of a runloop for resolving bound properties, ensuring some computations only run once, and ordering tasks. It is implemented as a series of queues, the most relevant to this discussion being the render and afterRender queues.

The render queue manages Ember’s own template rendering pipeline. By the time this queue has flushed, the property values rendered in DOM match the values of the properties in JavaScript. The view lifecycle hooks all run in this queue. Keep in mind that the whole DOM will not be rendered during those callbacks.

To perform an action after the entire rendering pipeline has finished, schedule a function to run in the afterRender queue.

App.SomeView = Em.View.extend({
  template: '{{template "lots_of_images"}}',
  didInsertElement: function(){
    // This callback runs before lots_of_images has rendered.
    this.scheduleMasonry();
  },
  scheduleMasonry: (function(){
    // scheduleOnce debounces applyMasonry to only run once per
    // runloop. scheduleMasonry is called on didInsertElement, and
    // whenever controller.images changes.
    Ember.run.scheduleOnce('afterRender', this, this.applyMasonry);
  }).observes('controller.images.@each'),
  applyMasonry: function(){
    // http://masonry.desandro.com/index.html
    this.$('.container').masonry({
      itemSelector: '.item',
      columnWidth: 150
    });
  }
});

This pattern provides a way to run code relevant to a given view after all the child views have rendered.

Ember’s runloop and lifecycle hooks are fantastic tools for interacting with the DOM. With a little practice, I expect most developers will begin to appreciate them more than their old spaghetti code.