DEV Community

loading...
Cover image for Animating Angular Route Transitions

Animating Angular Route Transitions

ng-conf
Home to the largest gathering of Angular developers world wide!
・7 min read

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.

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 {}
Enter fullscreen mode Exit fullscreen mode

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'
 }
];
Enter fullscreen mode Exit fullscreen mode

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'];
 }
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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', []);
Enter fullscreen mode Exit fullscreen mode

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 {...}
Enter fullscreen mode Exit fullscreen mode

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', [])
]);
Enter fullscreen mode Exit fullscreen mode

Now, for the actual transformation! First, notice what the official Angular documentation 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%'
      })
    ])
  ])
]);
Enter fullscreen mode Exit fullscreen mode

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 })]),
Enter fullscreen mode Exit fullscreen mode

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()),
Enter fullscreen mode Exit fullscreen mode

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 }))])
  ]),
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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.

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())
 ])
Enter fullscreen mode Exit fullscreen mode

Here is the result:

The same moving picture as above, only the direction the colored bars slide in from has changed.

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', [...])
Enter fullscreen mode Exit fullscreen mode

And the results:

Image for post

Voila! Successful Angular route transition animations!

Here is the whole trigger/transition definition.

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 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/

Discussion (0)