Animating Angular Route Transitions
Tags: angular, animation, router, routing
Published: 2021-06-07 17:07:03

Jared Youtsey | ng-conf | Oct 2019

#### Add style to your application by animating your route transitions!

For this article I’m going to assume you already understand the basics of Angular routing and components. I won’t bore you with building an entire application. We’ll get right to adding animations so you can see immediate results!

The finished code for this example can be found [here](https://github.com/fivedice/ng-router-animation).

#### Add BrowserAnimationsModule

In your `app.module.ts` add `BrowserAnimationsModule` to the module `imports`.

```
...
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
    imports: [
        ...,
        BrowserAnimationsModule
    ],
    ...
})
export class AppModule {}
```

#### A Note On Unit Testing
For unit testing, import `NoopAnimationsModule` instead. This fulfills the contracts while isolating unit tests from having to deal with the transitions.

#### Animation Affects User Experience
Have you ever seen a PowerPoint presentation that had a thousand different transitions, fonts, and colors? Yuck. Take a lesson and keep your transitions simple and consistent to avoid confusing or overwhelming your users.

#### The Premise
For this example, I’ll present a simple set of animations that make sense in the context of navigating forward and backward. Views are animated left or right based on the direction the router is navigating. We’ll have three components named `OneComponent`, `TwoComponent`, and `ThreeComponent`, for simplicity’s sake. When navigating from _One_ to _Two_, _One_ will slide out to the left while _Two_ will slide in from the right. _Two_ to _Three_ will do the same. When navigating from _Three_ to _Two_ the animations will be reversed. In addition, the opacity of the views will be animated as they leave and enter the page.

#### States, Transitions, and Triggers, Oh My!
State is a static style definition. A transition defines how a property in the style will change. A trigger defines what action will cause one state to transition to another state.

* State = What
* Transition = How
* Trigger = When
* “animation” = _Triggered transition(s)_ from one _state_ to another.

#### Router Configuration
To connect animations to the router we must add a `data` property to the route configuration. Here are our modified routes:

```
const routes: Routes = [
 {
  path: '',
  children: [
   {
    path: 'one',
    component: OneComponent,
    data: { animationState: 'One' }
   },
   {
    path: 'two',
    component: TwoComponent,
    data: { animationState: 'Two' }
   },
   {
    path: 'three',
    component: ThreeComponent,
    data: { animationState: 'Three' }
   },
   {
    path: '**',
    redirectTo: 'one'
   }
  ]
 },
 {
  path: '**',
  redirectTo: 'one'
 }
];
```

The name `animationState` is arbitrary. But, you’ll need to keep track of what you use. I’ve used this name because we’re defining **what** animation state this route represents. State = What.

#### AppComponent Configuration
Start by configuring the `AppComponent` to set up the animations for the route changes. In `app.component.ts` add a method:

```
prepareRoute(outlet: RouterOutlet) {
  return outlet && 
    outlet.activatedRouteData && 
    outlet.activatedRouteData['animationState'];
 }
```

Notice the check for a route with data for the state specified property, `animationState`.

Now, hook up the template. First, let’s add a template variable so that we can get a reference to the `<router-outlet>`.

```
<router-outlet #outlet="outlet"></router-outlet>
```

Next, add a synthetic property to the container element of the `<router-outlet>`. It’s critical that it be on a container div, not on the `<router-outlet>` itself. This synthetic property’s name is arbitrary, but it’s good to understand that it will correspond to an animation trigger’s name. For the sake of this example, let’s call it `triggerName`.

```
<div [@triggerName]="prepareRoute(outlet)">
  <router-outlet #outlet="outlet"></router-outlet>
</div>
```

We pass the method `prepareRoute` with the argument of the template variable `outlet` to the synthetic property `@triggerName`.

At this point, if you run the application, you’ll find that there is an error in the console:

```
ERROR Error: Found the synthetic property @triggerName. Please 
include either "BrowserAnimationsModule" or "NoopAnimationsModule" 
in your application.
```

But, wait, we did that already?! Angular is confused because we haven’t actually defined the trigger yet! So, let’s do that now.

#### Define the Animation
Remember, an animation is caused by a trigger that causes a transition from one state to another state. When we define an animation we start with the trigger and work inward on that definition.

Create a new file named `route-transition-animations.ts` next to `app.component.ts`. This will contain the trigger definition, `triggerName`, and the transitions from and to the states we wish to animate.

```
import { trigger } from '@angular/animations';
export const routeTransitionAnimations = trigger('triggerName', []);
```

Here we finally define the trigger `triggerName`! The array argument is where we will define the transitions.

Before we define the transitions, let’s hook the `app.component.ts` to the trigger definition:

```
...
import { routeTransitionAnimations } from './route-transition-animations';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
  animations: [routeTransitionAnimations]
})
export class AppComponent {...}
```

Now, let’s go back and flesh out the trigger’s transitions in the `route-transition-animations.ts`.

Angular uses simple arrow syntax to define the transition from one state to another. For example, if we want to handle the navigation from _One_ to _Two_ we use `One => Two`. If we want to handle both directions, we can use a bi-directional arrow, `One <=> Two`, and then the transition will be applied going from _One_ to _Two_ and from _Two_ to _One_.

Angular has some powerful pre-defined concepts in addition to the named states.

* `void` = an element is entering or leaving the view.
* `*` = any state
* `:enter` and `:leave` are aliases for the `void => *` and `* => void` transitions.

Let’s review the animations we wanted at the beginning of the article. `One => Two` and `Two => Three` should slide the previous view off to the left and bring the new view in from the right. Since they both have the same transition, both state changes can be defined in a single transition using comma separated values:

```
import { trigger, transition } from '@angular/animations';
export const routeTransitionAnimations = trigger('triggerName', [
 transition('One => Two, Two => Three', [])
]);
```

Now, for the actual transformation! First, notice what the [official Angular documentation](https://angular.io/guide/route-animations#styling-the-host-and-child-components) has to say:

> _During a transition, a new view is inserted directly after the old one and both elements appear on screen at the same time. To prevent this, apply additional styling to the host view, and to the removed and inserted child views. **The host view must use relative positioning, and the child views must use absolute positioning**. Adding styling to the views animates the containers in place, without the DOM moving things around._

Apply this to the style definition by adding the following:

```
import { trigger, transition, style, query } from '@angular/animations';
export const routeTransitionAnimations = trigger('triggerName', [
  transition('One => Two, Two => Three', [
    style({ position: 'relative' }),
    query(':enter, :leave', [
      style({
        position: 'absolute',
        top: 0,
        right: 0,
        width: '100%'
      })
    ])
  ])
]);
```

First, `style({ position: ‘relative’ })` sets the style on the element that is the target of the trigger to be `position: relative`. The target element is the one with the synthetic property `@triggerName`, which is the div that contains the `<router-outlet>`. Now, the “host view” is using relative positioning per the official docs.

Next, `query(':enter, :leave', [...])`. This means “query for child elements that are entering or leaving the view.” Then it applies the following style definition to those elements. I won’t dive too much into the CSS solution for the positions, but the real key is that we are setting the child elements to absolute positioning, per the official docs. Your CSS will almost certainly differ at this point based on your chosen animation style and application DOM layout.

Now, we need to define the individual transitions, in order. These will follow the first `query` in the `transition`'s array arguments.

This query defines what the start state is for the view that is entering, positioning it off screen to the far right:

```
query(':enter', [style({ right: '-100%', opacity: 0 })]),
```

The next query ensures that any child component animations that need to happen on the leaving component happen before the leaving view animates off screen:

```
query(':leave', animateChild()),
```

Next, we group the leave and enter together so that these transitions happen in unison (otherwise, the old would leave, leaving a blank space, and then the new would enter). We `animate`, meaning “transition existing styles to the specified styles over a period of time with an easing function.” The leaving view animates its `right` value to be 100% (the far left of the screen) and the entering animate’s its `right` value to be 0% (the far right of the screen):

```
group([
   query(':leave', [animate('1s ease-out', style({ right: '100%', opacity: 0 }))]),
   query(':enter', [animate('1s ease-out', style({ right: '0%', opacity: 1 }))])
  ]),
```

At this point, the old view has left, the new one has entered, and we want to trigger any child animations on the new view:

```
query(':enter', animateChild())
```

And here is what that looks like:

![Moving image of a mouse clicking buttons. There are three buttons at the top of the image labeled "One", "Two", and "Three". When the mouse clicks "One" a pink bar reading One slides onto the screen, when the mouse clicks "Two" a blue bar reading Two slides onto the screen, and when the mouse clicks "Three" a green bar reading Three slides onto the screen. All bars slide in from right to left.](https://miro.medium.com/max/700/1*3vfPkD8p2KUldeqPWXNWfQ.gif)

Now, add the transition for the reverse direction, `Three => Two`, and `Two => One`, after the first transition, and change the `right`’s to `left`'s:

```
transition('Three => Two, Two => One', [
  style({ position: 'relative' }),
  query(':enter, :leave', [
    style({
      position: 'absolute',
      top: 0,
      left: 0,
      width: '100%'
    })
  ]),
  query(':enter', [style({ left: '-100%', opacity: 0 })]),
  query(':leave', animateChild()),
  group([
    query(':leave', [animate('1s ease-out', style({ left: '100%', opacity: 0 }))]),
    query(':enter', [animate('1s ease-out', style({ left: '0%', opacity: 1 }))])
   ]),
   query(':enter', animateChild())
 ])
```

Here is the result:

![The same moving picture as above, only the direction the colored bars slide in from has changed. ](https://miro.medium.com/max/700/1*U94nNc5IdupWI_iyQLufSQ.gif)

Looking good! We’re just missing two transition definitions, `One => Three`, and `Three => One`. Rather than defining something different, we will add these to the existing ones. Add `One => Three` to the right definition, and the `Three => One` to the `left`. The transitions now look like this:

```
transition('One => Two, Two => Three, One => Three', [...]),
transition('Three => Two, Two => One, Three => One', [...])
```

And the results:

![Image for post](https://miro.medium.com/max/700/1*C_yFC9CUDxbbCR_heG_GpQ.gif)

Voila! Successful Angular route transition animations!

[Here is the whole trigger/transition definition](https://github.com/fivedice/ng-router-animation/blob/master/src/app/route-transition-animations.ts).

This just scratches the surface of what can be done with Angular animations. Check out my other article on [Animating Angular’s *ngIf and *ngFor](https://medium.com/ngconf/animating-angulars-ngif-and-ngfor-32a6ff26ed2d) to have more fun with Angular animations!

---

##ng-conf: Join us for the Reliable Web Summit

Come learn from community members and leaders the best ways to build reliable web applications, write quality code, choose scalable architectures, and create effective automated tests. Powered by ng-conf, join us for the Reliable Web Summit this August 26th & 27th, 2021.
https://reliablewebsummit.com/