Digging in to Angular Animations

Alisa Duncan

Alisa Duncan

Alisa Duncan

  • Software Architect
  • Co-organizer AngularKC
  • Core team, ngGirls
  • Enjoys learning, reading, cooking, and drinking a glass of wine
  • Not a designer, not a CSS expert

@AlisaDuncan

Why do we want animations?

Helps draw attention to elements

Makes applications feel more expressive and interactive

The traditional way

Defined in CSS

Use AnimationEvent on supported browsers

Have to manage a lot yourself

Why do it the Angular way?

Builds on top of CSS animations

First class citizen as a component and router property

Hook in to the router to define animations consistently across your app

Bind directly in the template as well as supports HostBinding in the component for more programmatic control

Angular manages coordination and supports complex animation sequences

How does it all work?

State, Transitions, and Triggers

Define different styles based on state

Define transitions between states

Define when animations trigger

Animations triggers emit callbacks so you can customize behavior

Complex animations

Angular handles setup, teardown, and cleanup as it coordinates elements across the page

Parent elements can explicitly control animation of child elements using animateChild

Apply multiple animation triggers on an element

Some helper methods

query to find elements matching a criteria

stagger animations to fire with a timing gap between multiple elements

group animations to run in parallel

sequence animation steps

Define a set of animation styles using keyframes

Let's see Angular animations in action

Angular Tour of Heroes

Quick demo of the basic Tour of Heroes. We'll apply animation concepts we cover during the presentation to this app.

Component Animations

Import the BrowserAnimationsModule to your app

Add the animations metadata property to the component


import { BrowserAnimationsModule } from
'@angular/platform-browser/animations';

@NgModule({
  imports: [
    BrowserAnimationsModule
  ],
  declarations: [
    AppComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }
								

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.css'],
  animations: [
  // animation triggers go here
  ]
})
								

Trigger a style change based on state

Triggering styles changes on state

The background color of the clear button for the Messages output at the bottom of the view changes based on state.

Trigger style changes on state

Define styles for states

Trigger animation by binding to template


animations: [
  trigger('message-state', [
    state('even-messages', style({
      backgroundColor: '#fce38a' // green
    })),
    state('odd-messages', style({
      backgroundColor: '#95e1d3' // yellow
    }))
  ])
]
					      

<button
  class="clear"
  [@message-state]="messageService.messages.length % 2 ? 'even-messages' : 'odd-messages'"
  (click)="messageService.clear()">clear
</button>
                

Animate a transition between states

Transitions and timings

Animate elements entering the view by defining animations to fire based on transitions between states.

Transitions and timings

Declare the transition for the beginning state to the ending state

Looks something like this transition('begin => end', ...)

Define the animation using animate('duration delay easing', ...) method

About those transition definitions...

Start and ends with void state

Match all states with wildcard state *

Built in shortcuts for :enter and :leave

Built in helper for associating numeric values to transitions with :increment and :decrement

Can declare multiple transition matches

Arrow can be bi-directional

Using transitions and timings

Define animation

Trigger animation by binding to template


animations: [
  trigger('animateIn', [
    transition('void => *', [
        style({transform: 'translateY(100%)'}),
        animate('0.5s')
    ])
  ])
]
              

<ul class="heroes">
  <li *ngFor="let hero of heroes" [@animateIn]>
    <a routerLink="/detail/{{hero.id}}">
      <span class="badge">{{hero.id}}</span> {{hero.name}}
    </a>
    <button class="delete" title="delete hero" (click)="delete(hero)">x</button>
  </li>
</ul>
              

How about a fancier example?

Stagger animation of each list item

Angular supports complex animation of child elements natively. Here we change the animation of list to stagger on each list item.

Fancy is easy

Stagger each element of the *ngFor easily with built in animation methods.
Complex animations just got easier.


animations: [
  trigger('animateIn', [
    query(':enter', [
      style({opacity: 0, transform: 'translateY(100%)'}),
      stagger('0.5s', [
        animate('0.5s', style({opacity: 1, transform: 'none'}))
      ])
    ], {optional: true})
  ])
]
            

Conditional animation

Drive animation logic from component variables by defining state as a component property to conditionally animate.


<li *ngFor="let hero of heroes" [@animateIn]="animateState"></li>
            

