A Deep Dive Into Angular Animations

Animations are getting more common in a lot of modern applications. So here, we’ll learn how to add animations to our Angular apps. By the end of this article, you’ll have a good understanding of,

  • the structure of the animations
  • ways of animating various elements in the DOM
  • how to create reusable animations
  • how to use the new animation functions in Angular 4.3
  • how to write clean and maintainable animation code

If you’re an absolute beginner with Angular, then you can start your journey from here.

Let’s get started.

Different Ways to Create the Animations

There are 2 primary ways to create the animation in the Web application. We can use CSS and Javascript to create the animations. In CSS, we have a couple of properties like transition and animation. And with these properties, we can animate DOM elements. Here is an example; let’s suppose we’ve defined the class in CSS.

  1. .stretch {  
  2.     animation-name: stretch;  
  3.     animation-duration: 1.5s;  
  4. }  

Now when we apply this class to a DOM element, the browser will animate that element. And if we have been working with CSS for a while, you might have heard about the library animate.css. This is the library that gives us a bunch of predefined classes for various kinds of animations. So, we don’t need to manually build the animations with CSS properties. Take a look at the GitHub page of this library. We integrate it in the project via npm. We need to import it in the styles.css just like how we imported bootstrap into our styles.css in our previous articles. And then, start working with animate.css classes.

So as you saw, we can certainly use CSS to add animations but this CSS animation gives us limited control. We often use this with simple, one-shot animations like showing a tooltip or toggling UI elements. If you want to build something more complex, then you really need to use JavaScript. In terms of implementation, there are various libraries out there that give you an API for implementing animations in JavaScript. For example,

  • jQuery
  • GSAP
  • Zepto
  • Web Animations API

The recommended approach is to use Web Animations API which is basically a specification of animating DOM elements and is currently supported natively with Chrome, Firefox, and Opera. So, if you head over to caniuse.com, here you can see what features are implemented in what browsers. Here, you can see Web Animations are supported in what browsers. Microsoft IE and Edge are behind in the game just like but don’t worry, because there are polyfills that you can install and then you can use Web Animation in these other browsers too. We get the HTML element with querySelector and apply the animate() method on that element.

So basically, there are 2 ways to make the animations with CSS and JavaScript. But where does Angular come into this picture? Well, Angular has a module called @angular/animations and this module is built on top of standard Web Animations API. So, instead of directly working with this API, we’re going to work with the abstractions provided by Angular. The benefit of this approach is that our code is going to be easier to unit test and also easier to port with different platforms. So when we code against different abstractions, then potentially we can take our code and run inside iOS or in Android environment and use animations natively in that environment. So we’re not tightly coupled with the implementation of Web Animations in browsers.

Angular Animations

Now let’s see how to build the animations in our angular apps. So as we know we have a module called @angular/animation. In this module, we have a bunch of helper functions for creating animations such as trigger(), transition(), state(), animate() and so on. Before we explore these functions in details, let’s take a look at the anatomy of an animation. Look at this illustration.

Angular Animation

What you see here is the concept behind every animation whether you’re a programmer or an animator, you’re dealing with the same concept. The concept is the DOM element can be in any given state (State1), in this state we have a certain look and feel perhaps its background is blue. Now during the animation, it transitions to a different state and in this state it has a different look. Perhaps its background is yellow. This is the anatomy of every animation. We have various states, in each state our element has a different look and during an animation it transitions to one state to another. Now what are these states? In Angular we have 3 kinds of states.

  • Void State
    It is not part of the DOM and this can happen when an element is created but now placed in the DOM yet or when it removes from the DOM.

    Angular Animation

    Here we have a real world example. Think of a Todo list, every time we add an item in the list the corresponding element is first created but is not in the DOM yet. So it is in the void state. And then it transition from this state to a new state. Similarly when we delete the item from the list, the corresponding element transitions from its current state into the void state. So here * represents to what we call the Default state.

    Angular Animation

So all elements do have a default state.

  • Default State
  • Custom State

We always use custom states because they only make sense in certain scenarios. For example, think of a zippy or an accordion component. This component is always visible in the view but it state can change from collapse to expanded. So if we want to implement an animation during this transition then will have to work with 2 custom states here (collapse and expanded)

Angular Animation 

Now let’s see the implementation, let’s say we want to apply the animation to each item in a list. We start with the component metadata, here we have a property called animation which takes an array. In this array, we register one or more triggers. Each trigger has a name and we add its implementation, we define all the states and transitions to that kind of animation. So this function you see here like trigger(), state() and transition() these are the helper functions that are defined in @angular/animations module.

  1. @Component({  
  2.   animations: [  
  3.     trigger('fadeIn', [  
  4.       state(...)  
  5.       transition(...)  
  6.     ])  
  7.   ]  
  8. })  

You will see this in action. So here we have a trigger called fadeIn. Now we can apply this trigger to any DOM element using this notation

  1. <div @fadeIn></div>  

So this is the big picture.

Importing the Animations Module and Polyfills

So we’re ready to implement the first animation. And if you want to code along with me I have attached the ‘animations-demo’ file with article. So run the command,

  • PS C:\Users\Ami Jan\animations-demo\animations-demo> npm install
  • PS C:\Users\Ami Jan\animations-demo\animations-demo> ng serve

So in this application we have simple todo list. We can add a new item and we can delete existing item by clicking it. Currently we don’t have any animation here. So

Step 1

Let’s import the animations module in the app module. So let’s go to the app.module.ts

  1. import { BrowserAnimationsModule } from '@angular/platform-browser/animations';  
  2. import { BrowserModule } from '@angular/platform-browser';  
  3.   
  4.   imports: [  
  5.     BrowserModule,  
  6.     BrowserAnimationsModule  
  7.   ]  

So this BrowserAnimationsModule includes all the code for running animations in a browser. As I told you before this BrowserAnimationsModule is implemented on top of the standard Web Animations API and this api is natively supported in firefox, chrome and opera. So to support the other browsers, we need to add polyfills.

Polyfills

It is basically the code that allows you to use modern javascript features in old browsers.

Step 2

So back in the src folder, we can see we have polyfills.ts. Open polyfills.ts, here on line 40 and 41 we have a commented line for importing Web Animations polyfill. Uncomment this line

  1. /** IE10 and IE11 requires the following to support `@angular/animation`. */  
  2. import 'web-animations-js';  // Run `npm install --save web-animations-js`.  

And now here we need to manually install this node module ‘web-animations-js’

PS C:\Users\Ami Jan\animations-demo\animations-demo> npm install --save web-animations-js

So this is how we add animations support to an existing application.

Implementing a Fade In Animation

Alright let’s go to the our todos.component.ts. So here we have a basic todos component. Now in the component metadata, we add a new property called ‘animations’ and we define the array and in this array we define one or more triggers.

  1. @Component({  
  2.   // tslint:disable-next-line:component-selector  
  3.   selector: 'todos',  
  4.   templateUrl: './todos.component.html',  
  5.   styleUrls: ['./todos.component.css'],  
  6.   animations: [  
  7.     trigger('fade', [  
  8.         
  9.     ])  
  10.   ]  
  11. })  

And here we give the name to the trigger() function and then as a second argument we need to pass an array. In this array, we register all the states and the transitions to implement this animation.

Before going any further, let’s go to template and apply this trigger. So, todos.component.html

  1. <div *ngIf="items" class="list-group" >  
  2.   <button type="button"  
  3.       @fade  
  4.       *ngFor="let item of items"  
  5.       (click)="removeItem(item)"  
  6.       class="list-group-item"  
  7.       >{{ item }}</button>  
  8. </div>   

This is where we render all the todo item. Each todo item is a button and here you can see we have applied ngFor directive. And this is where we apply the name of the trigger. And if you don’t do this, you’re not gonna see the animation. So back in the component, now here in trigger() function array we need to register all the states and the transitions of this animation. So we often have calls the 2 functions (state(), transition()) and both these functions are defined in @angular/animations

  1. @Component({  
  2.   // tslint:disable-next-line:component-selector  
  3.   selector: 'todos',  
  4.   templateUrl: './todos.component.html',  
  5.   styleUrls: ['./todos.component.css'],  
  6.   animations: [  
  7.     trigger('fade', [  
  8.       state(),  
  9.       transition()  
  10.     ])  
  11.   ]  
  12. })  

