Hierarchical Route Animations in Angular

by Stephen Fluin

2017-07-12

Hierarchical Route Animations in Angular
Angular makes animating visual elements based on state changes easy, and the most recent 4.2 release includes a lot of new features that makes animations even more powerful.
One of the new powers that is available is the ability to query for other sub-elements as part of an animation. This enables many new workflows, but one of the coolest is that ability to animate route transitions.
Sample Route Animations

Assumptions

I'm going to take an existing application with multiple routes. I'm going to give each route a depth (1, 2, 3), and then animate things sliding in from the right or from the left depending on whether we are descending or ascending the tree.
When I go from / to /subpage/, the subpage should slide in from the right, and the home page will leave to the left.
I could have designed these route animations however I wanted, but I feel like this is an easy to implement, relatively familiar paradigm.

The Process

To animate upon route change, we're going to need to follow a 3 step process.
  1. Setup CSS
  2. Setup Route Data
  3. Create Animation

I also cover this process in a video demo:
video demo

Setup CSS

Let's get back to basics with CSS. We are now about to be rendering two routes simultaneously as siblings of our <router-outlet> HTML element in the DOM. If both of these are being shown at the same time, you'll want to make sure that they can be rendered
The easiest way I have found to do this is to give the parent of my <router-outlet> a CSS class such as route-container.
<div class="route-container">
    <router-outlet></router-outlet>
</div>

We can't use this class on the router outlet itself, because remember, the routed components are added as peers of the router outlet.
Then, for this CSS class, let's setup our styles so things render nicely.
.route-container {
    position:relative;
}
.route-container>* {
    display:block;
}

Setup Route Data

Now let's add the metadata needed to track the depth of a given route manually. I could do this automatically by looking at the path and counting the number of /s in it, but I wanted to be more specific with my routes.
For each route in my application, I'm going to add a data property, with the depth value {depth: 1}. So now my routing configuration might look include:
{path: '', component: HomeComponent, data: {depth: 1}},
{path: 'posts/:id', component: PostComponent, data: {depth: 2}},
{path: 'posts/:id/details', component: PostDetailComponent, data: {depth: 3}},

Create The Animation

Let's attach the animation via our html template. We'll do this in the template that contains our router-outlet. We'll make up a name such as routeAnimation, and then pass it a state expression which tells us what the current page depth is. In order to figure out the current page and its depth, we'll want to name our router-outlet, and then pass it into the method that determines the state.
<div class="route-container" [@routeAnimation]="getDepth(myOutlet)">
    <router-outlet #myOutlet="outlet"></router-outlet>
</div>

We have referred to a method getDepth that doesn't exist yet. Let's add it to the component associated with this template.
getDepth(outlet) {
    return outlet.activatedRouteData['depth'];
}

The final part of creating the animation is to add our specific transitions to the routeAnimation. We do this in our component decorator's animations array. (We'll create this because it probably doesn't exist yet).
We're going to need some extra imports to get access to the methods we'll need.
import { trigger, transition, group, query, style, animate } from '@angular/animations';

First the code, then we'll go through this line by line..
animations: [
    trigger('routeAnimation', [
        transition('1 => 2, 2 => 3', [
            style({ height: '!' }),
            query(':enter', style({ transform: 'translateX(100%)' })),
            query(':enter, :leave', style({ position: 'absolute', top: 0, left: 0, right: 0 })),
            // animate the leave page away
            group([
                query(':leave', [
                    animate('0.3s cubic-bezier(.35,0,.25,1)', style({ transform: 'translateX(-100%)' })),
                ]),
                // and now reveal the enter
                query(':enter', animate('0.3s cubic-bezier(.35,0,.25,1)', style({ transform: 'translateX(0)' }))),
            ]),
        ]),
        transition('3 => 2, 2 => 1', [
            style({ height: '!' }),
            query(':enter', style({ transform: 'translateX(-100%)' })),
            query(':enter, :leave', style({ position: 'absolute', top: 0, left: 0, right: 0 })),
            // animate the leave page away
            group([
                query(':leave', [
                    animate('0.3s cubic-bezier(.35,0,.25,1)', style({ transform: 'translateX(100%)' })),
                ]),
                // and now reveal the enter
                query(':enter', animate('0.3s cubic-bezier(.35,0,.25,1)', style({ transform: 'translateX(0)' }))),
            ]),
        ]),
    ])
]

We first created an animations property in our @Component decorator. Inside this we have an array of transitions. Each transition corresponds to a state change, based on the listed states.
We have two transitions, one for descending the hierarchy of our application, one for ascending, but they function nearly identically.
Within each transition I have 4 things: a style, two queries, and a group.

The Style

The style refers to the height of the .route-container that holds both of these routes. We style it's height to match the height as it will be after the transition (!).

First Query

Our first query moves the incoming route horizontally off of the page so that we don't see it until it has animated onto the page.

Second Query

Our second query ensures that we can take full control of the positioning of both our entering and leaving routes.

The Group

The final piece, the group method, is responsible for the actual animation that happens. Simultaneously, we want our leaving element to leave horizontally, and our entering element to enter horizontally. Each should animate over around 0.3s, using a cubic bezier curve in terms of animation speed.
The animation itself is using the transform attribute and translating its X position to achieve the horizontal movement.

Summary

That's it! We now have the routes of our application labeled with metadata, we have an animation that uses this metadata to trigger transitions, and transition animations defined based on the type of transition. If we run our app and move between routes, our routes should animate.

But Why?

I feel like some people undervalue animations. They are sometimes perceived as unnecessary visual sugar. While you can build a great application without animations, they actually serve a very critical role in communicating to your user.
Very often when we build Single Page Applications, we need some time to load data for the user. Animations give us a few milliseconds to do this sort of data loading, while at the same time giving the user immediate feedback that their intent was received and that their requested data is coming.
Animations are also critical in the way that they turn interactions into a more effective conversation. Users may know when they are ascending or descending a hierarchy, but visually showing this as they move around in your application confirms this understanding. It makes your application feel more interactive and engaging.

Extra Notes

You will probably want to tweak the height of the animation so that the parent element has the height of the tallest route, regardless of whether it is the source route or destination route. This isn't the easiest thing right now, but you can write a method that memoizes the height after each route transition that will get you what you want.
Take a look at this commit for an example.