In web app development, animation is automation of the position and display characteristics of DOM elements. Angular provides its own programming interface for animations, insulating the developer from browser compatibility concerns. The developer defines styles for different states a DOM element can take on, and defines transitions for how to change from one state to another. Let’s first look at a non-animated example, and then animate it.
Hard cut
Say we have component with a variable named mood. Sometimes it holds the value “friendly” and sometimes it holds the value “mad.” In the component’s html template we display the value, but additionally we want a red background if mad. So far none of this requires Angular animation. We can use stock Angular to set the background color based on the mood value.
<p [style.background-color]=”mood==’mad’?’red’:’green'”>{{mood}}</p>
mad
This creates what Rachel Nabors calls a hard cut. This term comes from video/film editing and means there is no transition from one scene to the next. The first scene ends and the next scene appears abruptly. In our case, the moment the value for mood changes, the background color instantly changes. The text does too, but for simplicity let’s just focus our work on the background color.
Crossfade
We can introduce Angular animation to create a transition instead of a hard cut. Say we want the colors to slowly transition for three seconds when the mood changes state, a rudimentary animation known in video/film editing as a crossfade.
Instead of directly setting the style for mad, we define an animation trigger. In the trigger we define the style for mad (red) and the style for friendly (green), and how to animate the transition (take a leisurely 3 seconds to change color instead of doing it instantly). I have named the animation trigger moodAni.
<p [@moodAni]=’mood’>{{mood}}</p>
mad
In Angular Typescript, in addition to the usual @Component selector and template, we define animations. The <=> means do the transition when going either way, from mad to friendly or from friendly to mad:
@Component({ selector: 'app-root', template: '<p [@moodAni]="mood">{{mood}}</p>', animations: [ trigger('moodAni', [ state('mad',style({backgroundColor:'red'})), state('friendly',style({backgroundColor:'green'})), transition('friendly <=> mad',animate('3s')) ] ) ] })
Crossfade?
The term “crossfade” isn’t in the code. Any transition for an element that isn’t changing size or position is a crossfade. More generally, what the Angular animation module does is known as tweening. We define states or keyframes and then specify how long to take between each. This is quite a bit like CSS transitions. Indeed, you might be wondering how I managed to get Angular examples working directly inside this blog. I didn’t. They are pure CSS equivalents.
Nice corral you got there, Angular
If you haven’t made the plunge yet into Angular animations, you might be wondering if you’d be better off sticking with CSS transitions. It is possible. It’s what I was doing before I discovered Angular animations. I like the Angular way better. I don’t have to worry about browser differences, and the coding for animation is corralled into the component’s animations specification.
But some horses can get through that fence
Notice in the crossfade example above that if you click the button a second time quickly, the new animation immediately takes over. In Shufflizer I have a case where this isn’t desirable — the download progress spinner. For small playlists sometimes it would not fade smoothly in and out. When the download was done before the fade-in was done, the progress spinner would cut over to the fade-out animation. Nothing was technically wrong, but it was strange and unpolished. Here is how I solved it.
Angular provides an event that fires at the beginning and the end of an animation. I use this event to prevent shutting the progress spinner while the fade-in animation is still running.
<div class="dimmed" style="text-align:center;padding-top:100px;padding-left:20%;padding-right:20%" *ngIf="dimSet()" [@dimAnim] (@dimAnim.start)="setAnim($event)" (@dimAnim.done)="setAnim($event)"> <mat-card class="webLandscape"> <mat-card-content> <h2 class="example-h2">{{plLoadingDirection()}}loading... {{progressPcnt()}}%</h2> <mat-progress-spinner mode="determinate" [value]="progressPcnt()" style="margin-left:auto;margin-right:auto"> </mat-progress-spinner> </mat-card-content> </mat-card> </div>
The progress spinner renders when dimSet() is true. setAnim simply stashes the event object into a variable so dimSet can use it.
It all comes down to the last line in dimSet. Return true if a playlist is loading or if the fade-in animation is still running.
setAnim(ev): void {
this.anim=ev;
}
dimSet(): boolean {
let animStillRunning=this.anim
&& this.anim.triggerName == 'dimAnim'
&& this.anim.totalTime > 1
&& this.anim.phaseName == 'start'
&& this.anim.fromState == 'void' ;
return this.playlistService.plIsLoading
|| animStillRunning;
}
I found the names of the event attributes on the AnimationEvent documentation page.
So I like the animations “corral” but when we need to know the status of an animation, it’s not an animation we can just set into motion and forget about, then special coding for animations comes back into our TypeScript code.
For more information about Angular animation, see the Angular animations page. It’s where I learned most of what I know about Angular animations.