Liquid Fire: Animations & Transitions for Ember Apps

Liquid Fire is a toolkit for managing animated transitions in an Ember application. Like Ember itself, our goal is to cultivate shared abstractions so we're free to focus on bigger and better ideas. Good defaults. Convention over configuration. Composable pieces all the way down.

Key Ideas

Transitions need to be implemented within the view layer, but deciding what kind of transition to do at any given time is a higher-level question, dependent on the relationships between different routes and models.

Therefore, we split the responsibility between three key pieces: template helpers, the transition map, and transitions.

Template Helpers

The library provides helpers like <LiquidOutlet />, <LiquidIf />, etc, that are nearly drop-in replacements for the equivalent normal Ember helpers. The key difference is that they don't update instantly when bound data changes. Instead they consult your application's transition map, and if they discover a matching transition, they give the transition control over both the old and new content simultaneously. Read more about Template Helpers.

Transition Map

The transition map is analogous to your normal Ember router map. It's a single place to declaratively capture rules about how the pieces of your application relate to each other. By convention, the transition map goes in app/transitions.js, and it looks something like this:

export default function () {
  this.transition(
    this.fromRoute('people.index'),
    this.toRoute('people.detail'),
    this.use('toLeft'),
    this.reverse('toRight'),
  );
}
Old-school apps without a module resolver should pass their transition map function to LiquidFire.map. There is an example here.

Read more about the Transition Map.

Transitions

The transition map above mentions two named transitions: toLeft and toRight. These are both predefined transitions that come with the library. But you can compose new transitions too. They look like this:

import { animate, stop } from 'liquid-fire';

export default function fade(oldView, insertNewView) {
  stop(oldView);
  return animate(oldView, { opacity: 0 })
    .then(insertNewView)
    .then(function (newView) {
      return animate(newView, { opacity: [1, 0] });
    });
}

(In these examples, opacity: [1, 0] uses velocity's "force-feeding" capability to specify [endValue, startValue], so the meaning is "animate the opacity from 0 to 1".)

Transitions are implemented with a promise-based API that gives you control over all the relevant timing, including when to insert the new view into the DOM. Read more about Transitions.