Animating route transitions in ember.js

Animations aren’t just eye candy - they make your app feel faster. Unfortunately Ember still hasn’t settled on a standard method for animations.

There have been some noble attempts, for example Ember Animated Outlet by Sebastian Seilund, however, it doesn’t (currently) work with ember 1.0.

URLs, the new router, and promises

Ember has been placing emphasis on the URL as the central “state” of a web application. There’s a great talk by Tom Dale talking about the importance of the URL. The amazing work done on the new router by Alex Matchneer means we now have a promise based system for transitioning between routes and loading models, which makes it a nice destination for adding animations.

For some more background on the current situation of animations in ember, read through this discuss thread. A lot of the discussion here goes into depth of handlebars syntax and registering transitions on to container views. Personally, I think Thomas Reynolds has the right idea. Ember should provide javascript callbacks and it should be up to the user to decide how to deal with it.

Animation hooks

My solution is to add a promise hook to both the entering and exiting of a route. Include this code after including ember. (If you’re not a coffeescript fan, check the jsbin below for a javascript version.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
App.AnimatedRoute = Ember.Route.extend
  _transitioning: false
  willExit: -> Ember.RSVP.resolve()
  willEnter: -> Ember.RSVP.resolve()

  deactivate: -> @_transitioning = false

  # Have to eventually reset the transitioning property to false so that
  # future transitions out of this route get animated.
  afterModel: (model, transition) ->
    transition.then =>
      # Sometimes the template hasn't been rendered yet, even in aftermodel.
      new Ember.RSVP.Promise (resolve, _) =>
        Ember.run.next this, ->
          @willEnter().then resolve

    @_super.apply this, arguments

  actions:
    willTransition: (transition) ->
      # Only want to fire the exit transition when we're actually exiting -
      # i.e. not when we're transitioning into subviews. The willTransition
      # callback gets fired then too.
      isParent = transition.handlerInfos.pluck('name').contains @routeName
      return true if isParent
      return true if @_transitioning

      @_transitioning = true
      transition.abort()
      @willExit().then -> transition.retry()

You can use this in any of your routes by defining a willExit or willEnter property. If you return a promise from either, it will wait until the transition is completed before allowing the transition. Here’s a quick jsbin example showing fading of subroutes

One of the nice things about this is you can use the animation library of your choice to solve the problem - ember just asks for a promise and gets out of the way. You can use jquery (as in the example), or CSS transitions (you will have to listen for the transition finish callback ). Or my personal favorite - tween.js and requestAnimationFrame.

Nobody’s perfect

This solution certainly isn’t perfect. Notably, it doesn’t work for animations of views inside of a route - for example doing animations as you insert or remove items from a list.

Currently this isn’t possible to do on a view - there is no way to defer the willDestroyElement call for example. A patch allowing callbacks was briefly accepted but reverted soon after. If you need animated views, I’d say looking through that patch is a good place to start. Hopefully ember will move the view rendering / destroying process to be promise based as well.

NOTE

Ember moves quickly. This post was written with the following version of ember:

DEBUG: -------------------------------
DEBUG: Ember      : 1.3.0-beta.1+canary.74b7210f ember.js:3229
DEBUG: Handlebars : 1.0.0 ember.js:3229
DEBUG: jQuery     : 2.0.2 ember.js:3229
DEBUG: -------------------------------

You can check the version you are running by adding this line to your JS before initializing your application

Ember.LOG_VERSION = true