animations: [
  trigger('animateIn', [
    transition('* => heroes-group', [
      style({transform: 'translateY(100%)'}),
      animate('0.5s')
      ]),
    transition('* => heroes-stagger', [
      query(':enter', [
        style({opacity: 0, transform: 'translateY(100%)'}),
        stagger('0.5s', [
          animate('0.5s', style({opacity: 1, transform: 'none'}))
        ])
      ], {optional: true})
    ])
  ])
]
            

Watching for animation events

React to events using the built in Animation callbacks. Watch for animation started and completed events by using start and done properties.


<ul class="heroes" *ngIf="heroes" [@animateIn]
  (@animateIn.done)="animationComplete($event)">
  <li *ngFor="let hero of heroes">
    <a routerLink="/detail/{{hero.id}}">
      <span class="badge">{{hero.id}}</span> {{hero.name}}
    </a>
  </li>
</ul>
            

Let's step up the fanciness

Route transition animations

Hooks in to the router to have animation between views of a route change

Define animations for nested routes as well

Extra fancy

Route animations

Animations can be applied to routes to create a smooth transition between the different pages. Here we'll toggle between dashboard and Heroes-Group.

Using route animations

Add animation data to the Routes definition

Define the animation. This is where you might need to use the data in a transition

Add the template bindings

Update component with the router outlet to pull the animation data


const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'dashboard', component: DashboardComponent, data: {routeAnimation: 'Dashboard'} },
  { path: 'detail/:id', component: HeroDetailComponent },
  { path: 'heroes', component: HeroesComponent, data: {routeAnimation: 'Heroes'} }
];
                  

animations: [
  trigger('routeAnimation', [
    transition('Dashboard => Heroes', [
      query(':enter, :leave', [], { optional: true })
    ]),
    transition('Heroes => Dashboard', [
      query(':enter, :leave', [], {optional: true})
    ])
  ])
]
                  

<div [@routeAnimation]="prepareRoute(outlet)">
  <router-outlet #outlet="outlet"></router-outlet>
</div>
                  

public prepareRoute(outlet: RouterOutlet) {
  return outlet && outlet.activatedRouteData && outlet.activatedRouteData.routeAnimation;
}
                  

Animate route sequences

Helpful if you have more than two route states

Applicable to navigation lists

Animating route sequences

Here we'll navigate between three routes.

Animating route sequences

Use a numeric value to represent navigation progression

Utilize built in helpers :increment and :decrement to define transitions


const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'dashboard', component: DashboardComponent, data: {routeAnimation: 1} },
  { path: 'detail/:id', component: HeroDetailComponent },
  { path: 'heroes-group', component: HeroesComponent, data: {routeAnimation: 2} }
  { path: 'heroes-stagger', component: HeroesComponent, data: {routeAnimation: 3} }
];
    					

animations: [
  trigger('routeAnimation', [
    transition(':increment', [
      query(':enter, :leave', [], { optional: true })
    ]),
    transition(':decrement', [
      query(':enter, :leave', [], {optional: true})
    ])
  ])
]
    					

Reusing animations

Keep it DRY and reuse animations

Animations can be defined separately from component

Reuse animations across multiple components

Keep code modular

Clean up component file

Make it reusable

Define animation in separate file optionally using variables

Consume in component using useAnimation method


export const myAnimation = animation([
  query(':enter', style({ transform: '{{ enterParam }}'})),
  query(':leave', style({ transform: '{{ leaveParam }}'}))
]);
              

animations: [
  trigger('routeAnimation', [
    transition(':increment', [
      useAnimation(myAnimation, {
        params: {
          enterParam: 'translateX(-100%)',
          leaveParam: 'translateX(200%)'
        }
      })
    ]),
    transition(':decrement', [
      useAnimation(myAnimation, {
        params: {
          enterParam: 'translateX(200%)',
          leaveParam: 'translateX(-100%)'
        }
      })
    ])
  ])
]
              

Testing components and routes with animations

Testing tip

Prevent animations from triggering while component DOM testing by importing the NoopAnimationsModule in the TestBed.

Happy testing!

Want to learn more?

@AlisaDuncan

@AlisaDuncan Digging in to Angular Animations