A New Wave of Animation Features in Angular

Learn to make use of the awesome new animation features in Angular 4.2+

Published on Jun 9, 2017

Angular 4.2 introduces a wave of new animation features that allow for multi-element & reusable animations (with input param support) and as well as full-blown router-level animations.

This set of animation features was originally set for release in 4.1, however, this was then moved to 4.2 to allow give the API space to mature until the final release of 4.2.

It's been a bumpy road for angular's animations DSL. Hopefully these new API features give you the power to animate your Angular apps in ways you never thought of before! :)

1.0 The tl;dr on Angular animations

Animations have been around since the release of Angular 2 back in September of 2016. While there isn't a full blown article on this on yearofmoo, there are other helpful resources out there that explain how all of it works.

Nethertheless, a short segment on how animations work in Angular would be helpful. Let's break this up into a few short pieces.


1.1 Installation

Animations live in both the @angular/animations and @angular/platform-browser/animations packages of Angular. This means to get access to this stuff, please add the following dependencies into your package.json when starting an application.

// package.json
{
  "dependencies": {
    "@angular/animations": "latest",
    "@angular/platform-browser": "latest",
    // the rest of the stuff
  }
}

Animations are now apart of your project, but not yet enabled for your application. To enable animations, be sure to add in the BrowserAnimationsModule into your application.

import {NgModule} from "@angular/core";
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import {AppComponent} from './app.component';
@NgModule({
  declarations: [AppComponent],
  imports: [BrowserAnimationsModule]
})
class AppModule {
}

1.2 FadeIn/FadeOut in animations

Now that animations are hooked up in an application, it's time to get animations to render on screen when some kind of action occurs. By action we mean that when some component's value changes we want to have that change kick off an animation. Animations are fired using animation property bindings which are prefixed with an @ symbol.

<!-- app.component.html -->
<div [@someCoolAnimation]="bindingVar">...</div>

Looking at the example above, when the bindingVar input value changes then it will notify the someCoolAnimation code of a state change. It is then up to someCoolAnimation to handle the state change and therefore kick off a cool animation. For an animation to fire here, the someCoolAnimation animation needs to be registered on the component using a series of animation helper methods.

// app.component.ts
import {Component} from "@angular/core";
import {trigger, transition} from "@angular/animations";
@Component({
  templateUrl: 'app.component.html',
  animations: [
    trigger('someCoolAnimation', [
      transition('* => fadeIn', [
        // fade in Animation
      ]),
      transition('* => fadeOut', [
        // fade out Animation
      ])
    ])
  ]
})
class AppComponent {
  bindingVar = '';
  fadeIn() {
    this.bindingVar = 'fadeIn';
  }
  fadeOut() {
    this.bindingVar = 'fadeOut';
  }
  toggle() {
    this.bindingVar == 'fadeOut' ? this.fadeIn() : this.fadeOut();
  }
}

As you can imagine, the transition steps for * => fadeIn and * => fadeOut correspond to the state change when the bindingVar value changes to fadeIn or fadeOut respectively.

What's missing here is the code required to actually animate the animation fadeIn/fadeOut code:

// app.component.ts
import {Component} from "@angular/core";
import {trigger, transition, style, animate} from "@angular/animations";
@Component({
  templateUrl: 'app.component.html',
  animations: [
    trigger('someCoolAnimation', [
      transition('* => fadeIn', [
        style({ opacity: 0 }),
        animate(1000, style({ opacity: 1 }))
      ]),
      transition('* => fadeOut', [
        animate(1000, style({ opacity: 0 }))
      ])
    ])
  ]
})
class AppComponent {
  //...
}

Alright so now the fadeIn/fadeOut state changes are animated, all that's missing is having a toggle button to make this state change kick off. Let's add that to our template code.

<!-- app.component.html -->
<button (click)="toggle()">Toggle Fade</button>
<div [@someCoolAnimation]="bindingVar">hello there</div>

And now we can get going with the Angular 4.2. stuff.


to top

2.0 New Animation Features / Full Demo