It is easy to start with transition() function and this function needs 2 arguments. First argument is the string and called statechange expression. With this expression we define source (target state)

  1. @Component({  
  2.   // tslint:disable-next-line:component-selector  
  3.   selector: 'todos',  
  4.   templateUrl: './todos.component.html',  
  5.   styleUrls: ['./todos.component.css'],  
  6.   animations: [  
  7.     trigger('fade', [  
  8.       transition('void => *')  
  9.     ])  
  10.   ]  
  11. })  

That means when our element goes from the void state to the default state then we define the array and in this array we define the steps that should run during this animation. So in order to implement fadeIn effect, we want to apply certain styles during transition when the element is treated but is not placed in the DOM yet. In this array we often have called the 2 functions (style() and animate()). Style() function contains an object containing the key-value pairs where keys are CSS properties. So if we want to change the background color of this element during this transition.

  1. import { Component } from '@angular/core';  
  2. import { trigger, transition, style, animate } from '@angular/animations';  
  3.   
  4. @Component({  
  5.   // tslint:disable-next-line:component-selector  
  6.   selector: 'todos',  
  7.   templateUrl: './todos.component.html',  
  8.   styleUrls: ['./todos.component.css'],  
  9.   animations: [  
  10.     trigger('fade', [  
  11.       transition('void => *', [  
  12.         style({ backgroundColor: 'yellow', opacity: 0 }),  
  13.         animate()  
  14.       ])  
  15.     ])  
  16.   ]  
  17. })  

So this style function will apply these styles here immediately during this transition. And one more thing, all these triggers and functions which we are using to apply the animation belong to @angular/animations. Before Angular 4, they belonged to @angular/core. So if auto-import plugin adds these functions into @angular/core module in header reference, make the correction your own. animate() function is also similar and it takes 2 arguments. First argument is the timing and the 2nd argument is the style object

  1. animations: [  
  2.   trigger('fade', [  
  3.     transition('void => *', [  
  4.       style({ backgroundColor: 'yellow', opacity: 0 }),  
  5.       animate(2000, style({ backgroundColor: 'white', opacity: 1 }))  
  6.     ])  
  7.   ])  
  8. ]  

So this animate function will apply these styles over this given period of time which is 2000ms and 2s. This is the only difference between style() and animate() function. The style() function applies the style immediately but the animate() function applies them over a period of time. So when you run this code, the background color of our element is going to be initially yellow and then over 2s it will gradually change to white. And opacity will be initially 0 and then it will change to 1. Now open the browser and watch the results, when you refresh the page. The items initially had the yellow background and then it will gradually changes to white background over 2s.

