Select Page

Huge fan alert! Angular Animations are one of my favorite parts of Angular, and I think Matias Niemela did just an incredible job with the current implementation of the library. As a point of reference, I wanted to revisit an example that I built in AngularJS and update it to the latest version of Angular to show how we would accomplish the same project if we built it out today.

Source CodeDemo App

Create the Slider

The first step to building an animated slider is to build a functional slider. Practically speaking, it is amazing what you can accomplish when you judiciously “sprinkle” animations into an already working application. So first the slider and then the animations!

The Initial Setup

First, we create our Angular CLI app by running ng new angular-slideshow.

We then add Bootstrap styling to the app by running npm install –save bootstrap and then importing it into styles.css via @import ‘~bootstrap/dist/css/bootstrap.css’;.

Lastly, we add the @angular-devkit/build-angular package to our project via npm i –save-dev @angular-devkit/build-angular. This allows us to fully leverage the Angular CLI.

You may encounter an error like the following while trying to build your app: You seem to not be depending on "@angular/core" and/or "rxjs". This is an error.
If that is the case, remove your node_modules directory and reinstall dependencies.

Display the Slides

The first thing for us to do is add a slides collection to our AppComponent. You can pull the images from the sample repository or use your own as long as you put them in the assets directory.

export class AppComponent {
  slides = [
    {image: 'assets/img00.jpg', description: 'Image 00'},
    {image: 'assets/img01.jpg', description: 'Image 01'},
    {image: 'assets/img02.jpg', description: 'Image 02'},
    {image: 'assets/img03.jpg', description: 'Image 03'},
    {image: 'assets/img04.jpg', description: 'Image 04'}
  ];
}

We will then iterate over the collection in our template to display the images. This is accomplished with a basic ngFor directive where we bind the src attribute to the slide.image value.


<div class="container slider">
<div>
    <img class="slide slide-animation nonDraggableImage"
         *ngFor="let slide of slides"
         [src]="slide.image">
  </div>
</div>

I am not going to get into the CSS used in the layout but you can reference the styling in the app.component.css file.

Interact with the Slides

To determine the slide that we want to display, we need to keep track of the current slide. We do this by adding a currentIndex property to our class and creating a method called setCurrentSlideIndex that simply captures the current index and keeps track of it in the class. We also create a helper method called isCurrentSlideIndex that takes any given index and returns whether or not the index is of a selected slide.

export class AppComponent {
  currentIndex = 0;
  slides = [...];

setCurrentSlideIndex(index) {
    this.currentIndex = index;
  }

isCurrentSlideIndex(index) {
    return this.currentIndex === index;
  }
}

And this is where things get interesting! In Angular, it is not possible to use ngFor and ngIf on the same element and so we need to use the longhand version with ng-template. We will leave ngIf on the img element and then wrap that with ng-template. We are also going to track the index by assigning it to an i property.


<div class="container slider">
<div>
    <ng-template ngFor [ngForOf]="slides" let-slide let-i="index">
      <img class="slide slide-animation nonDraggableImage"
           *ngIf="isCurrentSlideIndex(i)"
           [src]="slide.image">
    </ng-template>
  </div>
</div>

This allows us to add navigation by iterating over the slides collection and adding a button that correlates to each slide.


<div class="container slider">
<div>
    <ng-template ngFor [ngForOf]="slides" let-slide let-i="index">
      <img class="slide slide-animation nonDraggableImage"
           *ngIf="isCurrentSlideIndex(i)"
           [src]="slide.image">
    </ng-template>
  </div>
<nav class="nav">
<div class="wrapper">
<ul class="dots">
<li class="dot" *ngFor="let slide of slides; let i = index;">
          <span [ngClass]="{'active':isCurrentSlideIndex(i)}"
                (click)="setCurrentSlideIndex(i);">
                {{slide.description}}</span>
        </li>
</ul></div>
</nav>
</div>

We then bind to the click event on the dot button and call setCurrentSlideIndex and pass in the index that was set during the ngFor iteration. We can also dynamically add an active class with ngClass by calling the same method.

Moving forward or backward in the slides is simply a matter of incrementing or decrementing currentIndex which we are doing in the nextSlide and prevSlide method. In our nextSlide method, we are checking to make sure that we do not extend beyond the bounds of our slides array. If we are not at the end of the array, we increment currentIndex and set it to the end if we are. In our prevSlide method, we make sure that we are not going to go negative and decrement currentIndex if we are safe.

export class AppComponent {
  currentIndex = 0;
  slides = [...];

setCurrentSlideIndex(index) {
    this.currentIndex = index;
  }

isCurrentSlideIndex(index) {
    return this.currentIndex === index;
  }

prevSlide() {
    this.currentIndex = (this.currentIndex > 0) ? --this.currentIndex : this.slides.length - 1;
  }

nextSlide() {
    this.currentIndex = (this.currentIndex < this.slides.length - 1) ? ++this.currentIndex : 0;
  }
}

And now all we have to do is add previous and next buttons that call prevSlide and nextSlide, respectively.


<div class="container slider">
<div>
    <ng-template ngFor [ngForOf]="slides" let-slide let-i="index">
      <img class="slide slide-animation nonDraggableImage"
           *ngIf="isCurrentSlideIndex(i)"
           [src]="slide.image">
    </ng-template>
  </div>
  <button class="arrow prev" (click)="prevSlide()"></button>
  <button class="arrow next" (click)="nextSlide()"></button>

<nav class="nav">
<div class="wrapper">
<ul class="dots">
<li class="dot" *ngFor="let slide of slides; let i = index;">
          <span [ngClass]="{'active':isCurrentSlideIndex(i)}"
                (click)="setCurrentSlideIndex(i);">
                {{slide.description}}</span>
        </li>
</ul></div>
</nav>
</div>

We are done! The majority of the work up to this point was in the actual layout and styling, which I took care of. Even after all these years, I still have these “I can’t believe I was able to build this with such a small amount of code” moments, which is pretty awesome.

Animate the Slider

Now comes the fun part! We will take our functional slider and add in a coat of animation polish.

Set Up Animations

To give our application animation capabilities, we need to add the BrowserAnimationsModule to our application. We will update our app.module.ts to include the import and then add it to the imports property on NgModule.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

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

Create an Animation Trigger

Now that our application is animation enabled, we are going to add an animation trigger to our application so that we have an entry point for our animations. We will add an animations property to our component metadata and define a slideAnimation trigger by calling the trigger method, passing in slideAnimation as the first parameter and an empty array as the second parameter. We will build out the animation within the array in just a moment, but for now, we will add the trigger to our template by adding @slideAnimation to the img tag.

All animation methods below are imported from @angular/animations. To conserve space, I am not going to include the import in the rest of the code samples.

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  animations: [
    trigger('slideAnimation', [])
  ]
})

<div class="container slider">
<div>
    <ng-template ngFor [ngForOf]="slides" let-slide let-i="index">
      <img @slideAnimation
           class="slide slide-animation nonDraggableImage"
           *ngIf="isCurrentSlideIndex(i)"
           [src]="slide.image">
    </ng-template>
  </div>
  ...

</div>

Add an Animation Transition

We are going to start out with a very basic animation by fading the slides in and out. This is done by adding a transition call to the slideAnimation animation definition for the slide that needs to fade in, and another for the slide that needs to fade out. Typically the first parameter of a transition call would be the starting and stopping state that the transition would apply to but we are going to use the :enter and :leave convenience values. Within the transition definition, we will call style to set the initial opacity to 0 and also call animate to fade in the slide to full opacity over 300 milliseconds. We will do the reverse for the slide we want to remove by setting its opacity to 0 over 300 milliseconds as well.

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  animations: [
    trigger('slideAnimation', [
      transition(':enter', [
        style({ opacity: 0 }),
        animate('300ms', style({ opacity: 1 })
      ]),
      transition(':leave', [
        animate('300ms', style({ opacity: 0 })
      ])
    ])
  ]
})

Query for a Child Element

Currently, we are adding our animations directly to the slides which is fine for simple animations, but what happens when you need to coordinate animations between multiple elements? This is where the animations library really shines in my opinion, as it is really easy to use a parent element as an entry point to your animations and then query and animate its children directly. We can select child elements using query and then animate them in parallel using the group method.

To make this work, we will move the trigger to the parent element so that we can capture the enter and leave transition in the same group. We will see this come into play when we want to define a different animation for when a slide is incrementing or decrementing. We will also add an optional flag to our leave transition so that we do not throw an error when the slider initially loads as there is nothing to take off the stage.

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  animations: [
    trigger('slideAnimation', [
      transition('* => *', group([
        query(':enter', [
          style({ opacity: 0 }),
          animate('300ms', style({ opacity: 1 })
        ]),
        query(':leave', [
          animate('300ms', style({ opacity: 0 })
        ], {optional: true})
      ]))
    ])
  ]
})

Since we are now querying the children of the trigger, we move the trigger to a parent div. We also turn the trigger into a property binding and set it to the currentIndex function, which triggers the state changes captured by ‘* => *’. Effectively we are listening for when the currentIndex changes, getting any image elements that are displaying or hiding at the time, and run their animations in parallel.


<div class="container slider">
<div [@slideAnimation]="currentIndex">
    <ng-template ngFor [ngForOf]="slides" let-slide let-i="index">
      <img class="slide slide-animation nonDraggableImage"
           *ngIf="isCurrentSlideIndex(i)"
           [src]="slide.image">
    </ng-template>
  </div>
  ...

</div>

Add Slide Effect with :increment and :decrement

In order to infer direction from currentIndex changes, we use the :increment and :decrement transition pseudo selectors. We query the displaying/hiding images like before, but we change the transition to be a “slide-left” (when the index increases) or “slide-right” (when the index decreases). Since we are already binding the trigger to currentIndex in the template, no further changes to the template are needed.

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  animations: [
    trigger('slideAnimation', [
      transition(':increment', group([
        query(':enter', [
          style({
            transform: 'translateX(100%)'
          }),
          animate('0.5s ease-out', style('<em>'))
        ]),
        query(':leave', [
          animate('0.5s ease-out', style({
            transform: 'translateX(-100%)'
          }))
        ])
      ])),
      transition(':decrement', group([
        query(':enter', [
          style({
            transform: 'translateX(-100%)'
          }),
          animate('0.5s ease-out', style('</em>'))
        ]),
        query(':leave', [
          animate('0.5s ease-out', style({
            transform: 'translateX(100%)'
          }))
        ])
      ]))
    ])
  ]
})

Bonus: Add Touch Gestures

Since we are here and mobile devices are a thing, I am going to add a bonus section and show how to add touch gestures with HammerJS.

Add Hammer.js

Naturally, the first step is to add the library to our application.

Run npm i —save hammerjs, then add the resource path to the Angular CLI config in angular.json.

{
  ...
  "projects": {
    "angular-slideshow": {
      ...
      "architect": {
        "build": {
          ...
          "options": {
            ...
            "scripts": [
              "node_modules/hammerjs/hammer.js"
            ]
          }
        }
      }
    }
  }
}

Add Event Listeners

And then we just need to add an event handler for the swiperight and swipeleft events and call nextSlide and prevSlide, respectively.


<div class="container slider">
<div [@slideAnimation]="currentIndex">
    <ng-template ngFor [ngForOf]="slides" let-slide let-i="index">
      <img class="slide slide-animation nonDraggableImage"
           *ngIf="isCurrentSlideIndex(i)"
           (swiperight)="nextSlide()" (swipeleft)="prevSlide()"
           [src]="slide.image">
    </ng-template>
  </div>
</div>

Not bad for a 60-second upgrade!

Bonus++: Preload Images

Okay fine! One more “bonus” since we are here and it is the little things that matter.

On the first load, the slideshow will actually appear choppy due to the images loading when they are selected. To avoid this, we can “preload” them in the component. We can accomplish this feat of magic by iterating over our slides collection and instantiating a new Image object which will force the image to load in the background. Tadah!

...
export class AppComponent implements OnInit {
  currentIndex = 0;
  slides = [
    {image: 'assets/img00.jpg', description: 'Image 00'},
    {image: 'assets/img01.jpg', description: 'Image 01'},
    {image: 'assets/img02.jpg', description: 'Image 02'},
    {image: 'assets/img03.jpg', description: 'Image 03'},
    {image: 'assets/img04.jpg', description: 'Image 04'}
  ];

ngOnInit() {
    this.preloadImages();
  }

preloadImages() {
    this.slides.forEach(slide => {
      (new Image()).src = slide.image;
    });
  }
}

Review

Let us take a moment to do a quick recap of the main points that we covered in this post.

  • Angular Animations were created by Matias Niemela and he is hash.tag.AWESOME!
  • Animations are enabled by importing the BrowserAnimationsModule into your application and then @angular/animations into your components.
  • The entry point for your animations is a trigger which is defined in your @Component definition and then added to your template with the @ symbol
  • With your trigger defined, the next step is to define a transition between states. There are pseudo-states such as :enter and :leave available for convenience.
  • You can sequence animations between children by adding the trigger to the parent element and querying the children using the query method.
  • You can use the group method run animations in parallel which is where things get REALLY interesting.

Resources

Here are some relevant resources to get you started on your next animations project.

The Source Code
The Demo
Animations FTW! Site
Angular API Document