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'; ...
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.
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.
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.