Now back to our component, and let’s make code little bit cleaner. So let’s say remove the 2nd argument of animate(), we don’t explicitly need to change the background color to white and opacity change to 1 because Angular is smart enough to know that in the target state which is the default(*) state

  1. transition('void => *',  

Our item should have the white background and the opacity should be 1. So we don’t need to explicitly specify them here. So,

  1. animations: [  
  2.   trigger('fade', [  
  3.     transition('void => *', [  
  4.       style({ backgroundColor: 'yellow', opacity: 0 }),  
  5.       animate(2000)  
  6.     ])  
  7.   ])  
  8. ]  

And now it is cleaner. So the lesson is, in a list of steps in a transition if you have a call to animate() with only the timing value without any styles then this animate() function will undo all the styles which we have applied

  1. style({ backgroundColor: 'yellow', opacity: 0 }),  

over the period of time.

For the fadeIn effect we don’t really need backgroundColor, I just really use this for demonstration. So here is how we implement the fadeIn effect when an element transitions from the void state to the default(*) state. Initially we want to set its opacity to 0 which will make this element invisible and then over the period of time, the opacity will be changes to 1 which will make the elements appear on the view.

  1. animations: [  
  2.   trigger('fade', [  
  3.     transition('void => *', [  
  4.       style({ opacity: 0 }),  
  5.       animate(2000)  
  6.     ])  
  7.   ])  
  8. ]  

Implementing a Fade-out Animation

So here in this page if we add the new item, it will appear with fadeIn effect however if we click on the item it will appear immediately, there is no animation. So let’s see how to implement fadeout effect. So let’s go back to the fade trigger property. Now we define another transition from default(*) to void state because when we click an item, it’s going to be remove from the DOM. And at this point, it will go from the default(*) state to the void state.

  1. import { Component } from '@angular/core';  
  2. import { trigger, transition, style, animate } from '@angular/animations';  
  3.   
  4. @Component({  
  5.   // tslint:disable-next-line:component-selector  
  6.   selector: 'todos',  
  7.   templateUrl: './todos.component.html',  
  8.   styleUrls: ['./todos.component.css'],  
  9.   animations: [  
  10.     trigger('fade', [  
  11.       transition('void => *', [  
  12.         style({ opacity: 0 }),  
  13.         animate(2000)  
  14.       ]),  
  15.       transition('* => void', [  
  16.         animate(2000, style({ opacity: 0 }))  
  17.       ])  
  18.     ])  
  19.   ]  
  20. })  
  21. export class TodosComponent {  
  22.   items: any[] = [  
  23.     'Wash the dishes',  
  24.     'Call the accountant',  
  25.     'Apply for a car insurance'];  
  26.   
  27.   addItem(input: HTMLInputElement) {  
  28.     this.items.splice(0, 0, input.value);  
  29.     input.value = '';  
  30.   }  
  31.   
  32.   removeItem(item) {  
  33.     const index = this.items.indexOf(item);  
  34.     this.items.splice(index, 1);  
  35.   }  
  36. }  

Now let’s test this in the browser.

Angular Animation 

States

So it was the implementation of our fadeIn and fadeout effect. In this implementation, we can see something is duplicating here.

  1. animations: [  
  2.   trigger('fade', [  
  3.     transition('void => *', [  
  4.       style({ opacity: 0 }),  
  5.       animate(2000)  
  6.     ]),  
  7.     transition('* => void', [  
  8.       animate(2000, style({ opacity: 0 }))  
  9.     ])  
  10.   ])  
  11. ]  

Duplicated stuff:

  1. style({ opacity: 0 })  

So how can we make this code cleaner?

If you pay closer attention, this is the style of void state. So when the element is out of the DOM, its opacity should be 0. So to cleanup this code we can define the style with the void state. So earlier we discussed that here in the trigger() we often have calls with the transition and/or state() function. So here we’ll define the state() function to define the state. And the state is void. The next parameter is,

Angular Animation 

The type of the 2nd parameter is AnimationStyleMetadata which might sound little bit confusing. And in fact, a lot of functions in the Animations Module  look something like this. So they always have Animations…..Metadata. Here is a tip for you whenever you see types like that drop the Animations and Metadata word (AnimationsStyleMetadata) and when you drop this then we have the word Style which means we call the style function to get a style object.

  1. import { Component } from '@angular/core';  
  2. import { trigger, transition, style, state, animate } from '@angular/animations';  
  3.   
  4. @Component({  
  5.   // tslint:disable-next-line:component-selector  
  6.   selector: 'todos',  
  7.   templateUrl: './todos.component.html',  
  8.   styleUrls: ['./todos.component.css'],  
  9.   animations: [  
  10.     trigger('fade', [  
  11.   
  12.       state('void', style({ opacity: 0 })),  
  13.   
  14.       transition('void => *', [  
  15.         animate(2000)  
  16.       ]),  
  17.   
  18.       transition('* => void', [  
  19.         animate(2000)  
  20.       ])  
  21.     ])  
  22.   ]  
  23. })  

So once again inside the trigger we often have calls to the state and/or transition function. Most of the time we use transition functions but depending upon what we’re implementing we may use the state function to make our code cleaner and more maintainable.

Transitions

We still have little bit of duplication in our code.

  1. transition('void => *', [  
  2.   animate(2000)  
  3. ]),  
  4.   
  5. transition('* => void', [  
  6.   animate(2000)  
  7. ])  

Look we have repeated animate(2000) in 2 places. So let’s see the cleaner way to implement the fadeIn and fadeout effect. In the transition function as you know, we can supply multiple state changes.

  1. animations: [  
  2.   trigger('fade', [  
  3.     state('void', style({ opacity: 0 })),  
  4.     transition('void => *, * => void', [  
  5.       animate(2000)  
  6.     ])  
  7.   ])  
  8. ]  

But we can make this even more clean. So instead of having 2 unidirectional state changes expressions, we can use Bi-directional state change expression

  1. transition('void <=> *', [  
  2.   animate(2000)  
  3. ])  

This is much better from where we started. So if we have multiple transitions and these transitions have the same implementation, we can refactor them into one transition with multiple state change expressions. Also here we have couple of aliases, but first let’s revert it back.

  1. transition('void => *, * => void', [  
  2.   animate(2000)  
  3. ])  

These two state expressions are very common. So in Angular we have the alias for these expressions. So when the element transitions from the void state to default state, our alias is :enter. And for the 2nd expression our alias is :leave. So when the element is leaving the DOM. So using these aliases is more cleaner and readable then bi-directional state change expressions

  1. transition(':enter, :leave', [  
  2.   animate(2000)  
  3. ])  

Creating Reusable Triggers

So we have defined fade trigger in our todos component and chances are somewhere else in the application we may want to apply ‘fade’ effect to another element. We don’t want to repeat all this code. So it is better to extract this code and put it in different module that we can reuse in multiple places. So we add a new file ‘animations.ts’ in src/app folder. And in this file, we’re going to define a reusable animations. If you a lot of animations instead of one file, you want to create a directory and in that directory you can manage different files for different kinds of animations. But here is just an example for demonstration.

Back in the todos component, here we have trigger() function which is actually defined in @angular/animations module. And if you look at the return type of trigger() function, it returns AnimationTriggerMetadata object. And we have already discussed the tip, whenever you see the type like this drop the words (Animation & Metadata). So this trigger() function returns a trigger object and all we have to do is to export the trigger object from our animations module. So we cut the animations code and paste it into the new file and export it. And now our actual code looks like,

  1. import { trigger, transition, style, state, animate } from '@angular/animations';  
  2. export let fade = trigger('fade', [  
  3.   
  4.     state('void', style({ opacity: 0 })),  
  5.   
  6.     transition(':enter, :leave', [  
  7.       animate(2000)  
  8.     ])  
  9. ]);  

Now we can reuse this fade trigger into multiple places. So back to our component and here in the animations array, we’ll add fade trigger.

  1. import { fade } from './../animations';  
  2. import { Component } from '@angular/core';  
  3.   
  4. @Component({  
  5.   // tslint:disable-next-line:component-selector  
  6.   selector: 'todos',  
  7.   templateUrl: './todos.component.html',  
  8.   styleUrls: ['./todos.component.css'],  
  9.   animations: [  
  10.     fade  
  11.   ]  
  12. })  

This is how we can reuse our animations.

Now before going further, let’s build one more animation from what we have learned so far. So let’s add one more animation ‘slide’ in our ‘animations.ts’

  1. import { trigger, transition, style, state, animate } from '@angular/animations';  
  2.   
  3. export let slide = trigger('slide', [  
  4.     transition(':enter', [  
  5.         style({ transform: 'translateX(-10px)' }),  
  6.         animate(500)  
  7.     ]),  
  8.   
  9.     transition(':leave', [  
  10.         animate(500, style({ transform: 'translateX(-100%)' }))  
  11.     ])  
  12. ])  
  13.   
  14. export let fade = trigger('fade', [  
  15.   
  16.     state('void', style({ opacity: 0 })),  
  17.   
  18.     transition(':enter, :leave', [  
  19.       animate(2000)  
  20.     ])  
  21. ]);  

Now consume slide animation into our component,

  1. import { slide } from './../animations';  
  2.   
  3. @Component({  
  4.   // tslint:disable-next-line:component-selector  
  5.   selector: 'todos',  
  6.   templateUrl: './todos.component.html',  
  7.   styleUrls: ['./todos.component.css'],  
  8.   animations: [  
  9.     slide  
  10.   ]  
  11. })  

And one last thing to work this new animation.

  1. <div *ngIf="items" class="list-group" >  
  2.   <button type="button"  
  3.       @slide  
  4.       *ngFor="let item of items"  
  5.       (click)="removeItem(item)"  
  6.       class="list-group-item"  
  7.       >{{ item }}</button>  
  8. </div>   

Add this otherwise you’ll see a lot of errors in the browser console. Now let’s see the animation in action.

Angular Animation 

Easings

Note that when we click on the item, it slides to the left with the constant speed. But in real life Objects don’t move with constant speed i.e. if we drop the ball from the top of the building, the speed of this ball increases gradually or when you’re driving your car and push the break the speed of your car decreases gradually. So in real life objects don’t move at a constant speed. Now let’s see how to make this animation more interesting. So back to our slide animation and let’s take a look at :leave transition. So the first argument to this animate function we passed numbers for timing the animation

  1. transition(':leave', [  
  2.     animate(500, style({ transform: 'translateX(-100%)' }))  
  3. ])  

If you want more control over this timing, we should see the string

  1. animate('500', style({ ... }))  

This string has 3 components. First component is the duration which is required. Second component is the optional which is delay. And finally the 3rd component of the string is Easing.

Easing is basically a function that determines the speed of an animation over time. In CSS, we have few standard easings.

Angular Animation 

So these are the standard easings defined in CSS. If you want to implement a custom easing, you need to use Cubic Bezier function. So this is basically a function that determines the shape of the curve, we pass four numbers and these numbers determine the shape of easing curve.

Angular Animation 

There is a tool that helps you to design the curve and then you’ll get the cubic Bezier function. Here we have GUI to design the curve and numbers dynamically changes in the function. Here we also can preview the movement of the animation and compare it with other available animation categories as well. So we can get the idea of how our animation will work on the webpage.

Angular Animation 

So here is the programmatic tip for you. Use the Ease-in function to move things out of the screen.

Angular Animation 

Now back to our animate function. So as we’re moving the component out of the screen so we’ll use ease-in.

  1. animate('0.5s 1s ease-in', style({ transform: 'translateX(-100%)' }))  

And we don’t really need the delay so remove it,

  1. transition(':leave', [  
  2.     animate('0.5s ease-in', style({ transform: 'translateX(-100%)' }))  
  3. ])  

Now back in the browser and test the animation.

Now let’s see how to use cubic-bezier function. So copy the values from cubic-bezier.com, we have the function containing values and we can use this function where we add ease-in

  1. transition(':leave', [  
  2.     animate('0.5s cubic-bezier(0,.7,1,.63)', style({ transform: 'translateX(-100%)' }))  
  3. ])  

And now see what happens in the browser. Just feel the movement of the objects.

Keyframes

So we’ve implemented slide animation. But still we can do something better. So earlier we discussed animate.css library.

Angular Animation 

Here in this dropdownlist we have all kinds of interesting effects. We’ll work with bounceOutLeft effect.

Now let’s see how we implement this in Angular. 1 way is to define this transition and play with the animate and style functions but it is very time consuming and hard. Because you’ve to work with all kinds of styles and transformation until you get the desired effect. But these days, there a lot of examples out there we can reuse their code instead of building everything from scratch. So let’s go to the github page of animate.css and go to the source directory and so on. Here we can see the implementation of bounceOutLeft effect in CSS. Here we’re using @keyframes feature of CSS. With Keyframe, we can specify multiple frames for an application and in each frame we can apply the different style. Here is the example,

Angular Animation 

In this animation, we have 2 keyframes. The first keyframe is 20% from the beginning and to is the 2nd keyframe which is the end of the animation. More complex animations have more complex keyframes and on each keyframe we have obviously different style. Now what we need to do is to get these keyframes and apply them in an animation.

  1. transition(':leave', [  
  2.     animate('0.5s ease-out', keyframes([  
  3.         style({})  
  4.     ]))  
  5. ])  

keyframes() are defined in @angular/animations. It contains an array where we can multiple calls to the style() but here we have an additional property with each style offset. And if you move back to the github page 20% on the first keyframe is actually the offset of our style here.

  1. transition(':leave', [  
  2.     animate('0.5s ease-out', keyframes([  
  3.         style({ offset: .2 })  
  4.     ]))  
  5. ])  

Now on the github page, we have 2 styles on first keyframe. Now we’ll apply them here.

  1. transition(':leave', [  
  2.     animate('0.5s ease-out', keyframes([  
  3.         style({   
  4.             offset: .2,  
  5.             opacity: 1,  
  6.             transform: translate3d(20px, 0, 0)  
  7.         })  
  8.     ]))  
  9. ])  

Now instead of using translate3d, make it simpler

  1. transition(':leave', [  
  2.     animate('0.5s ease-out', keyframes([  
  3.         style({   
  4.             offset: .2,  
  5.             opacity: 1,  
  6.             transform: 'translateX(20px)'  
  7.         })  
  8.     ]))  
  9. ])  

So it is our first keyframe

  1. transition(':leave', [  
  2.     animate('0.5s ease-out', keyframes([  
  3.         style({   
  4.             offset: .2,  
  5.             opacity: 1,  
  6.             transform: 'translateX(20px)'  
  7.         }),  
  8.         style({   
  9.             offset: 1,  
  10.             opacity: 0,  
  11.             transform: 'translateX(-100%)'  
  12.         })  
  13.     ]))  
  14. ])  

So in the 2nd keyframe we’re moving outside of the screen. So basically what we have here is the combination of fadeout and slideout with the bouncing effect. Now test the result,

Angular Animation 

So this is how we use keyframes to build more complex animations and define various styles in each keyframe. And each keyframe has an offset property that determines the relative position of the keyframe on the beginning of the animation.

Creating Reusable Animations with animation()

Let’s review what we have done. When this main page loads our items slides left side of the screen. And when we click on an item, the item slides to the left of the screen. But if we want more customized animation i.e. when the page loads instead of slideIn effect, we want fadeIn effect and when we click on the item, its background turns into red and then we see the slideOut effect. So let’s see how we can implement them.

Back to our animations.ts, we defined the reusable trigger called slide and look at the implementation of the :leave transition

  1. animate('0.5s ease-out', keyframes([  
  2.     style({   
  3.         offset: .2,  
  4.         opacity: 1,  
  5.         transform: 'translateX(20px)'  
  6.     }),  
  7.     style({   
  8.         offset: 1,  
  9.         opacity: 0,  
  10.         transform: 'translateX(-100%)'  
  11.     })  
  12. ]))  

This is our slideout effect with some kind of bouncing. Now here I don’t want to apply red background because it only make sense in the context of the todo items. I want this to be reusable to use it in multiple places. So now we need custom triggers todo items. So let’s go to our template and change the animation.

  1. <div *ngIf="items" class="list-group" >  
  2.   <button type="button"  
  3.       @todoAnimation  
  4.       *ngFor="let item of items"  
  5.       (click)="removeItem(item)"  
  6.       class="list-group-item"  
  7.       >{{ item }}</button>  
  8. </div>   

Now in the todos.component.ts

  1. import { trigger, transition, style, state, animate, keyframes } from '@angular/animations';  
  2. import { slide, fade } from './../animations';  
  3. import { Component } from '@angular/core';  
  4.   
  5. @Component({  
  6.   // tslint:disable-next-line:component-selector  
  7.   selector: 'todos',  
  8.   templateUrl: './todos.component.html',  
  9.   styleUrls: ['./todos.component.css'],  
  10.   animations: [  
  11.     trigger('todoAnimation', [  
  12.       transition(':enter', [  
  13.         // Need fadeIn animation here.  
  14.           
  15.       ]),  
  16.       transition(':leave', [])  
  17.     ])  
  18.   ]  
  19. })  

Now we need fadeIn animation here in but we already define this in animations.ts. So it is the problem that we already defined it and export it. We have entire trigger

  1. export let fade = trigger('fade', [  
  2.   
  3.     state('void', style({ opacity: 0 })),  
  4.   
  5.     transition(':enter, :leave', [  
  6.       animate(2000)  
  7.     ])  
  8. ]);  

And we can’t use a trigger inside a transition. So temporarily I’m going to duplicate the code in a slightly different way in the component. And after we’ll refactor this.

  1. animations: [  
  2.   trigger('todoAnimation', [  
  3.     transition(':enter', [  
  4.       // Need fadeIn animation here.  
  5.       style({ opacity: 0}),  
  6.       animate(2000)  
  7.     ]),  
  8.     transition(':leave', [])  
  9.   ])  
  10. ]  

Now what about the leave transition. Open the animations.ts. Here we have developed the leave transition and here we have implemented the bounceOutLeft animation and it is really very complicated having couple of keyframes with lots of details. So I don’t want to duplicate this and use so many places. So let’s see how to extract this code, put it somewhere else and then reuse it in multiple places. So first off cut the entire animate() function from leave transition and make another export variable and apply all the stuff into another animation() function

  1. import { trigger, transition, style, state, animate, keyframes, animation, useAnimation } from '@angular/animations';  
  2.   
  3. export let bounceOutLeftAnimation = animation(  
  4.     animate('0.5s ease-out', keyframes([  
  5.         style({   
  6.             offset: .2,  
  7.             opacity: 1,  
  8.             transform: 'translateX(20px)'  
  9.         }),  
  10.         style({   
  11.             offset: 1,  
  12.             opacity: 0,  
  13.             transform: 'translateX(-100%)'  
  14.         })  
  15.     ]))  
  16. );  
  17.   
  18. export let slide = trigger('slide', [  
  19.   
  20.     transition(':enter', [  
  21.         style({ transform: 'translateX(-10px)' }),  
  22.         animate(500)  
  23.     ]),  
  24.   
  25.     transition(':leave', [  
  26.         useAnimation(bounceOutLeftAnimation)  
  27.     ])  
  28. ]);  

animation() function is used to create the Reusable function. Now back to our leave transition trigger. We use helper function useAnimation() and use this export variable into it.

Now to make this code even more clean, we can get rid of this square bracket

  1. transition(':leave', [  
  2.     useAnimation(bounceOutLeftAnimation)  
  3. ])  

Because we use this square bracket to define all the steps during a transition. But here we have only 1 step which is using predefined animation

  1. transition(':leave',   
  2.     useAnimation(bounceOutLeftAnimation)  
  3. )  

So now we have reusable animation.

Now come back to the component, apply this new reusable animation in the metadata.

  1. @Component({  
  2.   // tslint:disable-next-line:component-selector  
  3.   selector: 'todos',  
  4.   templateUrl: './todos.component.html',  
  5.   styleUrls: ['./todos.component.css'],  
  6.   animations: [  
  7.     trigger('todoAnimation', [  
  8.       transition(':enter', [  
  9.         // Need fadeIn animation here.  
  10.         style({ opacity: 0}),  
  11.         animate(2000)  
  12.       ]),  
  13.       transition(':leave', [  
  14.         useAnimation(bounceOutLeftAnimation)  
  15.       ])  
  16.     ])  
  17.   ]  
  18. })  

And also here we can apply that red background. So when the element is about to leave the screen, we’ll change its background immediately. So,

  1. animations: [  
  2.   trigger('todoAnimation', [  
  3.     transition(':enter', [  
  4.       // Need fadeIn animation here.  
  5.       style({ opacity: 0}),  
  6.       animate(2000)  
  7.     ]),  
  8.     transition(':leave', [  
  9.       style({ backgroundColor: 'red' }),  
  10.       animate(1000),  
  11.       useAnimation(bounceOutLeftAnimation)  
  12.     ])  
  13.   ])  
  14. ]  

Now let’s take a look at the result.

Angular Animation 

Here is the lesson, if you have complex animation with multiple steps and you want to reuse this in multiple places in your application

  1. export let bounceOutLeftAnimation = animation(  
  2.     animate('0.5s ease-out', keyframes([  
  3.         style({   
  4.             offset: .2,  
  5.             opacity: 1,  
  6.             transform: 'translateX(20px)'  
  7.         }),  
  8.         style({   
  9.             offset: 1,  
  10.             opacity: 0,  
  11.             transform: 'translateX(-100%)'  
  12.         })  
  13.     ]))  
  14. );  

We can call the animation() function to create reusable animation. And then we can reuse it by calling useAnimation()

  1. transition(':leave',   
  2.     useAnimation(bounceOutLeftAnimation)  
  3. )  

Parameterizing Reusable Animations

So in our custom todoAnimation, we have duplicated the implementation of the fadeIn effect.

  1. @Component({  
  2.   // tslint:disable-next-line:component-selector  
  3.   selector: 'todos',  
  4.   templateUrl: './todos.component.html',  
  5.   styleUrls: ['./todos.component.css'],  
  6.   animations: [  
  7.     trigger('todoAnimation', [  
  8.       transition(':enter', [  
  9.         style({ opacity: 0}),  
  10.         animate(2000)  
  11.       ]),  
  12.       transition(':leave', [  
  13.         style({ backgroundColor: 'red' }),  
  14.         animate(1000),  
  15.         useAnimation(bounceOutLeftAnimation)  
  16.       ])  
  17.     ])  
  18.   ]  
  19. })  

Let’s go ahead and refactor this. Back to our animations.ts, here we have fade trigger

  1. export let fade = trigger('fade', [  
  2.   
  3.     state('void', style({ opacity: 0 })),  
  4.   
  5.     transition(':enter, :leave', [  
  6.       animate(2000)  
  7.     ])  
  8. ]);  

Now the problem we have implementation is that transition only includes animate(2000). And opacity style is outside to this transition, we have defined it as part of the ‘void’ state. The problem we have here is that we can only reuse the content of the transition, in other words we create a reusable animation using animation() function what we pass inside is only the content of the transition function, we can’t pass the state here. So we need to change the implementation and make opacity as part of transition so that we can create reusable animation. So first of all let’s separate the transitions and refactor the code.

  1. export let fade = trigger('fade', [  
  2.   
  3.     transition(':enter', [  
  4.         style({ opacity: 0 }),  
  5.         animate(2000)  
  6.     ]),  
  7.   
  8.     transition(':leave', [  
  9.         animate(2000, style({ opacity: 0 }))  
  10.     ])  
  11. ]);  

So with this we can create the reusable animation. So let’s make it reusable.

  1. export let fadeInAnimation = animation([  
  2.     style({ opacity: 0 }),  
  3.     animate(2000)  
  4. ]);  
  5.   
  6. export let fadeOutAnimation = animation([  
  7.     animate(2000, style({ opacity: 0 }))  
  8. ]);  
  9.   
  10. export let fade = trigger('fade', [  
  11.   
  12.     transition(':enter',  
  13.         useAnimation(fadeInAnimation)  
  14.     ),  
  15.   
  16.     transition(':leave', [  
  17.         useAnimation(fadeOutAnimation)  
  18.     ])  
  19. ]);  

Look at the naming convention, we can easily identify what is the reusable animation code and what is animation trigger. Now let’s take this to the next level,

  1. export let fadeInAnimation = animation([  
  2.     style({ opacity: 0 }),  
  3.     animate(2000)  
  4. ]);  

In this implementation we have hard coded the duration in the animate() function. What if someone else in the application  wants a different duration or perhaps a different easing function. Well the good thing is we can add parameters to these reusable animation dynamically.

  1. export let fadeInAnimation = animation([  
  2.     style({ opacity: 0 }),  
  3.     animate('{{ duration }} {{ easing }}')  
  4. ]);  

So here duration and easing are the parameters which come dynamically. Now I want to give these parameters default values, in case the client of this code doesn’t provide the values. So as the 2nd argument of this animation function we can provide AnimationOptions object. Let’s have a quick look at the documentation of animation function. Look this function takes 2 parameters, the first is steps which can be either an instance of AnimationMetadata or AnimationMetadata array (if we have multiple steps).

Angular Animation 

As I’ve already told you before whenever you see AnimatinMetadata drop these words. So what can be passed here, in theory we can pass the return value of any of functions define with animations module. But this is actually a design problem in the animations module. If you’re more experienced and you’re familiar with solid principals of Object Oriented Programming, here we have the concept called ‘Liskov’s Substitution Principle’. So here is the real world example of the violation of this principal. So the Angular team have defined this hierarchy where animation metadata is an interface on the top and below this we have bunch of different interfaces that extend this interface.

Angular Animation 

So in principal if a function takes AnimationMetadata, we should be able to get any of its child interfaces as well. But here in our case,

Angular Animation 

We can’t pass certain types to the animation function, we’ll get the runtime error. And this is the example of bad hierarchy design. So back to the signature of the animation(), look at the second parameter which is ‘AnimationOptions’ now let’s take a look at the documentation of this interface. So this interface defines the type with these 2 fields.

Angular Animation 

Both these fields are optional. We use the params field to supply any parameters to our reusable animations. So back in the animations.ts and in the animation function

  1. export let fadeInAnimation = animation([  
  2.     style({ opacity: 0 }),  
  3.     animate('{{ duration }} {{ easing }}')  
  4. ], {  
  5.     params: {  
  6.         // Here we have key-value pairs to supply default parameters  
  7.         duration: '2s',  
  8.         easing: 'ease-out'  
  9.     }  
  10. });  

Now the consumer of this fadeInAnimation can overwrite these values. So let’s go back to our todos component and reuse this animation

  1. import { trigger, transition, style, state, animate, keyframes, useAnimation } from '@angular/animations';  
  2. import { slide, fade, bounceOutLeftAnimation, fadeInAnimation } from './../animations';  
  3. import { Component } from '@angular/core';  
  4.   
  5. @Component({  
  6.   // tslint:disable-next-line:component-selector  
  7.   selector: 'todos',  
  8.   templateUrl: './todos.component.html',  
  9.   styleUrls: ['./todos.component.css'],  
  10.   animations: [  
  11.     trigger('todoAnimation', [  
  12.       transition(':enter', [  
  13.         useAnimation(fadeInAnimation, {  
  14.           params: {  
  15.             duration: '500ms'  
  16.           }  
  17.         })  
  18.       ]),  
  19.       transition(':leave', [  
  20.         style({ backgroundColor: 'red' }),  
  21.         animate(1000),  
  22.         useAnimation(bounceOutLeftAnimation)  
  23.       ])  
  24.     ])  
  25.   ]  
  26. })  

Now let’s take a look at the implementation in the browser. Refresh the page; we have quick fadeIn effect.

Animation Callbacks

So now we have apply the animations on the html elements. We have the couple of callback methods that we can use to see when an animation starts and when it is done. So here back in the template,

  1. <div *ngIf="items" class="list-group" >  
  2.   <button type="button"  
  3.       @todoAnimation  
  4.       *ngFor="let item of items"  
  5.       (click)="removeItem(item)"  
  6.       class="list-group-item"  
  7.       >{{ item }}</button>  
  8. </div>   

This is where we have applied the todoAnimation. We can use the event binding syntax to handle the done and start events of this animation. So,

  1. <div *ngIf="items" class="list-group" >  
  2.   <button type="button"  
  3.       @todoAnimation  
  4.       (@todoAnimation.start)="animationStarted($event)"  
  5.       (@todoAnimation.done)="animationDone($event)"  
  6.       *ngFor="let item of items"  
  7.       (click)="removeItem(item)"  
  8.       class="list-group-item"  
  9.       >{{ item }}</button>  
  10. </div>   

Now let’s go ahead and implement these methods. So back in the component for simplicity let’s define these events.

  1. export class TodosComponent {  
  2.   items: any[] = [  
  3.     'Wash the dishes',  
  4.     'Call the accountant',  
  5.     'Apply for a car insurance'];  
  6.   
  7.   addItem(input: HTMLInputElement) {  
  8.     this.items.splice(0, 0, input.value);  
  9.     input.value = '';  
  10.   }  
  11.   
  12.   removeItem(item) {  
  13.     const index = this.items.indexOf(item);  
  14.     this.items.splice(index, 1);  
  15.   }  
  16.   
  17.   animationStarted($event) { console.log($event); }  
  18.   animationDone($event) { console.log($event); }  
  19. }  

Now run the application and refresh the page. Look into the console, here we get 6 messages,

Angular Animation 

The first 3 messages phaseName comes from the animation started method and the last 3 come from the animation done method. So because we have 3 todo items and for each todo item we have animation start and animation done message. Now let’s explore one of these in more detail.

Angular Animation 

This event object has a bunch of useful properties. Here we have element which gives us access to the underlying element on which we have apply the animation. We have fromState and toState, so we can see our element transition from the void state to null which is the default state because we have not defined custom state here. And here the phaseName property can be start or done, start for the beginning of the animation and done is for at the end of the animation. We got totalTime which is 500ms and trigger name. It’s the custom trigger that we have applied on our animation. So in your animation you might be working on something more complex then if you know when the animation starts and finishes, you can handle the start and done event of this animation trigger.

Querying Child Elements With query()

Upto this point when we refresh the page our todo items fadeIn. Now we also want to animate the Todos heading. So let’s see how can we implement this. Back to our template, just like we apply the todoAnimation trigger on the button we can create another trigger to apply on the heading. But that approach is little bit messy because for the trigger we need to define the transition as well.

  1. trigger('todoAnimation', [  
  2.   transition(':enter', [  

Also chances are the more complex markup, we’re gonna have different kinds of elements here may be a paragraph or an image. So we don’t want to create a separate trigger for each of these. If all these elements are logically part of one thing which is basically one component, it would be better to create a trigger and apply that trigger to the component as a whole. And then we can animate its children independently. So let us see how to implement this.

Let’s put all the markup inside the parent div.

  1. <div @todosAnimation>  
  2.   <h1>Todos</h1>  
  3.   
  4.   <input #itemInput  
  5.     class="form-control"  
  6.     (keyup.enter)="addItem(itemInput)">  
  7.   
  8.   <div *ngIf="items" class="list-group" >  
  9.     <button type="button"  
  10.         @todoAnimation  
  11.         (@todoAnimation.start)="animationStarted($event)"  
  12.         (@todoAnimation.done)="animationDone($event)"  
  13.         *ngFor="let item of items"  
  14.         (click)="removeItem(item)"  
  15.         class="list-group-item"  
  16.         >{{ item }}</button>  
  17.   </div>   
  18. </div>  

Now let’s go back to our component and here in the animations property, we’ll define a new trigger

  1. @Component({  
  2.   // tslint:disable-next-line:component-selector  
  3.   selector: 'todos',  
  4.   templateUrl: './todos.component.html',  
  5.   styleUrls: ['./todos.component.css'],  
  6.   animations: [  
  7.     trigger('todosAnimation', [  
  8.       transition(':enter', [  
  9.           
  10.       ])  
  11.     ]),  
  12.   
  13.     trigger('todoAnimation', [  
  14.       transition(':enter', [  
  15.         useAnimation(fadeInAnimation, {  
  16.           params: {  
  17.             duration: '500ms'  
  18.           }  
  19.         })  
  20.       ]),  
  21.       transition(':leave', [  
  22.         style({ backgroundColor: 'red' }),  
  23.         animate(1000),  
  24.         useAnimation(bounceOutLeftAnimation)  
  25.       ])  
  26.     ])  
  27.   ]  
  28. })  

Now I want to animate the heading as well as the todo items separately. So how do we get the heading? For that we need to use one of the helper functions called query(). It is also the part of @angular/animations. And if you see the intellisense what parameters it can take then you’ll get an idea the first parameter is the string which is actually the selector. And here we use the CSS selector like we can select the elements by className (.something) by their id (#something) and by element itself (element). But we also have a bunch of pseudo-selectors tokens as well. So, we can use

  • query(‘:enter’), query(‘:leave’)
    When the child element enters or leaves this container.

  • query(‘:animating’)
    When the child container is animating.

  • query(‘@trigger’)
    We can also query elements by animation trigger we have applied on them.

  • query(‘@*’)
    If you want to get all the elements that have an animation trigger using asterisk (*)

  • query(‘:self’)
    if you reference the same element which is the container, we can use self token.

We don’t any need to memorize them because we always come back to the documentation. And even we don’t need to use all of them when we’re working with animations. So it was just an overview.

Now come back to the point here we have applied h1 tag for query.

Angular Animation 

Look the 2nd argument which is the AnimationMetadata or AnimationMetadata array. By dropping these words, in theory we can pass anything here, but in practical terms that’s not gonna work. Because here is the design problem in the animations module in Angular. But that aside what we pass here as the second argument is the steps that should run when animating h1. So it can be one step or can be multiple steps. So here I will  go with multiple steps.

  1. @Component({  
  2.   // tslint:disable-next-line:component-selector  
  3.   selector: 'todos',  
  4.   templateUrl: './todos.component.html',  
  5.   styleUrls: ['./todos.component.css'],  
  6.   animations: [  
  7.     trigger('todosAnimation', [  
  8.       transition(':enter', [  
  9.         query('h1', [  
  10.           style({ transform: 'translateY(-20px)' }),  
  11.           animate(2000)  
  12.         ])  
  13.       ])  
  14.     ]),  
  15.   
  16.     trigger('todoAnimation', [  
  17.       transition(':enter', [  
  18.         useAnimation(fadeInAnimation, {  
  19.           params: {  
  20.             duration: '500ms'  
  21.           }  
  22.         })  
  23.       ]),  
  24.       transition(':leave', [  
  25.         style({ backgroundColor: 'red' }),  
  26.         animate(1000),  
  27.         useAnimation(bounceOutLeftAnimation)  
  28.       ])  
  29.     ])  
  30.   ]  
  31. })  

Now let’s see the application back in the browser. And now our heading is sliding from the top. However our todo items  no longer have animations but here we expect to see fadeIn animation. But when we insert a new item, the animation is working.

Angular Animation 

Animating Child Elements With animateChild()

Animation is not applying on the list-group because of this hierarchy.

Angular Animation 

So in this hierarchy we have 2 animation triggers, todosAnimation is the parent trigger and todoAnimation is the child trigger. Now back to our component, look -- both these triggers handle the enter transition,

Angular Animation 

In this situation when the enter transition happens, the parent trigger will always get priority and the children will be blocked. In simple words todoAnimation trigger code is not executed at all. So how can we fix this problem? So in parent trigger, just like how we query the heading we need to query our todo items and allow their animations to run. So,

  1. animations: [  
  2.   trigger('todosAnimation', [  
  3.     transition(':enter', [  
  4.       query('h1', [  
  5.         style({ transform: 'translateY(-20px)' }),  
  6.         animate(1000)  
  7.       ]),  
  8.       query('@todoAnimation', [  
  9.           
  10.       ])  
  11.     ])  
  12.   ]),  
  13.   
  14.   trigger('todoAnimation', [  
  15.     transition(':enter', [  
  16.       useAnimation(fadeInAnimation, {  
  17.         params: {  
  18.           duration: '500ms'  
  19.         }  
  20.       })  
  21.     ]),  

Now we have already implemented what should happen when todo items enter in the view, we have applied fadeInAnimation here. And we don’t want to repeat it in the above trigger. So we simply tell the angular to run this trigger animation inside parent trigger animation.

  1. trigger('todosAnimation', [  
  2.   transition(':enter', [  
  3.     query('h1', [  
  4.       style({ transform: 'translateY(-20px)' }),  
  5.       animate(1000)  
  6.     ]),  
  7.     query('@todoAnimation', animateChild())  
  8.   ])  
  9. ]),  
  10.   
  11. trigger('todoAnimation', [  
  12.   transition(':enter', [  
  13.     useAnimation(fadeInAnimation, {  
  14.       params: {  
  15.         duration: '500ms'  
  16.       }  
  17.     })  
  18.   ]),  

So by using animateChild() we’re telling Angular not to block child animation. Now save the file and see the animation in action. Everything is working perfectly.

Angular Animation 

Look animations are working in sequence. So first the heading is animated and when it is done then the todo items appear.

  1. trigger('todosAnimation', [  
  2.   transition(':enter', [  
  3.     query('h1', [  
  4.       style({ transform: 'translateY(-20px)' }),  
  5.       animate(1000)  
  6.     ]),  
  7.     query('@todoAnimation', animateChild())  
  8.   ])  
  9. ]),  

Here we have multiple queries, each query has the different kind of animation and this animation runs sequentially one after another.

Running Parallel Animations with group()

Let’s see how to run the animations in parallel so here we’ll use the helper function called group() so we’ll put these above queries inside group() then the animations associated with these queries will run in parallel

  1. trigger('todosAnimation', [  
  2.   transition(':enter', [  
  3.     group([  
  4.       query('h1', [  
  5.         style({ transform: 'translateY(-20px)' }),  
  6.         animate(1000)  
  7.       ]),  
  8.       query('@todoAnimation', animateChild())  
  9.     ])  
  10.   ])  
  11. ]),  

Now if you see the end result, our heading and todo items are animating at the same time.

Now let’s see the simplified version of group(), we don’t need to use query() inside the group()

  1. trigger('todosAnimation', [  
  2.   transition(':enter', [  
  3.     group([  
  4.       animate(1000, style({ background: 'red' })),  
  5.       animate(2000, style({ transform: 'translateY(50px)' }))  
  6.     ])  
  7.   ])  
  8. ])  

So with this group() we can run these 2 animations in parallel.

Staggering Animations With stagger()

So upto this point all the todo items appear together. It would be nicer if these items appear one after another, creating some kind of certain effect. So let’s see how to implement this. So back to our component

  1. transition(':enter', [  
  2.   group([  
  3.     query('h1', [  
  4.       style({ transform: 'translateY(-20px)' }),  
  5.       animate(1000)  
  6.     ]),  
  7.     query('@todoAnimation', animateChild())  
  8.   ])  
  9. ])  

This is where we’re querying our todo items.

  1. query('@todoAnimation', animateChild())  

We can wrap this call to the animateChild() function inside another function called stagger()

  1. transition(':enter', [  
  2.   group([  
  3.     query('h1', [  
  4.       style({ transform: 'translateY(-20px)' }),  
  5.       animate(1000)  
  6.     ]),  
  7.     query('@todoAnimation',  
  8.       stagger(2000, animateChild()))  
  9.   ])  
  10. ])  

So with this code, we’re going to have 2s delay which will animating each todo item. Here 2s is the huge wait just for the demonstration. Now if you run the application, you’ll see how the items render one after another.

200ms to 500ms is the sweet spot for any animation. This kind of effect you want to apply if you have the page like photo gallery. It would be nicer if you render the photos in a staggering effect.

As the 2nd argument of stagger() we don’t necessarily call the animateChild() because we have defined the animation of this child element somewhere else inside todoAnimation trigger. Now suppose you don’t have this second trigger todoAnimation and you have implemented everything as part of the 1st trigger then in that case the 2nd argument of the stagger() function will include the steps or animation. So let’s see how that works

  1. transition(':enter', [  
  2.   group([  
  3.     query('h1', [  
  4.       style({ transform: 'translateY(-20px)' }),  
  5.       animate(1000)  
  6.     ]),  
  7.     query('@todoAnimation',  
  8.       stagger(2000, useAnimation(fadeInAnimation)))  
  9.   ])  
  10. ])  

Now optionally we could overwrite the default parameters here as well. And if you don’t have any reusable animation like we have here then you can also provide the steps here as well.

  1. transition(':enter', [  
  2.   group([  
  3.     query('h1', [  
  4.       style({ transform: 'translateY(-20px)' }),  
  5.       animate(1000)  
  6.     ]),  
  7.     query('@todoAnimation',  
  8.       stagger(2000, [  
  9.         style({ transform: 'translateX(-20px)' }),  
  10.         animate(1000)  
  11.       ])  
  12.     )  
  13.   ])  
  14. ])  

Now let’s see the certain effect. So remove the trigger selector and change it with class.

  1. @Component({  
  2.   // tslint:disable-next-line:component-selector  
  3.   selector: 'todos',  
  4.   templateUrl: './todos.component.html',  
  5.   styleUrls: ['./todos.component.css'],  
  6.   animations: [  
  7.     trigger('todosAnimation', [  
  8.       transition(':enter', [  
  9.         group([  
  10.           query('h1', [  
  11.             style({ transform: 'translateY(-20px)' }),  
  12.             animate(1000)  
  13.           ]),  
  14.           query('.list-group-item',  
  15.             stagger(200, [  
  16.               style({ opacity: 0, transform: 'translateX(-20px)' }),  
  17.               animate(1000)  
  18.             ])  
  19.           )  
  20.         ])  
  21.       ])  
  22.     ])  
  23.   ]  
  24. })  

And our template html code is,

  1. <div *ngIf="items" class="list-group" >  
  2.   <button type="button"  
  3.       *ngFor="let item of items"  
  4.       (click)="removeItem(item)"  
  5.       class="list-group-item"  
  6.       >{{ item }}</button>  
  7. </div>   

Now let’s see this in action.

Angular Animation 

This is kind a nicer animation, it doesn’t have flickering effect. However look here we have a problem when we’re adding a new item there is no animation. So with this implementation our todo items are animated only the first time load in the page because we query them with class name when todosAnimation container (div) enter in the view. And in this case the container enters only once in the view that’s why our todo items are animated only once.

So in a page like this we really want to have another trigger. Now revert back the changes. Our component code is,

  1. @Component({  
  2.   // tslint:disable-next-line:component-selector  
  3.   selector: 'todos',  
  4.   templateUrl: './todos.component.html',  
  5.   styleUrls: ['./todos.component.css'],  
  6.   animations: [  
  7.     trigger('todosAnimation', [  
  8.       transition(':enter', [  
  9.         group([  
  10.           query('h1', [  
  11.             style({ transform: 'translateY(-20px)' }),  
  12.             animate(1000)  
  13.           ]),  
  14.           query('@todoAnimation',  
  15.             stagger(200, animateChild()))  
  16.         ])  
  17.       ])  
  18.     ]),  
  19.   
  20.     trigger('todoAnimation', [  
  21.       transition(':enter', [  
  22.         useAnimation(fadeInAnimation, {  
  23.           params: {  
  24.             duration: '500ms'  
  25.           }  
  26.         })  
  27.       ]),  
  28.       transition(':leave', [  
  29.         style({ backgroundColor: 'red' }),  
  30.         animate(1000),  
  31.         useAnimation(bounceOutLeftAnimation)  
  32.       ])  
  33.     ])  
  34.   ]  
  35. })  

And our html template code is,

  1. <div @todosAnimation>  
  2.   <h1>Todos</h1>  
  3.   
  4.   <input #itemInput  
  5.     class="form-control"  
  6.     (keyup.enter)="addItem(itemInput)">  
  7.   
  8.   <div *ngIf="items" class="list-group" >  
  9.     <button type="button"  
  10.         @todoAnimation  
  11.         (@todoAnimation.start)="animationStarted($event)"  
  12.         (@todoAnimation.done)="animationDone($event)"  
  13.         *ngFor="let item of items"  
  14.         (click)="removeItem(item)"  
  15.         class="list-group-item"  
  16.         >{{ item }}</button>  
  17.   </div>   
  18. </div>  

The stagger() only designed to work inside the query()

Working With Custom States

Now let’s take a look at a different example. Open app.component.html and comment out the todos element and uncomment the zippy code. And then open the browser. And you’ll see it is a very simple accordion code. Now let’s add some nice animation here. So let’s go to zippy.component.html and here we have a div with class ‘zippy-body’ where we rendered the body of zippy. And now at this element we want to apply animation trigger. So,

  1. <div   
  2.   @expandCollapse  
  3.   class="zippy-body" [hidden]="!isExpanded">  
  4.   <ng-content></ng-content>  
  5. </div>  

Now let’s go to zippy.component.ts and implement this trigger

  1. import { stagger, group, animateChild, trigger, transition, style,  
  2.   state, animate, keyframes, useAnimation, query } from '@angular/animations';  
  3. import { Component, Input } from '@angular/core';  
  4.   
  5. @Component({  
  6.   // tslint:disable-next-line:component-selector  
  7.   selector: 'zippy',  
  8.   templateUrl: './zippy.component.html',  
  9.   styleUrls: ['./zippy.component.css'],  
  10.   animations: [  
  11.     trigger('expandCollapse', [  
  12.         
  13.     ])  
  14.   ]  
  15. })  

Now here we need to add our transitions. This example is little bit different from our todos component. Back to our template here we have used the hidden property to hide the body of zippy, I’m not using ngIf here. In simple words this div doesn’t transition between the void and default state, it always there but of course more accurately the first time the page is loaded it transitions from the void state to the default(*) state. When we click on the zippy to expand and collapse it, this element is no longer transition between void to the default state. So here we need to deal with custom states.

So back in the trigger, here we’ll define the custom state.

  1. @Component({  
  2.   // tslint:disable-next-line:component-selector  
  3.   selector: 'zippy',  
  4.   templateUrl: './zippy.component.html',  
  5.   styleUrls: ['./zippy.component.css'],  
  6.   animations: [  
  7.     trigger('expandCollapse', [  
  8.       state('collapsed', style({  
  9.         height: 0,  
  10.         overflow: 'hidden'      // This will hide the children of the div  
  11.       })),  
  12.   
  13.       state('expanded', style({  
  14.         height: '*',          // Height depends upon the content. So Angular computes it dynamically  
  15.         overflow: 'auto'  
  16.       })),  
  17.   
  18.       // finally we need to add transition  
  19.       transition('collapsed => expanded', [  
  20.         // Don't want any initially animation at collapsed state. So,  
  21.         animate('300ms ease-out')  
  22.       ])  
  23.     ])  
  24.   ]  
  25. })  

Now back to our template. Here we need to set the value of this trigger using Property binding syntax. So,

  1. <div   
  2.   [@expandCollapse]="isExpanded ? 'expanded' : 'collapsed'"  
  3.   class="zippy-body" [hidden]="!isExpanded">  
  4.   <ng-content></ng-content>  
  5. </div>  

Initially when the page loads the value of this trigger or the state is collapsed and when we click on it, the state changes to expanded. At this point

  1. transition('collapsed => expanded', [  

this transition runs. If you see the results in browser, it expands smoothly however when we collapse the zippy we have no animations. So let’s go ahead and add new transition

  1. transition('collapsed => expanded', [  
  2.   animate('300ms ease-out')  
  3. ]),  
  4.   
  5. transition('expanded => collapsed', [  
  6.   animate('300ms ease-in')  
  7. ])  

Now back again to the browser and see what happens,

Angular Animation 

So back in our template. Here we have applied the hidden property in this div. And when isExpanded is false this property will immediately hide the element. So even though we have defined the transition from expanded to collapsed because the element becomes hidden immediately we don’t see the transition. So remove the hidden attribute, we don’t really need this

  1. <div   
  2.   [@expandCollapse]="isExpanded ? 'expanded' : 'collapsed'"  
  3.   class="zippy-body">  
  4.   <ng-content></ng-content>  
  5. </div>  

Now back in the browser again,

Angular Animation 
This is we get. It looks like our zippy is in the collapse state. So let’s investigate it in more details. Back to our component
  1. state('collapsed', style({  
  2.   height: 0,  
  3.   backgroundColor: 'yellow',  
  4.   overflow: 'hidden'      // This will hide the children of the div  
  5. })),  

And now it is 100% sure that it is in the collapsed state.

Angular Animation 

So we set the height to 0 but the height is not exactly the 0 here because if you open the zippy.component.css here we have applied the padding 20px applied to this zippy-body element. So

  1. state('collapsed', style({  
  2.   height: 0,  
  3.   padding: 0,  
  4.   backgroundColor: 'yellow',  
  5.   overflow: 'hidden'      
  6. })),  
  7.   
  8. state('expanded', style({  
  9.   height: '*',            
  10.   padding: '*',  
  11.   overflow: 'auto'  
  12. })),  

Now if we test the application in the browser,

Angular Animation 

Out text looks like coming from the top-left corner of the body of zippy. And it is because here in the collapsed state we set the padding to 0 that applies to all directions. Because initially the left padding is 0 as the element transitions to the expanded state, left padding also increases. That’s why we have weird motion. So,

  1. animations: [  
  2.   trigger('expandCollapse', [  
  3.     state('collapsed', style({  
  4.       height: 0,  
  5.       paddingTop: 0,  
  6.       paddingBottom: 0,  
  7.       overflow: 'hidden'      // This will hide the children of the div  
  8.     })),  
  9.   
  10.     state('expanded', style({  
  11.       height: '*',          // Height depends upon the content. So Angular computes it dynamically  
  12.       padding: '*',  
  13.       overflow: 'auto'  
  14.     })),  
  15.   
  16.     // finally we need to add transition  
  17.     transition('collapsed => expanded', [  
  18.       // Don't want any initially animation at collapsed state. So,  
  19.       animate('300ms ease-out')  
  20.     ]),  
  21.   
  22.     transition('expanded => collapsed', [  
  23.       animate('300ms ease-in')  
  24.     ])  
  25.   ])  
  26. ]  

Now let’s make our code cleaner. So look in the transition, here when we call the animate() will only the timing value it is going to undo all the previous styles applied. So when we’re going to the expanded state from collapsed state, all the styles applied in the collapsed state will be undone.

  1. state('expanded', style({  
  2.   height: '*',          // Height depends upon the content. So Angular computes it dynamically  
  3.   padding: '*',  
  4.   overflow: 'auto'  
  5. })),  

We don’t really need these styles explicitly. This is exactly for the same reason that when we implemented the fadeIn effect we give an opacity to 1. Now remove this expanded state code and test it one more time. And everything is working fine in the browser.

  1. @Component({  
  2.   selector: 'zippy',  
  3.   templateUrl: './zippy.component.html',  
  4.   styleUrls: ['./zippy.component.css'],  
  5.   animations: [  
  6.     trigger('expandCollapse', [  
  7.       state('collapsed', style({  
  8.         height: 0,  
  9.         paddingTop: 0,  
  10.         paddingBottom: 0,  
  11.         overflow: 'hidden'      
  12.       })),  
  13.   
  14.       transition('collapsed => expanded', [  
  15.         animate('300ms ease-out')  
  16.       ]),  
  17.   
  18.       transition('expanded => collapsed', [  
  19.         animate('300ms ease-in')  
  20.       ])  
  21.     ])  
  22.   ]  
  23. })  

So the lesson is if you want to animate an element that is always on the view chances are you may need to define custom states. If that’s the case you use property binding syntax to set the value of this animation trigger.

  1. [@expandCollapse]="isExpanded ? 'expanded' : 'collapsed'"  

Multi-step Animations

Now let’s take this animation to the next level. So when we expand the zippy it appears immediately and looks like the text is appearing at the same time as it is expanded. So we want to expand the zippy first and then we want the text to appear with the fadeIn effect. Let’s see how can we implement this, so as we discussed when we call the animate() function it is going to undo all the previous styles like here we have in collapsed.

  1. animations: [  
  2.   trigger('expandCollapse', [  
  3.     state('collapsed', style({  
  4.       height: 0,  
  5.       paddingTop: 0,  
  6.       paddingBottom: 0,  
  7.       overflow: 'hidden'        
  8.     })),  
  9.   
  10.     transition('collapsed => expanded', [  
  11.       animate('300ms ease-out')  
  12.     ]),  

Now what we need to implement here is a multi-step animation. So first we want the zippy to expand and then the content in the body fadeIn. So when the zippy in the collapsed state

  1. animations: [  
  2.   trigger('expandCollapse', [  
  3.     state('collapsed', style({  
  4.       height: 0,  
  5.       paddingTop: 0,  
  6.       paddingBottom: 0,  
  7.       opacity: 0  
  8.     })),  
  9.   
  10.     transition('collapsed => expanded', [  
  11.       animate('300ms ease-out', style({  
  12.         height: '*',  
  13.         paddingTop: '*',  
  14.         paddingBottom: '*'  
  15.       })),  
  16.       animate('1s', style({ opacity: 1 }))  
  17.     ]),  
  18.   
  19.     transition('expanded => collapsed', [  
  20.       animate('300ms ease-in')  
  21.     ])  
  22.   ])  
  23. ]  

Let’s look at the result.

Angular Animation 

It looks awesome. Content is faded In after zippy.

Separation of Concerns (SOC)

We have built the animation on zippy. But we have a problem in this implementation. The problem is the violation of the principal of Separation of Concern. Because we have dedicated a large amount of code in the component file of animations which is distracting us from the actual components implementation. So in the component file, our focus should be on the component rather than animation. So to make this code clean, we’ll extract all the animations code and put it in the separate file. So add a new file in zippy component folder ‘zippy.component.animations.ts’

And now, cut and paste the zippy animations code in the new file and export it to make it accessible in the component file.

  1. import { stagger, group, animateChild, trigger, transition, style,  
  2.     state, animate, keyframes, useAnimation, query } from '@angular/animations';  
  3.   
  4. export const expandCollapse = trigger('expandCollapse', [  
  5.     state('collapsed', style({  
  6.       height: 0,  
  7.       paddingTop: 0,  
  8.       paddingBottom: 0,  
  9.       opacity: 0  
  10.     })),  
  11.   
  12.     transition('collapsed => expanded', [  
  13.       animate('300ms ease-out', style({  
  14.         height: '*',  
  15.         paddingTop: '*',  
  16.         paddingBottom: '*'  
  17.       })),  
  18.       animate('1s', style({ opacity: 1 }))  
  19.     ]),  
  20.   
  21.     transition('expanded => collapsed', [  
  22.       animate('300ms ease-in')  
  23.     ])  
  24.   ]);  

And now, back to the component file and import it in the animations array.

  1. import { Component, Input } from '@angular/core';  
  2. import { expandCollapse } from './zippy.component.animations';  
  3.   
  4. @Component({  
  5.   // tslint:disable-next-line:component-selector  
  6.   selector: 'zippy',  
  7.   templateUrl: './zippy.component.html',  
  8.   styleUrls: ['./zippy.component.css'],  
  9.   animations: [  
  10.     expandCollapse  
  11.   ]  
  12. })  

Look, now the component is cleaner.


Similar Articles