Animation support in Ember 1.1


#1

Hello everybody!

The core team has wished for built-in animation support as a feature in Ember 1.1. This is a proposal for the public API.

Scope

Only route transitions. No list view animation for now.

Enabling animated transitions on an outlet

All you have do is define a transition effect on your outlet.

{{outlet transition="slideLeft"}}

There is no special {{animatedOutlet}} helper and no this.transitionToAnimated() like Ember Animated Outlet had. It’s all built-in and triggered by having transition set to a non-empty value.

Handling “back-transitions”

When a user hits the browser’s back-button we typically want to handle this in a different way. If an outlet has a slideLeft effect, we want to perform a slideRight effect when navigating back in the browser’s history stack. The “back-transition” effect should also be configurable on the outlet:

{{outlet transition="slideDown" backTransition="slideRight"}}

If you want to trigger the equivalent as the user hitting the browser’s back-button you can use the {{linkBack}} helper, which works just like {{linkTo}} except that you can’t specify a route path. {{linkBack}} will always take you one step back in the browser’s history stack. Example:

{{#linkBack}}Go back{{/linkBack}}

If you programmatically want to trigger a back-transition from a controller/route you can call this.transitionBack(). Example:

App.PostEditController = Ember.ObjectController.extend({
  save: function() {
    var self = this;
    this.get('model').commit().then(function() {
      self.transitionBack();
      //This will take you back to whichever route triggered the edit, which could be either `post.index` (show) or `posts.index` (list). The `backTransition` effect will automatically be used
    });
  }
});

The transitionBack method delegates to the Ember.Location instance being used by the router. Ember.HistoryLocation and Ember.HashLocation instances will delegate to window.history.go(-1), while Ember.NoneLocation will have to store a URL history stack itself (we don’t want to mess with window.history when running integration tests for example).

Transition flow

  • As soon as a transition starts animating it can’t be stopped. Any following transitions (both animated and non-animated) will be queued until the transition animation is done. The queue can only hold one transition. Consecutive transitions will cancel pending transitions (which is the same case with async transitions). Transition effects should be fast, so we don’t block the user’s workflow.
  • A transition should never animate more than one outlet (the topmost outlet that changes it’s contents in the transition).

Registering your own effects

Ember should ship with its own standard effects. Examples: slideLeft, slideRight, slideUp, slideDown, flipLeft, flipRight etc.

But it should also be easy to add your own effects. This can be done this way:

Ember.ContainerView.registerTransition('slideOverLeft', function(ct, oldView, newView, done) {
  //Perform animation and invoke `done` when completed
});

You can tell Ember about back-transitions:

Ember.ContainerView.registerTransition('slideOverRight', function(ct, oldView, newView, done) {
  //...
});

Ember.ContainerView.registerBackTransition('slideOverLeft', 'slideOverRight');

This way you can do {{animation transition="slideOverLeft"}} and Ember will automatically know to perform slideOverRight when the user clicks the back-button.

Animation implementation

The animations should definitely use CSS transitions where supported with a fallback to old school JS animation.

I believe the best way to implement the transitions is to utilize an external micro-library built to programmatically handle CSS transitions. The best one I know of is jQuery.transit. It will add an extra dependency, but only if you’re going to use animation.

The (bad) alternative would be to build a lot of css transition handling into Ember with fallback to jQuery.animate. But there is no sense in adding this big a maintenance burden.

Issues from Ember Animated Outlet that will be fixed:

  • Views that are animated will be frozen (i.e. no DOM changes can occur). This will avoid the issue where an outgoing view is changed since it’s controller changes its content. It will also improve FPS especially on mobile (source: @krisselden).
  • Animated transitions to the same route with a different model will work. It does not work today since the same view is reused = nothing to animate out.
  • Correct back-button handling.
  • Child views no longer need to be explicitly defined so they get an element and are not just metamorphs.

Known limitations

  1. Since the transition effect is set directly on the outlet it is not possible to manually override which effect to use for a specific transition. You can somewhat achieve this by using a binding for the transition property on the outlet: {{outlet transition=myTransitionProperty}}.
  2. It’s a limitation (that we probably want to look at at a later point) that animations are queued. With some effects, such as slide effects, it would actually be possible to animate multiple transitions at the same time. Think of a photo album where the user can click Next and Previous in the bottom. But there are a number of issues with this that we need to solve:
  • When a transition is midway and the user hits the back button the router needs to reuse the exact same view so we don’t duplicate the first view’s content.
  • The animation effect code has to take multiple views into account (not just oldView and newView).
  • Some effects are not compatible with each other meaning that they can’t be performed at the same time (e.g. a slideLeft and a flip).

Give me all the feedback!


Internal view lifecycle hooks for animation purposes
#2

How are you distinguishing forward/back? This is resource <-> nested resource? What about between sibling routes? Will you be able to provide an animation name in route.transitionTo?

Also, seems like the animation definition can contain forward/back and you could just specify horizontalSlide.


#3

Good questions.

The idea is that everything is a forward transition unless invoked with the browser’s back button, {{linkBack}} or this.transitionBack(). It doesn’t matter how you’re navigating around in the route hierarchy.

This limitation #1 I mentioned. You can achieve it by doing:

var self = this;
this.set('myTransitionProperty', 'flip');
this.transition('somewhere').then(function() {
  self.set('myTransitionProperty', 'slideLeft');
});

I would like to avoid changing the API of this.transitionTo like I had to do in Ember Animated Outlet by adding a this.transitionToAnimated(). Another option would be to add a setter on the Transition object:

this.transitionTo('somewhere')
  .setTransitionEffect('flip')

That would actually be a really good solution. Flexible, and optional to use.[quote=“krisselden, post:2, topic:1977”] Also, seems like the animation definition can contain forward/back and you could just specify horizontalSlide. [/quote]

True. I just think it’s more simple to think in forward transition effects and then the framework knows what the opposite is. Some effects don’t necessarily have a back-effect (like fade).


#4

What about implementation details? Since you register new animations on ContainerView I hope this is where you implement the animation logic.

If the animations are implemented (as I think they should be) on the ContainerView what is preventing us from having animations on lists? Or when you say no list animations you mean “no sugar syntax” but we can use lower level api? What is the low level api?


#5

As for animations implementations, I was thinking that maybe it is better to go full css animations way? Like angular. Using https://github.com/h5bp/Effeckt.css maybe?


#6

Thanks for weighing in @tchak.

This discusion was meant to be primarily about the public API. Implementation details is next step. But yes, the idea is that Ember.ContainerView will be responsible for coordinating the animation when currentView changes.

List animations are different than route animations. Lists should animate each added/removed item in isolation. Routes need to coordinate an appearing view and a disappearing view. The two are probably going to share a lot of logic. But let’s try to stick to route transition animations in this thread.


#7

I’m fine with the animation set on the outlet by default, but if the view being rendered (or destroyed) has any animation declared, it should override the outlet’s setting.

So the precendence level may look like this:
outlet < view being rendered/removed < direct call to setTransitionEffect() method


#8

@seilund have you seen my early commit in ember (that was merged and immediately reverted maybe a year ago) that added animations support to ContainerView? It was doing exactly what you describe, coordinate currentView. And this is why it was reverted. The idea was that coordination should be lower level - on any child add/remove of ContainerView, so we could handle currentView (outlets) or collection views.


#9

jQuery.transit is full CSS animations. Do you mean use CSS stylesheets and classes? I did not have good experience with that in Ember Animated Outlet. It makes the code ugly, and adds a really annoying css stylesheet dependency.


#10

Can you elaborate a bit more?


#11

jQuery.transit use css transitions not css animations. In my opinion it make sense to have animation keyframes declared in a stylesheet (I see nothing ugly about it). Using classes is probably not necessary. You can programaticly assign animations and listen to animationEnd just like with translations.


#12

@seilund this is the commit I was referring to https://github.com/emberjs/ember.js/commit/a2cd03b0f219142c52f291de7a439931d83ffaa3


#13

Regarding backwards transitions, {{linkBack}}, and friends… Let’s say I have a standard list view with slideLeft to drill down to an item detail and slideRight to go back. If I enter the app via a deep link to am item detail, I have no history. How would a “back to list view” button animate correctly in this proposal?


#14

I don’t think adding css stylesheet dependencies in a JS project is not ugly. You can achieve the same as keyframes with jQuery.transit, and you get a reliable callback when animation is complete (I’ve had trouble with animationEnd fx in PhantomJS). It’s also full backwards compatible (at least for simple slide effects).


#15

Sure.
If we have this:

{{outlet transition="slideLeft"}}

the animation will be slideLeft unless the view has:

App.FooView = Ember.View.extend({
  transitionIn: 'slideUp'
});

which will override the {{outlet}}'s transition, but still loses to:

this.transitionTo('somewhere')
  .setTransitionEffect('flip');

(Don’t judge the API/naming too hard, my sole point is to demonstrate which animation “wins” if we have them defined in multiple places.)


#16

Good point. It would not know how to do this. {{linkBack}} would only work if the browser actually has a history. Which is suitable when you have routes that can be navigated to from many places.

If you know that your item detail should always go back to list view, you can do:

this.transitionTo('listview')
  .setTransitionEffect('slideRight');

I would also be fine with adding a transitionEffect property on {{linkTo}}:

{{linkTo "listview" transitionEffect="slideRight"}}Back to list view{{/linkTo}}

#17

@seilund as far as I know, on mobiles keyframe animations is probably the only viable option… I may be wrong. But any way, I suppose we are going to have a perf test suite along side this work, so we could see what is doable.


#18

Makes sense. Great idea.


#19

@xeppelin I would actually expect the other way around. We definitely should be able to set animation on the view, but outlet definition should take the precedence.


#20

We should of course test the perf. I haven’t heard that setting css transition properties with JS should be slow on mobile before. Do you have any sources for this?