So the remainder of the guide goes over each of the new animation features. The new animation features in 4.2.0 enable us to:

  • Configure options and set input variables within animations
  • Define reusable animations using animation()
  • Query for inner elements within animations using query()
  • Stagger multiple elements within an animation using stagger()
  • Enable queried elements to trigger their own animations
  • Orchestrate a full-blown animation when routes change
  • Programmatically build/control an animation using AnimationBuilder

Let's get started.

2.1 What version of Angular do you need?

This new wave of animation features is apart of the Angular 4.2.0 release (at the time of this article the 4.2.3 version was out). Therefore make sure to have that setup in your package.json code:

// package.json
{
  "dependencies": {
    "@angular/animations": "latest",
    "@angular/common": "latest",
    "@angular/compiler": "latest",
    "@angular/core": "latest",
    "@angular/forms": "latest",
    "@angular/http": "latest",
    "@angular/platform-browser": "latest",
    "@angular/platform-browser-dynamic": "latest",
    "@angular/router": "latest"
  }
}

2.2 What about when using the angular cli?

If you want to setup a new CLI project using these animations then follow the instructions below:

npm install @angular/cli -g
# make sure that this is >= 1.0.0
ng --version 
# create the new angular application
ng new animationApp
# look into the `package.json` file to see that each
# @angular/dep dependency is 4.2.0 or higher

2.3 Demo application

For the new features introduced in this blog article there is a companion demo application that contains everything in working order. This demo should be familiar as it was presented at ng-conf back in April.

https://github.com/matsko/ng4-animations-preview

Also, here is a live view of the demo in case you want to skip installation and everything.

to top

3.0 Animation Options / Input Params

The first major addition to the animation DSL in Angular 4.2 is the ability to configure/override options for each of the step-based animation methods. These methods are:

sequence([...], { /* options */ })
group([...], { /* options */ })
trigger([...], { /* options */ })
transition([...], { /* options */ })
query([...], { /* options */ })

3.1 What options are supported?

As mentioned above, each of step-based animation methods support various animation options. How are these values passed in? Well now there is an additional method parameter that takes in all this stuff:

@Component({
  animations: [
    trigger('someCoolAnimation', [
      transition('* => *', [
        // the animation itself...
      ], {
        // the animation options
      })
    ])
  ]
})

Each of the step-based animation methods support the following options:

options.delay

same idea here as with duration only it can postpone the animation from happening until a bit later. Percentage values here are not supported. Negative delays are not supported now but they will be in a future release.

options.params

This is where input params are placed (more explained later).

What about binding-level options?

Options and input params can also be passed into an animation binding value when the animation kicks off.

<div [@animation]="{value: expVal, /* option1: value, option2: value */ }">...</div>

3.2 How to use animation input parameters

Alright so now that we understand how to pass option data into an animation (or a specific chunk of a sequence) it's possible to make use of animation input parameters by setting even more data via the options.params property.

Input parameters are just substitutions of data that can be applied to style properties and timing values anywhere in an animation sequence (depending on where the params are assigned). If a param key/value entry is set in the options.params object then it can be "echoed" using the oh-so-familiar {{ binding }} syntax:

// inside of @Component.animations...
transition('* => *', [
  style({ opacity: 0 }),
  animate("{{ time }}",
    style({ opacity: "{{ opacity }}" }),
], {
  time: "1s",
  opacity: "1"
})

Keep in mind that input parameter substitutions only work for style property values and timing data (for now). It is also possible to update inner param values with subsitutions as well.

Read once then animate...

One important thing to remember is that substitution values are not updated "on the fly". This means that as soon as the animation starts then all the input parameters are evaluated and the animation is built (they do not update as the animation is underway).

to top

4.0 Using animation() and useAnimation()

At this point you may be wondering what's the real advantage in having these input parameter substitution mechanisms when we can only place them in animations that exist directly within the component source code. Well, while it's always possible to create animations that are imported from other files, however, input parameters be passed in directly during animation rendering is still not possible this way. What's missing is a way to define and package an animation into a reusable piece of code that can be used elsewhere alongside full input parameter support. This is possible using the animation() animation helper method.

// animations.ts
import {animation, style, animate} from "@angular/animations";
export var fadeAnimation = animation([
  style({ opacity: "{{ from }}" }),
  animate("{{ time }}", style({ opacity: "{{ to }}" }))
], { time: "1s" })

The reusable fadeAnimation animation can now be imported into our application and invoked using the useAnimation method.

// inside of @Component.animations...
import {useAnimation, transition} from "@angular/animations";
import {fadeAnimation} from "./animations";
transition('* => *', [
  useAnimation(fadeAnimation, {
    from: 0,
    to: 1,
    time: '1s'
  })
])

And there you go, now you have a super reusable fade animation!

Animations using other animations

Also note that it's totally OK for a reusable animation to call another reusable animation directly within itself. Just don't go making any infinite animation loops.

to top

5.0 Using query() and stagger()

In case reusable animations do not meet your animation feature standards, maybe the new query() and stagger() features will. query() allow for multiple elements to be animated in parallel and stagger() allows for each queried item to be visually choreographed with a curtain-like animation effect.

5.1 Using query()

This is the biggest, baddest and most insane animation feature to hit Angular since ngAnimate back in the AnuglarJS days.

Long story short, query() allows for child elements to be collected and animated against a bunch of animation steps. So instead of having to animate everything on the element containing the animation trigger, we can now grab inner elements and make a really cool multi-element animation sequence.

import {query, style, animate} from "@angular/animations";
// the code below is expected to be inside of
// a trigger() and transition() definition
// apply some styling to all inner elements
query('*', style({ opacity: 0 }))
// grab a bunch of things and then animate
query('div, .inner, #id', [
  animate(1000, style({ opacity: 1 }))
])
// then reset the state for each element
query('*', [
  animate(1000, style('*'))
])

The selector value that is passed into query supports the following tokens:

Regular CSS Selectors

query('div, #id, .class, [attr], a + b') will return one or more items that match the selector string.

:enter and :leave

calling query(':enter') will return all newly inserted nodes within a container and query(':leave') will return nodes that have been set to be removed.

@trigger and @*

query('@trigger') will find one or more items that have an an animation trigger with that name. query("@*") will return all items that have any animation triggers.

:animating

query(':animating') will collect all elements that are currently animating.

:self

query(':self') will return the container element where the query is issued (this is useful when a container element is to be included in the same animation code along with other queried items)

What about the option values that can be passed into query?

Marking a query as optional

By default the query function will throw an error if it doesn't find any items. Using {optional:true} within the query options will ignore the error.

Can you limit queried values?

In addition to the optional options value, the limit option value can be used to cap the amount of elements that are selected.

The query feature is used extensively throughout the animation demo code (mostly alongside stagger and animateChild). Therefore, let's have a look at stagger so that we can start to understand how the code works in the demo application.

5.2 Space things out with stagger()

Back with ngAnimate in AngularJS we had the ability to stagger using some special CSS selector values. This is now possible directly in Angular's animation DSL code using the stagger() animation helper method.

Let's say that we have an ngFor with a container wrapped around it.

<div [@listAnimation]="items.length">
  <div *ngFor="let item of items">
    {{ item }}
  </div>
</div>

See the @listAnimation animation? Well that code right there is where we will be querying to find each of the newly inserted nodes that were placed there by ngFor.

trigger('listAnimation', [
  transition('* => *', [
    // remember that :enter is a special
    // selector that will return each
    // newly inserted node in the ngFor list
    query(':enter', [
      style({ opacity: 0 }),
      animate('1s', style({ opacity: 1 }))
    ])
  ])
])

In this example we see that the nodes are faded into the page all at the same time. Now if we use a stagger we can space this out so that it animates each step with a nice gap in between:

trigger('listAnimation', [
  transition('* => *', [
    // this hides everything right away
    query(':enter', style({ opacity: 0 })),
    
    // starts to animate things with a stagger in between
    query(':enter', stagger('100ms', [
      animate('1s', style({ opacity: 1 }))
    ])
  ])
])

And vuola! We are now able to apply a stagger animation.

How do you reverse stagger?

This can be done by using a negative number (like stagger(-1000, [])) or using the reverse label in the string-based timing value (like stagger("100ms reverse", [])).

Now that we understand how this works, let's examine a piece of code from the demo application.

<!-- image-preview.component.html -->
<div *ngFor="let image of activeImages" class="image-container" @image>
  <div class="image-info">
    <h2>Title</h2>
    <p>...</p>
    <p>...</p>
  </div>
  <div class="image-preview">
    <app-image [image]="image"></app-image>
  </div>
</div>

The code above is the preview area in the demo where an image's contents are displayed when clicked. Inside of the area that is displayed we have an @image animation that fires that will stagger animate in each of the inner contents (the paragraph tags and the image info). The animation code for that is as follows:

// image-preview.component.ts
trigger('image', [
  transition(':enter', [
    query('*', [
      style({ transform: 'translateX(200px)', opacity: 0 }),
      stagger(100, [
        animate('1200ms cubic-bezier(0.35, 0, 0.25, 1)', style('*'))
      ])
    ])
  ])

The code right here will run when the item is entered on screen and it will start off by hiding and horizontally offsetting each item on screen. Then it will go through each item and stagger animate them to be visible.

What does style("*") mean?

When style("*") is used it will revert all used styles in the animation back to their end state value (which is the same as setting each value to * explicitly).

to top

6.0 Sub animations with animateChild()

Now what about the situation when there is an element in the application that has an animation ready-to-go, but a parent animation runs instead? Is there a way that the parent animation can dictate when the child animation runs, how long it runs for or if it doesn't run at all?

<!-- do these two guys animate at the same time? -->
<div [@parent]="parentVal">
  <div [@child]="childVal">
    ...
  </div>
</div>

Using the animateChild animation function, the parent animation can allow the child animation to run at the exact right time.

Let's say that the @parent and @child animations look like so:

trigger('parent', [
  style({ transform: 'translate(-100px)' }),
  animate('500ms', style({ transform: 'translate(0px)' }))
]),
trigger('child', [
  style({ opacity:0 }),
  animate('0.5s', style({ opacity:1 }))
])

If these animations trigger at the same time then the parent animation will always win and the child animation will be skipped. However, using animateChild we can have the parent animation query into the child and tell it when it's allowed to animate:

trigger('parent', [
  style({ transform: 'translate(-100px)' }),
  animate('500ms',
    style({ transform: 'translate(0px)' })),
  query('@ child', animateChild())
]),
trigger('child', [
  style({ opacity:0 }),
  animate('0.5s', style({ opacity:1 }))
])

And there you have it. Now the parent animation can dictate exactly when the sub animation runs (if options.duration is provided then it can also override the duration to speed up, slow down or skip the animation entirely).

Now that we understand this, let's examine some of the image preview code from the demo application:

@Component({
  animations: [ ... ]
})
export class ImagePreviewComponent {
  @HostBinding('@preview')
  public count: number = 0;
}

This simplified representation of the component code has a host animation binding bound to a counter value that updates each time the current image changes in the selected carousel. Here's what the HTML looks like again:

<!-- image-preview.component.html -->
<div *ngFor="let image of activeImages" class="image-container" @image>
  <div class="image-info">
    <h2>Title</h2>
    <p>...</p>
    <p>...</p>
  </div>
  <div class="image-preview">
    <app-image [image]="image"></app-image>
  </div>
</div>

Since a host binding is being used here this means that it will run an animation on the entire carousel container.

Let's see what the animation looks like:

trigger('preview', [
  //...
  transition('* => *', [
    query(':enter, :leave', style({ position: 'absolute', left: '0%' })),
    query(':enter', style({ left: '100%' })),
    group([
      query(':leave', group([
        animateChild(),
        animate('1200ms cubic-bezier(0.35, 0, 0.25, 1)', style({ opacity:0, left:'-100%' }))
      ])),
      query(':enter', group([
        animate('1200ms cubic-bezier(0.35, 0, 0.25, 1)', style('*')),
        animateChild()
      ]), { delay: 200 }),
    ])
  ])
])

So there's alot going on here, but just about all of the querying occurs on the :enter and :leave nodes (which are the nodes inserted by ngFor). Once the animation styles are setup, a group animation (using group) is run which then animates the leave and enter items together in parallel. It is in this code that animateChild is called which then allows the @image animation code to run.

What about the starting styles for the inner animation?

One thing that isn't obvious until you run the animation is the what the starting visual state of the content will be before animateChild() is run (for the sub animation elements). Logically speaking it should just be visible right? Well yes, but Angular knows that this happens so it applies the very first frame of the inner animation at the start of the parent animation to the sub animation element(s). This way the parent animation doesn't have to have any idea of how to style the inner sub animation before its called (but it still can if it wants).

to top

7.0 Routable Animations

Seeing as the query and animateChild methods are designed to allow inner animations to fire at a convenient time within an animation sequence, it would make perfect sense to allow for the router to activate an animation for pages that are inserted/removed into the viewport. In other words, wouldn't it be cool if it were possible to allow for the router to animate the construction and destruction of views as navigation occurs between routes? Let's give it a try.

7.1 Getting router-outlet to animate

Due the mechanics of the <router-outlet> directive, we can't just place an animation trigger directly on it since it the router component views are not inserted directly inside of it (they are placed right after where the router outlet element is). Therefore what we need to do is wrap a div element around the router outlet so we can have a parent element that handles all the animations based on the state of the router.

<!-- app.html -->
<div [@routeAnimation]="prepRouteState(routerOutlet)">
  <!-- make sure to keep the ="outlet" part -->
  <router-outlet #routerOutlet="outlet"></div>
<div>

Notice how we're grabbing ahold of a local variable called routerOutlet? Well this is the special guy who contains the details about what page/route is active.

The outlet variable is passed into the prepRouteState function which is used to decide what state value to pass into the routeAnimation animation.

// app.ts
@Component({
  animations: [
    trigger('routeAnimation', [
      // no need to animate anything on load
      transition(':enter', []),
      // but anytime a route changes let's animate!
      transition('* => *', [
        // we'll fill this in later
      ])
    ])
  ]
})
class AppComponent {
  prepRouteState(outlet: any) {
    return outlet.activatedRouteData['animation'] || 'firstPage'; 
  }
}

The prepRouteState function takes in the outlet and returns a string value representing the state of the animation based on the custom data of the current active route. So as you can imagine, for this to work, we need to specify an animation state value directly in the spot where each of our routes are defined.

Let's see what our routing definition code looks like:

<!-- app.ts -->
const ROUTES = [
  { path: '',
    component: HomePageComponent,
    data: {
      animation: 'homePage'
    }
  },
  { path: 'support',
    component: SupportPageComponent,
    data: {
      animation: 'supportPage'
    }
  }
]

See how the animation property is defined inside of the route's data object? Yes this is the state of the route that we'll be using within our routeAnimation trigger transition code.

trigger('routeAnimation', [
  // no need to animate anything on load
  transition(':enter', []),
  // when we go away from the home page to support
  transition('homePage => supportPage', [
    // ...
  ]),
  // and when we go back home
  transition('supportPage => homePage', [
    // ...
  ])
])

Now we're talking. Let's animate some stuff!

7.2 :enter and :leave are the new and old views

Just as we have been using :enter and :leave each time we've used the query code, we will be using it again here.

So as the routes change, we want to grab ahold of the old and new pages that are apart of the route change. So if our animation goes from homePage => supportPage then the homePage can be queried by :leave and the supportPage can be queried by :enter.

trigger('routeAnimation', [
  //...
  transition('homePage => supportPage', [
    // make sure the new page is hidden first
    query(':enter', style({ opacity: 1 })),
    // animate the leave page away
    query(':leave', [
      animate('0.5s', style({ opacity: 0 })),
      style({ display: 'none' })
    ]),
    // and now reveal the enter
    query(':enter', animate('0.5s', style({ opacity: 1 }))),
  ]),
  //...
])

The code above will animate out the homePage first and then animate in the supportPage. If we use group() here then can have this animation run parallel

trigger('routeAnimation', [
  //...
  transition('homePage => supportPage', [
    group([
      query(':enter', [
        style({ opacity: 0 }),
        animate('0.5s', style({ opacity: 1 }))
      ]),
      query(':leave', [
        animate('0.5s', style({ opacity: 0 }))
      ])
    ])
  ]),
  //...
])

Now each page might even have its own animation that can be called when an enter or a removal happens. This means that we can use animateChild to kick off animations at the right time.

// the animation itself
trigger('routeAnimation', [
  //...
  transition('homePage => supportPage', [
    group([
      query(':enter', [
        style({ opacity: 0 }),
        animate('0.5s', style({ opacity: 1 })),
        animateChild()
      ]),
      query(':leave', [
        animate('0.5s', style({ opacity: 0 })),
        animateChild()
      ])
    ])
  ]),
  //...
])

And that's how router-level animations are run.

to top

8.0 Programmatic Animations with AnimationBuilder

The final new feature is the addition of programmatic animation construction using the injectable AnimationBuilder service (yes inside of components).

Building animations within components is much more low-level than using animation triggers. The reason for this is because the animation trigger code does a lot of work to keep track of state and to automatically cancel animations when a new one starts. All of this is out the window with when AnimationBuilder is used. But fear not, a player object is returned each time an animation is run and that player can be used to control the state of the animation.

So how is AnimationBuilder used? Well once injected into a component, then it can be used directly to create an animation:

import {AnimationBuilder} from "@angular/animations";
@Component({...})
class MyCmp {
  constructor(public builder: AnimationBuilder) {}
  
  animate() {
    // this makes instructions on how to build the animation
    const factory = this.builder.build([
       // the animation
    ]);
    
    // this creates the animation
    const player = factory.create(this.someElement);
    
    // start it off
    player.play();
  }
}

In the demo application code we have a modal that pops up which contains a loading indicator to represent the progress state of the upload. The progress state value itself is applied using a programmatic animation built directly in the component.

import {AnimationBuilder, AnimationPlayer} from "@angular/animations";
@Component({
  selector: 'loader',
  template: `
 <div class="loading-stage">
   <div class="loading-bar" #loadingBar>
     {{ percentage }}%
   </div>
 </div> 
  `
})
export class LoaderComponent {
  @ViewChild('loadingBar')
  public loadingBar;
  
  public player: AnimationPlayer;
  private _percentage = 0;
  
  constructor(private _builder: AnimationBuilder) {}
  
  get percentage() { return this._percentage; }
  
  @Input('percentage')
  set percentage(p: number) {
    this._percentage = p;
    
    if (this.player) {
      this.player.destroy();
    }
    
    const factory = this._builder.build([
      style({ width: '*' }),
      animate('350ms cubic-bezier(.35, 0, .25, 1)', style({ width: (p * 100) + '%' }))
    ]);
    this.player = factory.create(this.loadingBar.nativeElement, {});
    this.player.play();
  }
}

The usage of the loader component looks like so:

<loader [percentage]="percentageValue"></loader>

The percentage value will be applied when the binding changes and that will in turn call the percentage setter within the component. It's in that setter code that the animation will be built and run.

set percentage(p: number) {
  // this builds the animation
  const factory = this._builder.build([
    style({ width: '*' }),
    animate('350ms cubic-bezier(.35, 0, .25, 1)', style({ width: (p * 100) + '%' }))
  ]);
  // and this will will create a player for that animation
  // (notice how the loading bar is referenced)
  this.player = factory.create(this.loadingBar.nativeElement, {});
}

Now the player can be interacted with directly to play/pause/reset and destroy the animation. Oh and scrubbing is possible by pausing the player and using player.setPosition(percentage).

to top

9.0 Conclusion

Thank you for reading this article and for testing out the new animation features. In case you come across any bugs please report them to the github angular/angular project's issue page.

to top