Animating Angular Routes

Creating the First Screen with Angular Material

Derived from photo by Danijel-James Wynyard / flickr.com, CC BY

This is one of a series of posts detailing the process of building an application with Angular, Visual Studio, ASP.NET Core, and Azure. See the complete listing of posts here.

Summary of items learned and decisions made for this implementation:

  • The animate.css library contains quality animations for route transitions
  • The animate.css library is extensible enough to handle customizations, mainly duration
  • This solution encapsulates the animations in a container component which houses the rest of the page's UI using the ng-content element
  • The container component is referenced as a @ViewChild component
  • Animations are applied by calling the container component's animate() method returning a Promise that resolves when the animation is complete

Upon starting on the second screen for project bebop, it was apparent that the basic routing wasn't going to cut it. The page transition was abrupt and I started working on applying animations to ease the transition. As usual, I started with the attitude to just 'make it work' and quickly saw that I was writing duplicate code in each component to accomplish these transitions. At which point, I started on a re-usable component to apply animations when navigating.

This post outlines the start of that work so that you can leverage similar concepts in your application and keep your code DRY. The solution uses animate.css and standard Angular components to achieve the result.

Page animation

Approach

The idea is to create a re-usable piece of UI, a component, that serves as the container for each page and is responsible for applying the animations related to page transitions. The component class is named AnimatedPageComponent with the selector app-animated-page. It uses the ng-content element to render the page contents. This is the required markup:

<app-animated-page #page>
    <!-- rest of the UI -->
</app-animated-page>

The #page identifer is explained shortly. The main take away is that the markup is very minimal.

Applying animate.css

Don't worry if you don't have any animation skills. The animate.css library has a full set of quality animations. To use it, simply apply two CSS classes. These are the basic steps to add animate.css to an Angular CLI application and animate an element:

  1. Install via npm
    npm install animate.css --save
    
  2. Add to the \src\styles.css file
    @import "~animate.css/animate.css";
    
  3. Apply two classes to the target element: animate and a class for the selected animation

    <div class="animate slideInUp"></div>
    

    That's it. There were, however, a couple scenarios requiring additional CSS. First, the animations caused the page heights to shrink affecting absolute positioning.

    Button positioning incorrect

    To fix this, position the container element to take the full height and width of the container, which in this case is the body element.

.app-page {
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  bottom: 0;
}

In addition, some of the animations cause content to appear in an overflow position displaying unnecessary scrollbars.

Scrollbars appear for animations

To correct this, set oveflow: hidden on the parent element, which in this case is the body element. This application already has a rule for the body tag in \src\styles.css so that is where I added it.

body {
  margin: 0;
  padding: 0;
  overflow: hidden;
}

Finally, some of the animations were just too slow. The animate.css GitHub page explains how to adjust the animation speed. Don't forget to apply any relevant vendor-prefix versions of the rule.

.fadeIn {
  animation-duration: .7s;
  /* vendor prefixes */
}

.fadeOut, .slideInUp, .slideOutDown {
  animation-duration: .3s;
  /* vendor prefixes */
}

AnimatedPageComponent Usage

Before diving into the AnimatedPageComponent implementation, I first thought about how it would be consumed by other components. You already saw the minimal markup.

Next, I needed to tie into different events of the consuming component to trigger the animation. I thought of ways to potentially pass values through @Input bindings but this information is not state and didn't make sense to pass it this way. This is an action that needs to take place on a specific event and then has an ending.

If you remember, there was a #page identifier added to the markup.

<app-animated-page #page>
    <!-- rest of the UI -->
</app-animated-page>

This identifier makes it easy to take a reference to this component in the consuming component's class using the @ViewChild decorator.

@ViewChild('page') page: AnimatedPageComponent;

The consuming component calls the animate() method with the specified animation. To reduce magic strings, I used the built-in TypeScript string enum support.

ngOnInit() {
    this.page.animate(PageAnimation.SlideInUp);
}

Finally, I need a way to perform an action once an animation has completed. For instance, when navigating away from a page, I need to apply the animation and then navigate to the new page once the animation is complete. Again, I considered using bindings for this to propagate an event but I had no way to know which event was triggered for which animation without a bunch of state management. In addition, I didn't foresee that anything else would need to respond to the animationend event, so I used a simple Promise to resolve when the animation is complete. Now, the navigation scenario works.

onSaveClick() {
    this.page.animate(PageAnimation.SlideOutDown).then(() => {
        this.router.navigateByUrl('routines');
    });
}

This creates a one-to-one relationship between the request to animate and the notification that the animation has completed.

AnimatedPageComponent Implementation

The implementation is straightforward but there are some design details worth further explanation. To simplify things, the AnimatedPageComponent handles only one animation at a time. Subsequent calls to animate are ignored until the first animation has completed. The Promise object and it's resolve callback are maintained as state while the animation is in progress. Finally, this implementation is only handling the standard animationend event to respond to the end of an animation but other browsers may need a prefixed version. See the animate.css GitHub page for more details

Here is the code for AnimatedPageComponent. To see the component in an application context, see this branch on GitHub.

animated-page.component.ts

import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';

export enum PageAnimation {
  None = '',
  FadeIn = 'fadeIn',
  FadeOut = 'fadeOut',
  SlideInUp = 'slideInUp',
  SlideOutDown = 'slideOutDown',
}

@Component({
  selector: 'app-animated-page',
  templateUrl: 'animated-page.component.html',
  styleUrls: ['animated-page.component.css']
})
export class AnimatedPageComponent {
  animationClass: PageAnimation = PageAnimation.None;
  animationInProgress: Promise<void> = null;
  animationResolver: () => void = null;

  constructor() { }

  onAnimationEnd() {
    this.animationClass = PageAnimation.None;
    this.animationResolver();

    this.animationInProgress = null;
    this.animationResolver = null;
  }

  animate(animation: PageAnimation) {
    if (this.animationInProgress) {
      return this.animationInProgress;
    }

    this.animationClass = animation;
    this.animationInProgress = new Promise((resolve, reject) => {
      this.animationResolver = resolve;
    });

    return this.animationInProgress;
  }
}

animated-page.component.html

<section class="app-page animated"
    [ngClass]="[animationClass]"
    (animationend)="onAnimationEnd()">
  <ng-content></ng-content>
</section>

animated-page.component.css

.app-page {
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  bottom: 0;
}

.fadeIn {
  animation-duration: .7s;
  /* vendor prefixes */
}

.fadeOut, .slideInUp, .slideOutDown {
  animation-duration: .3s;
  /* vendor prefixes */
}

Page animation

Next Steps

While this animation component is helpful, there are more ways to enhance this. Thinking about the design, one logical next step is to create a navigation service that is aware of these high-level page components. At that point, each component can make one API call to apply the leave transition, enter transition, and navigation. Furthermore, the app-animated-page element could be moved outside of the router-outlet eliminating the need to define this component in every page.

For now, the hope is that this example helps you think about these opportunities to keep your code DRY while improving the user experience.

What ways have you applied page transitions in your applications? Sound off in the comments.