Animating components with the AnimationBuilder

If you ever decide to investigate how the CSS class hooks triggered by the ng-animate class binding work under the hood, you will be positively surprised by the fact that all the heavy lifting is carried out by an instance of the CssAnimationBuilder class instantiated through the AnimationBuilder API.

The AnimationBuilder class (which is an injectable type and therefore subject to be imported through the constructor of our components) is a factory type whose API provides access to instantiate more specialized animation builders such as the CssAnimationBuilder class. This type has a very broad and powerful API whose methods allow us to add or remove CSS class names in order to trigger transitions or animations, or even configure by hand animation parameters such as styles, duration, or delay on our DOM elements of choice. In this sense, we can define general purpose animation handlers and then use them as animation adapters for any DOM element. In that sense, animation handlers created by the CssAnimationBuilder are agnostic of the DOM elements. Therefore, a single animation adapter can be applied to one or many HTML elements.

So, in order to create our own animations programmatically using only JavaScript, we just need an instance of the AnimationBuilder type, which we will use to instantiate a CssAnimationBuilder for creating a specific (HTML node-agnostic) animated transition. Last but not least, an accessor type to the DOM elements we want to animate. Sounds daunting? A quick and easy example will clarify all this.

A good way to put all these to the test is to introduce a new visual effect on our Pomodoro timer: a rendering animation effect when the component is loaded, so every time we activate the PomodoroTimer route, the component gets loaded gracefully by fading in on screen.

Let's begin by importing new tokens in the block of import statements of TimerWidgetComponent. We will need to fetch ElementRef from angular/core, since it will give us access to the DOM element we want to animate. Then we will have to bring the AnimationBuilder symbol, which will be used to instantiate a CssAnimationBuilder object. The latter is also imported so we can properly annotate our class members. The code is as follows:

app/timer/timer-widget.component.ts

import { Component, OnInit, ElementRef } from '@angular/core';
import { RouteParams, CanReuse, OnReuse } from '@angular/router-deprecated';
import { SettingsService, TaskService } from '../shared/shared';
import { AnimationBuilder } from '@angular/platform-browser/src/animate/animation_builder';
import { CssAnimationBuilder } from '@angular/platform-browser/src/animate/css_animation_builder';
...

Note

Please note the source locations for AnimationBuilder and CssAnimationBuilder. At the time of writing, the @angular/animate barrel has not been registered in any specific bundle within the Angular 2 framework, so we need to use the full path for importing each symbol. This may change in the future so please refer to the official Angular 2 documentation in case you get a 404 error when importing the types.

With all these tools in place, we can begin setting up the foundation for our animation. First let's create a new member in our component controller class under the name of fadeInAnimationBuilder. This new class property will represent the CssAnimationBuilder instance object that will define the animated transition we are about to build now. First, let's inject the dependencies we need through the class constructor, properly prefixed with access modifiers so they become class members instantly. The code is as follows:

app/timer/timer-widget.component.ts

...
export default class TimerWidgetComponent implements OnInit, CanReuse, OnReuse {
  minutes: number;
  seconds: number;
  isPaused: boolean;
  buttonLabelKey: string;
  buttonLabelsMap: any;
  taskName: string;
  fadeInAnimationBuilder: CssAnimationBuilder;

  constructor(
    private settingsService: SettingsService,
    private routeParams: RouteParams,
    private taskService: TaskService,
    private animationBuilder: AnimationBuilder,
    private elementRef: ElementRef) {
      this.buttonLabelsMap = settingsService.labelsMap.timer;

      this.fadeInAnimationBuilder = animationBuilder.css();
      this.fadeInAnimationBuilder.setDuration(1000)
        .setDelay(300)
        .setFromStyles({ opacity: 0 })
        .setToStyles({ opacity: 1 });
  }
  // Rest of the class remains the same
}

The AnimationBuilder.css() method is used in the constructor implementation to instantiate a CssAnimationBuilder object, and it is directly assigned to the fadeInAnimationBuilder member. Once assigned, we can use the CssAnimationBuilder API to define an animation that will kick off after 300 milliseconds, after which will endure along 1000 milliseconds (that is, a second) a style transition on any given DOM element from full transparency to solid color state. It is worth remarking that the CssAnimationBuilder features a chainable API, so we can conveniently chain settings one after another.

But as we pointed out in the beginning of this section, this animation handler is completely agnostic of the elements it can be applied on. Let's see how we can make all this transition happen on an actual DOM element. To do so, we can trigger the animation using the start() method exposed by the CssAnimationBuilder. This method expects an HTML element in its signature, on which the configured animations will be applied. Go to the ngOnInit() hook and add the following block of code at the end:

app/timer/timer-widget.component.ts

...
ngOnInit(): void {
  this.resetPomodoro();
  setInterval(() => this.tick(), 1000);

  let taskIndex = parseInt(this.routeParams.get('id'));
  if (!isNaN(taskIndex)) {
    this.taskName = this.taskService.taskStore[taskIndex].name;
  }

  this.fadeInAnimationBuilder.start(

    this.elementRef.nativeElement.firstElementChild);
}
...

