The Angular Animations API provides a declarative API to build and reuse animations throughout our components. In this post, we will learn how to make simple transition animation and then build on what we learned to make a complex animation for a text and image-based lists using *ngFor
.
First, let’s start with a creating a basic fade animation when something is show or hidden from view. Feel free to have a click around the below StackBlitz example to see what you’re about to learn!
Table of contents
Using a toggle with *ngIf
we show and hide a text block based on a boolean condition in our component.
@Component({
selector: 'my-app',
template: `
<button (click)="show = !show">toggle</button>
<h1 *ngIf="show">Angular Animations</h1>
`
})
export class AppComponent {
show = true;
}
With *ngIf
the element will be removed entirely from the DOM when the show
property is false and added back when true. The *ngIf
will run when our animation completes.
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
Adding an Animation
To add a simple fade animation to our toggle example, we need to import the Angular Animations module into our application.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppComponent } from './app.component';
@NgModule({
imports: [BrowserModule, BrowserAnimationsModule],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule { }
Now that we have the BrowserAnimationsModule
imported, we can start creating our animation. To create our first animation, we will add a new property to our component decorator, animations
.
@Component({
selector: 'my-app',
template: `
<button (click)="show = !show">toggle</button>
<h1 *ngIf="show">Angular Animations</h1>
`,
animations: []
})
export class AppComponent {
show = true;
}
The animations
property can take one to many animations we would like to use in our component template. We define our animation in a variable so we can use it in multiple places in our application.
import { trigger, transition, style, animate, query } from '@angular/animations';
export const fadeAnimation = trigger('fadeAnimation', [
transition(':enter', [
style({ opacity: 0 }), animate('300ms', style({ opacity: 1 }))]
),
transition(':leave',
[style({ opacity: 1 }), animate('300ms', style({ opacity: 0 }))]
)
]);
Let’s walk through this animation step by step. First is the trigger
function. The trigger
takes two parameters, first the name of the animation we refer to in our template. The second parameter we provide the animation metadata or definition describing how the animation should behave.
In the animation, we can define a transition and how that transition is triggered. In this example, our transition
function first parameter is :enter
. The first parameter describes that our transition will run whenever something enters the DOM (example using *ngIf
).
In our transition, we can write styles and animations that we want to be applied when the transition occurs. In our :enter
state we set the element to have an opacity of 0
and then use the animate
function to animate it to an opacity of 1
over 300ms
.
Our second transition :leave
is similar to the first but inverted in behavior. When the element leaves or is removed from the DOM, we animate it back to 0
opacity over 300ms
. To use this animation, we add it to the animations
list in the component decorator and add a reference in the template.
import { Component } from '@angular/core';
import { trigger, transition, style, animate, query } from '@angular/animations';
...
@Component({
selector: 'my-app',
template: `
<button (click)="show = !show">toggle</button>
<h1 @fadeAnimation *ngIf="show">Angular Animations</h1>
`,
animations: [fadeAnimation]
})
export class AppComponent {
show = true;
}
To reference an animation to an element, we add the name of the animation prefixed with a @
on the element we would like to animate. For this example, the animation name in the template will be @fadeAnimation
. Now that we have a straightforward transition animation working, let’s move onto creating a list animation.
Animating Lists with ngFor
In this next example, we will build a basic text list and animate each item.
First, let’s build out a dynamic list with *ngFor
.
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
<button (click)="toggleList()">toggle list</button>
<ul class="items">
<li *ngFor="let item of items" class="item">
Item {{ item }}
</li>
</ul>
`,
animations: [fadeAnimation]
})
export class AppComponent {
items = [];
toggleList() {
this.items = this.items.length ? [] : [0,1,2,3,4,5,6,7,8,9,10];
}
}
Whenever we click our toggle button, we add or remove ten items from our list. When we animate our list, we don’t show all items all at once but wait until the previous list item is visible before starting the next item animation. This delay technique is called staggering. To stagger the list items, our syntax for the animation changes a bit.
import { trigger, transition, style, animate, query, stagger } from '@angular/animations';
const listAnimation = trigger('listAnimation', [
transition('* <=> *', [
query(':enter',
[style({ opacity: 0 }), stagger('60ms', animate('600ms ease-out', style({ opacity: 1 })))],
{ optional: true }
),
query(':leave',
animate('200ms', style({ opacity: 0 })),
{ optional: true }
)
])
]);
In this example, we have a single transition
function with the first parameter as * <=> *
. This syntax triggers the animation when any value passed into the trigger via the template has changed. This syntax can be used for when specific values have changed, read more in the documentation. We will refer back to this syntax when we get to the template code. The query
function allows us to query child elements and trigger an animation when child element leaves or enters our list.
Like before when an item enters, we use the :enter
syntax and run our animation. We start the item with a 0
opacity nad then apply our opacity 1
style. In this example, we use the stagger
function which will delay the animation from running until a given time frame after the previous item has finished animating. This creates the nice stagger animation on the list items we see in the video clip.
The last parameter for our query
is a configuration object { optional: true }
. This option tells the query animation not to throw an error if the element does not exist when the animation is triggered in the view.
In our :leave
query we could stagger the items again but for a better user experience we quickly fade away all the items for the user so we can start to render the next list.
To add our animation to our component, we need to add the trigger to the template.
<ul [@listAnimation]="items.length" class="items">
<li *ngFor="let item of items" class="item">
Item {{ item }}
</li>
</ul>
Notice how the @listAnimation
takes the items.length
as a property value. This binding is important and the reason why we use the * <=> *
syntax we saw earlier. Any time the binding value changes on the trigger our animation re-runs and executes our animation queries.
Because we are using the query
and stagger
functions to delay each item animation the trigger @listAnimation
needs to be on a parent element. In this case, the parent element is the ul
element. Now that we have a basic list working, we can make things a little more interesting by using images and use the animation we just created.
import { Component } from '@angular/core';
import { trigger, transition, style, animate, query, stagger } from '@angular/animations';
const listAnimation = trigger('listAnimation', [
transition('* <=> *', [
query(':enter',
[style({ opacity: 0 }), stagger('60ms', animate('600ms ease-out', style({ opacity: 1 })))],
{ optional: true }
),
query(':leave',
animate('200ms', style({ opacity: 0 })),
{ optional: true}
)
])
]);
const images = [
'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
'https://media.giphy.com/media/6C4y1oxC6182MsyjvK/giphy.gif',
'https://media.giphy.com/media/Ov5NiLVXT8JEc/giphy.gif',
'https://media.giphy.com/media/SRO0ZwmImic0/giphy.gif',
'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif'
];
@Component({
selector: 'my-app',
template: `
<button (click)="toggleImages()">toggle images</button>
<section [@listAnimation]="images.length" class="images">
<img *ngFor="let img of images" [src]="img" class="image" />
</section>
`,
animations: [fadeAnimation]
})
export class AppComponent {
images = [];
toggleImages() {
this.images = this.images.length ? [] : images;
}
}
By applying the same animation over a list of images, we can get a nice effect when loading the list of images.
Notice how the animation applies in order of the items in the list and not the order they are rendered in DOM by rows.