Animate your Angular App using Animate.css

williamjuan27

William Juan

Posted on September 22, 2021

Animate your Angular App using Animate.css

Most of you probably have heard of Animate.css. It is an open-source CSS animation library with lots of predefined animations.

Animate.css groups their animations into categories such as entrances, exits, and attention seekers, each addressing a different use case. In addition, this library also lets us pass in flags to further customize their library of animations. These customizations include animation timing, delay, and iterations.

Instead of discussing the general usage of Animate.css, I'm going to focus on how you can use this library in your Angular application along with some tips and tricks I've learned using this library.

For those of you who are interested in the demo and going straight to the source code. You can check those out here

Installation

Add Animate.css's dependency either through the CDN or via npm. I used the CDN for this demo. Add the following code to your index.html

<head>
  ...
  <!-- Animate.css -->
  <link
    rel="stylesheet"
    href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
  />
</head>
Enter fullscreen mode Exit fullscreen mode

Use Angular's Class Binding

In a plain Javascript application, we would typically add a dynamic class by using the following code:

const element = document.querySelector(".my-element");
element.classList.add("animate__animated", "animate__bounce");
Enter fullscreen mode Exit fullscreen mode

The same could be done entirely in the template using class binding in Angular. To animate an element using Animate.css, we would need to add the class that contains the animation (see here for a list of available classes) along with any static configurations. We can then bind that class to a property that triggers the animation. For example, if we want to trigger a bounce animation when a boolean value of the variable shouldBounce is true, we would bind the animate__bounce class to shouldBounce

<div class="animate__animated" [class.animate__bounce]="shouldBounce"></div>
Enter fullscreen mode Exit fullscreen mode

Bounce and wobble animations

To customize your animations (timing, delays, iterations, etc.), you can either add these classes directly to the class property of the element if they are static or bind them like the code snippet above if they are dynamic and depend on an external factor.

Adding Javascript into the mix

The approach above works for most cases, however, it doesn't work if we want to trigger the same animation multiple times. For example, clicking on a button to execute the animation, and clicking it again to re-execute the animation.

This is by design, as Animate.css runs an animation by adding a class containing the animation, we wouldn't be able to retrigger the animation if the class is already added to the element. To work around this, we can use the animationend event to listen to when the animation is done executing, and remove the class that was added to trigger the animation. This allows us to add the classes again whenever we want to retrigger the animation.

import { ViewChild, ElementRef } from '@angular/core';

...
export class AnimateStyleComponent {

    @ViewChild('cardContainer') cardContainer: ElementRef;

    bounceCard(): void {
        this._animate('bounce').catch((e) => {
        console.error('Error animating element:', e);
        });
    }

    private _animate(animationName: string, persistClass = false): Promise<void> {
        if (!this.cardContainer || !this.cardContainer.nativeElement) {
        return Promise.reject('element not defined');
        }

        if (this.isAnimating) {
        return Promise.reject('element is animating');
        }
        return new Promise((resolve) => {
        this.isAnimating = true;

        // listen to animationend to allow additional logic to be run
        // after the animation from Animate.css is done executing
        this.cardContainer.nativeElement.addEventListener(
            'animationend',
            (event) => {
            if (event.animationName === animationName) {
                this.isAnimating = false;
                if (!persistClass) {
                this.cardContainer.nativeElement.classList = '';
                }
                resolve();
            }
            },
            { once: true }
        );
        this.cardContainer.nativeElement.classList = `animate__animated animate__${animationName}`;
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Entrance and Exits

We can also animate entrances and exits by simply attaching the class conditionally.

<div
  class="animate__animated"
  [class.animate__zoomInDown]="isShowing"
  [class.fadeOut]="!isShowing"
></div>
Enter fullscreen mode Exit fullscreen mode

Enter and exit animations

However, the above approach just animates the element in and out of the view, not necessarily removing the element from the DOM like what an *ngIf does.

Let me give you an example of what it looks like if we use an *ngIf and class binding entirely in the template.

<div
  *ngIf="isShowing"
  class="animate__animated"
  [class.animate__zoomInDown]="isShowing"
  [class.fadeOut]="!isShowing"
></div>
Enter fullscreen mode Exit fullscreen mode

The above code results in the following behavior.

Enter only animations

Notice that the element only animates in and completely ignores the exit animation. This is expected as the *ngIf directive immediately removes the element once the condition is false without waiting for any animation to complete.

You could remove the *ngIf and this animation would work. However, for certain types of animations, the element could still be left in the DOM and could be blocking the views behind it as it is not removed from the view, merely animated out.

To address this issue, we would need to approach it in a slightly different way, with a little help from Javascript.

To add and remove the element to and from the DOM, there is some additional setup we need to do. First, we will need to use an ng-template to wrap the component that we want to animate so we can add and remove it from the DOM. We'll also set the opacity to 0 to prevent the view from flashing before executing the enter animation. More on this later.

<div #container></div>
  <ng-template #template>
    <!-- set opacity to 0 to prevent flashing before enter animation starts -->
    <div #cardContainer [style.opacity]="0">
      <app-card-demo-sample></app-card-demo-sample>
    </div>
  </ng-template>
</div>
Enter fullscreen mode Exit fullscreen mode

We will then need to access the template reference and its container to conditionally add and remove the template from its parent container.

export class AnimateStyleDemoComponent {
  @ViewChild("container", { read: ViewContainerRef })
  container: ViewContainerRef;
  @ViewChild("cardContainer") cardContainer: ElementRef;
  @ViewChild("template", { read: TemplateRef }) template: TemplateRef<null>;

  private _addCardToView(): Promise<void> {
    return new Promise((resolve) => {
      if (!this.viewRef) {
        this.container.clear();

        // add element to container
        this.viewRef = this.container.createEmbeddedView(this.template);

        // wrap this in a settimeout if it tries to animate before view is loaded
        if (this.cardContainer && this.cardContainer.nativeElement) {
          // set opacity to 1 to make element visible before starting enter animation
          this.renderer.setStyle(
            this.cardContainer.nativeElement,
            "opacity",
            1
          );
        }
        resolve();
        });
      } else {
        resolve();
      }
    });
  }

  private _removeCardFromView(): void {
    this.container.clear();
    if (this.viewRef) {
      this.viewRef.destroy();
      this.viewRef = null;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

To animate the adding and removing of the element from the DOM, we will chain the adding/removing of the element and the animate function via promises.

For elements entering, add the card to view first, followed by the animate function. On the other hand, elements leaving will execute the animation first, followed by removing the card from view. We can verify this by adding console.log messages in the component being added/removed's ngOnInit and ngOnDestroy to check if these are called as the component is added and destroyed.

export class AnimateStyleDemoComponent {
  private _showCard(): void {
    this._addCardToView().then(() => {
      this._animate("zoomInDown").catch((e) => {
        console.error("Error animating element:", e);
      });
    });
  }

  private _hideCard(): void {
    this._animate("fadeOut", true)
      .then(() => {
        this._removeCardFromView();
      })
      .catch((e) => {
        console.error("Error animating element:", e);
      });
  }
}
Enter fullscreen mode Exit fullscreen mode

Enter and exit animations

Wrapping Up

That's all for now. I'll be adding more posts around different ways to add animations in Angular including Angular Animations, some cool libraries, and some tips and tricks. If you are interested in more content like this or have any questions let me know in the comments or tweet me at @williamjuan27

💖 💪 🙅 🚩
williamjuan27
William Juan

Posted on September 22, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related