Angular LAB: animating lists and using AnimationBuilder for imperative animations
Michele Stieven
Posted on October 9, 2024
Did you know that Angular includes a complex animation system? I find it especially useful when I want to animate elements when they enter the screen or when they get destroyed!
Also, you can use AnimationBuilder
in order to imperatively play, pause or stop some custom animations! Let's see how it's done.
Creating a list
In this exercise we start by creating a list, something like this:
@Component({
selector: 'app-root',
standalone: true,
template: `
<button (click)="addUser()">Add user</button>
<ul>
@for (user of users(); track user.id) {
<li>{{ user.name }}</li>
}
</ul>
`,
})
export class AppComponent {
users = signal<User[]>([
{ id: Math.random(), name: 'Michele' }
]);
addUser() {
this.users.update(users => [...users, { id: Math.random(), name: 'New user' }]);
}
}
Notice that we've included a button which adds a user to the list!
Animating the list
Now, what if we want to animate the new user which will be added? First, we want to tell Angular that we want to use its animation system by providing it in your main configuration:
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
bootstrapApplication(AppComponent, {
providers: [
provideAnimationsAsync(),
]
});
Then, we can create our animation:
import { trigger, transition, style, animate } from '@angular/animations';
const fadeInAnimation = trigger('fadeIn', [
transition(':enter', [
style({ transform: 'scale(0.5)', opacity: 0 }),
animate(
'.3s cubic-bezier(.8, -0.6, 0.2, 1.5)',
style({ transform: 'scale(1)', opacity: 1 })
)
])
])
With these helpers we:
- Created an animation trigger called
fadeIn
- Added a transition when the element enters the screen
- Applied an initial style
- Immediately started the animation which will result in a new style applied to the element
For more information on how to write animations, you can refer to the official guide which is awesome!
Now let's apply this animation to each of our elements in the list:
@Component({
...,
template: `
<button (click)="addUser()">Add user</button>
<ul>
@for (user of users(); track user.id) {
<li @fadeIn>{{ user.name }}</li> <!-- Notice here -->
}
</ul>
`,
// Also, add the animation to the metadata of the component
animations: [fadeInAnimation]
})
Now, when a new item is added, it gets animated! Our first step is done.
Notice that in order for our animation to work correctly, Angular has to track each element in our for
, because otherwise it could end up re-creating the same elements when updating the template, resulting in unwanted animations. This comes for free with the new Control Flow syntax because the track
property is mandatory, but if you're using old verisons of Angular with the *ngFor
directive, you must use the trackBy
option like this:
<li
*ngFor="let user of users; trackBy: trackByUserId"
@fadeIn
>{{ user.name }}</li>
// A class method in your component:
trackByUserId(index, user: User) {
return user.id;
}
Now, let's add another type of animation to our list.
AnimationBuilder
Let's add a button to each element of our list:
<li @fadeIn>
{{ user.name }}
<button>Make me blink</button>
</li>
Imagine this: we want to make the element blink when we press the button. That'd be cool! This is where the AnimationBuilder
service comes in.
First, let's create a Directive which will be applied to every element. In this directive, we'll inject both ElementRef
and AnimationBuilder
:
import { AnimationBuilder, style, animate } from '@angular/animations';
@Directive({
selector: '[blink]',
exportAs: 'blink', // <--- Notice
standalone: true
})
export class BlinkDirective {
private animationBuilder = inject(AnimationBuilder);
private el = inject(ElementRef);
}
Notice that we exported the directive: we'll get to the reason in a few seconds.
Then, we can create a custom animation like this:
export class BlinkDirective {
...
private animation = this.animationBuilder.build([
style({ transform: 'scale(1)', opacity: 1 }),
animate(150, style({ transform: 'scale(1.1)', opacity: .5 })),
animate(150, style({ transform: 'scale(1)', opacity: 1 }))
]);
}
We're using the same functions that we used in the previous animation, just with different styles.
Now we want to create a player which will perform the animation on our element:
export class BlinkDirective {
...
private player = this.animation.create(this.el.nativeElement);
}
And now let's expose a method which will actually start the animation!
export class BlinkDirective {
...
start() {
this.player.play();
}
}
There's only one step left: we must import the directive, apply it to our elements, grab it with a template variable and call the method when the button is pressed!
@Component({
selector: 'app-root',
standalone: true,
template: `
<button (click)="addUser()">Add user</button>
<ul>
@for (user of users(); track user.id) {
<li @fadeIn blink #blinkDir="blink">
{{ user.name }}
<button (click)="blinkDir.start()">Make me blink</button>
</li>
}
</ul>
`,
imports: [BlinkDirective],
animations: [
fadeInAnimation
]
})
We can grab the directive's instance and put it in a local variable because we previously exported the directive with exportAs
. That's the key part!
Now, try clicking on the button: the element whould be animated correctly!
The exercise is complete, but this is just the tip of the iceberg! The AnimationPlayer
has a lot of commands which you can use to stop, pause and resume the animation. Very cool!
interface AnimationPlayer {
onDone(fn: () => void): void;
onStart(fn: () => void): void;
onDestroy(fn: () => void): void;
init(): void;
hasStarted(): boolean;
play(): void;
pause(): void;
restart(): void;
finish(): void;
destroy(): void;
reset(): void;
setPosition(position: number): void;
getPosition(): number;
parentPlayer: AnimationPlayer;
readonly totalTime: number;
beforeDestroy?: () => any;
}
Here's our full example if you want to play with it: just put it in your main.ts
file and see it in action!
import { Component, signal, Directive, ElementRef, inject } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { trigger, transition, style, animate, AnimationBuilder } from '@angular/animations';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
interface User {
id: number;
name: string;
}
@Directive({
selector: '[blink]',
exportAs: 'blink',
standalone: true
})
export class BlinkDirective {
private animationBuilder = inject(AnimationBuilder);
private el = inject(ElementRef);
private animation = this.animationBuilder.build([
style({ transform: 'scale(1)', opacity: 1 }),
animate(150, style({ transform: 'scale(1.1)', opacity: .5 })),
animate(150, style({ transform: 'scale(1)', opacity: 1 }))
]);
private player = this.animation.create(this.el.nativeElement);
start() {
this.player.play();
}
}
const fadeInAnimation = trigger('fadeIn', [
transition(':enter', [
style({ transform: 'scale(0.5)', opacity: 0 }),
animate(
'.3s cubic-bezier(.8, -0.6, 0.2, 1.5)',
style({ transform: 'scale(1)', opacity: 1 })
)
])
])
@Component({
selector: 'app-root',
standalone: true,
template: `
<button (click)="addUser()">Add user</button>
<ul>
@for (user of users(); track user.id) {
<li @fadeIn blink #blinkDir="blink">
{{ user.name }}
<button (click)="blinkDir.start()">Make me blink</button>
</li>
}
</ul>
`,
imports: [BlinkDirective],
animations: [
fadeInAnimation
]
})
export class App {
users = signal<User[]>([
{ id: Math.random(), name: 'Michele' }
]);
addUser() {
this.users.update(users => [...users, { id: Math.random(), name: 'New user' }]);
}
}
bootstrapApplication(App, {
providers: [
provideAnimationsAsync()
]
});
Posted on October 9, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 24, 2024