As we mentioned, the start() method will expect an HTML element on which to apply the animation setup and that we do by feeding the function with the first child node of the nativeElement property of the elementRef class member. The ElementRef type we imported in the constructor gives us a reference pointer to the component directive itself (this is the PomodoroTimer component) in the context of the parent view or template it currently exists. Its nativeElement property gives us access to the underlying native element of the component, which is usually the template HTML nodes tree. From that point onward, we just need to fetch its firstElementChild property value, which will point to the root node of the component template, and apply the animation tweening on it. It is important to remark that the component will not react to any animation applied directly to it. Therefore, we need to traverse the nativeElement property after the actual DOM element we want to animate. We can introduce other DOM selectors here, leveraging the web element API methods such as document.getElementById() or document.querySelector(), but this is discouraged since it creates a tight coupling between the controller and the rendering layers and can compromise future maintainability of our components.

Now, save all your work and re-run the Pomodoro application, accessing the Pomodoro timer. Voila! Our beloved timer now gracefully shows up on screen with a smooth transition.

The CssAnimationBuilder API

We have just seen how we can create agnostic animation handlers with the CssAnimationBuilder, and to do so we have leveraged some powerful methods of its API (such as setDelay, setDuration, setFromStyles, or setToStyles). This is just a subset of all methods available in its API, which encompass some more methods that are really useful for building complex animations. These methods, including the signatures, are as follows:

  • setDelay(delay: number): As we saw in our example, this sets the animation delay and overrides any other animation delay previously defined through CSS.
  • setDuration(duration: number): This sets the animation duration and, similar to setDelay(), overrides any animation duration previously defined through CSS.
  • setFromStyles(from: {[key: string]: any}): As we saw in our example, it sets the initial styles for the animation, in the form of a hash object of key/value pairs. Be careful when styling CSS properties named with camel case. All CSS property names must be converted to camel case. In that sense, properties such as margin-top or background-color would turn into marginTop or backgroundColor.
  • setToStyles(to: {[key: string]: any}): This sets the destination styles for the animation.
  • setStyles(from: {[key: string]: any}, to: {[key: string]: any}): This is syntactic sugar to directly access the functionality provided by setFromStyles and setToStyles in a single method, which obviously sets styles for both the initial state and the destination state.
  • addClass(className: string): This adds a class that will remain on the element after the animation has finished. This method is especially useful for overriding CSS properties on DOM elements already managed by CSS transitions.
  • removeClass(className: string): As the counterpart of the previous method, this removes a class from the element.
  • addAnimationClass(className: string): This adds a temporary class that will be removed at the end of the animation. Angular 2 leverages this method under the covers for handling the CSS hooks triggered by the ng-animate class binding we overviewed at the beginning of this chapter.
  • start(element: HTMLElement): This starts the animation on the HTML element defined in the payload when executing the method and returns an Animation object. This Animation object exposes very useful methods we can leverage to implement additional functionalities as callbacks to be executed when the animation is complete, among other functionalities.

All these chainable methods allow us to build really complex animation handlers and reuse them throughout our applications with no effort.

Tracking animation state with the Animation class

Our applications' interactivity does not end in the moment an animation completes its interpolation. In fact, this can become the starting point of many other animations or interactive events occurring in our user interface. For that reason, it is important for our applications to be able to detect when an animation completes its interpolation.

Fortunately, we have the Animation class for this, and the CssAnimationBuilder.start() method precisely returns an instance of this type, as we can see in the following example:

app/timer/timer-widget.component.ts

...
ngOnInit(): void {
  this.resetPomodoro();
  setInterval(() => this.tick(), 1000);

  let taskIndex = parseInt(this.routeParams.get('id'));
  if (!isNaN(taskIndex)) {
    this.taskName = this.taskService.taskStore[taskIndex].name;
  }

  const animation = this.fadeInAnimationBuilder.start(
    this.elementRef.nativeElement.firstElementChild);

  animation.onComplete(() => console.log('Animation completed!'));
}

The Animation class exposes in its API the onComplete event handler, which is fired as soon as the animation triggered by the CssAnimationBuilder.start() method finishes. So, we can leverage it to trigger any other action in our app, such as logging operations (as depicted in the preceding example) or further animations.

Regarding the Animation class, it is in fact the one that carries out all the hard work of managing the transition. In that sense, the CssAnimationBuilder is just a facade providing a friendly interface for setting up the animation flow. When it comes then to performing further operations once the animation is ongoing or has just finished, the Animation class is our only resource.

All in all, we will rarely interact with the Animation class beyond using its callback functions or leveraging its built-in methods to handle CSS classes or swapping styles when the animation is over. On the other hand, it is not an Injectable class so we cannot instantiate it using Angular's dependency injection system. For these reasons, we will not cover its API in detail here.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset