angular animation: how to run animation after component visible in the viewport
Tags: angular, animation, css
Published: 2022-01-19 10:45:47

## References
- [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)

- [Angular Animation](https://angular.io/guide/animations)

- [NGX Animate In](https://github.com/Epenance/ngx-animate-in)

## Scenario
- When I create landing page with angular 2+, animation is run immediately even component is not show. After contact with google =)), well, I found that is normal case we meet went using css animation. This is why I create this post. Enjoy!

- Requirement: Read about intersection observer api and angular animation above.

- Thank @Epenance because he creates the lib NGX Animate In. I took a reference from his code and created this post with some of my own opinions and my scenario.

## Implement Code

```ts
// animations/directives/animate-after-appear.directive.ts
import { Directive, Input, ElementRef, OnInit } from '@angular/core';
import {
  animate,
  AnimationBuilder,
  AnimationFactory,
  AnimationMetadata,
  AnimationPlayer,
  style,
} from '@angular/animations';
import { IntersectionObserverService } from '../services/intersection-observer.service';
import * as buildInAnmiations from '../animations';

@Directive({
  selector: '[animateAfterAppear]',
})
export class AnimateAfterAppearDirective implements OnInit {
  @Input() animateAfterAppear: 'fadeIn' | 'fadeInDown';
  @Input() animationOptions: any; // custom your own animations

  player?: AnimationPlayer;

  constructor(
    private _observer: IntersectionObserverService,
    private el: ElementRef,
    private animationBuilder: AnimationBuilder
  ) {}

  ngOnInit() {
    let animation: AnimationFactory;

    if (
      !!this.animationOptions !== null &&
      this.animationOptions !== undefined
    ) {
      animation = this.animationBuilder.build(this.animationOptions);
    }
    if (
      !!this.animateAfterAppear &&
      !!buildInAnmiations[this.animateAfterAppear]
    ) {
      console.log('build in', this.animateAfterAppear);
      animation = this.animationBuilder.build(
        buildInAnmiations[this.animateAfterAppear]
      );
    } else {
      animation = this.animationBuilder.build([
        style({ opacity: 0, transform: 'translateX(-100px)' }),
        animate(
          '1200ms cubic-bezier(0.35, 0, 0.25, 1)',
          style({ opacity: 1, transform: 'translateX(0)' })
        ),
      ]);
    }

    if (this._observer.isSupported()) {
      this.player = animation.create(this.el.nativeElement);
      this.player.init();

      const callback = this.startAnimating.bind(this);
      this._observer.addTarget(this.el.nativeElement, callback);
    }
  }

  /**
   * Builds and triggers the animation
   * when it enters the viewport
   * @param {boolean} inViewport
   */
  startAnimating(inViewport?: boolean, element?: Element): void {
    console.log('start animating');
    if (inViewport) {
      this.player?.play();
    }
  }
}

```

```ts
// animations/services/intersection-observer.service.ts
import { Injectable, Optional } from '@angular/core';

export class IntersectionObserverServiceConfig {
  root?: Element | null;
  rootMargin?: string;
  threshold?: number | number[];
}

export type CallbackType = (inViewport?: boolean, element?: Element) => void;

export interface WatchedItem {
  element: Element;
  callback: CallbackType;
}

@Injectable()
export class IntersectionObserverService {
  options: IntersectionObserverServiceConfig = {
    rootMargin: '0px',
    threshold: 0.1,
  };

  // where Intersection is support
  supported = false;

  watching: Array<WatchedItem> = [];

  observer: IntersectionObserver | null;

  /**
   * Assigns the user config if they wish to
   * override the defaults by using forRoot
   * @param {IntersectionObserverServiceConfig} config
   */
  constructor(@Optional() config: IntersectionObserverServiceConfig) {
    this.supported =
      'IntersectionObserver' in window && 'IntersectionObserverEntry' in window;

    if (config) {
      this.options = { ...this.options, ...config };
    }

    this.observer = this.supported
      ? new IntersectionObserver(this.handleEvent.bind(this), this.options)
      : null;
  }

  /**
   * Handles events made by the observer
   * @param {IntersectionObserverEntry[]} entries
   */
  handleEvent(entries: IntersectionObserverEntry[]): void {
    entries.forEach((entry: IntersectionObserverEntry) => {
      const target = this.watching.find((element) => {
        return element.element === entry.target;
      });

      if (entry.isIntersecting) {
        // un observe after intersecting
        this.observer?.unobserve(entry.target);

        // callback
        target?.callback(true, entry.target);

        // remove item in watching list
        this.watching = this.watching.filter(
          (element) => element.element !== entry.target
        );
      }
    });
  }

  /**
   * Adds the target to our array so we can call its
   * call back when it enters the viewport
   * @param {Element} element
   * @param {CallbackType} callback
   */
  addTarget(element: Element, callback: CallbackType): void {
    this.observer?.observe(element);

    this.watching.push({
      element: element,
      callback: callback,
    });
  }

  isSupported() {
    return this.supported;
  }
}
```

```ts
// animations/animations/fade.ts
import { animate, AnimationMetadata, state, style } from '@angular/animations';

export const fadeIn: AnimationMetadata[] = [
  style({ opacity: 0 }),
  animate('1000ms', style({ opacity: 1 })),
];

export const fadeInDown: AnimationMetadata[] = [
  style({ opacity: 0, transform: 'translate3d(0, -20%, 0)' }),
  animate('500ms', style({ opacity: 1, transform: 'translate3d(0, 0, 0)' })),
];

export const fadeInUp: AnimationMetadata[] = [
  style({ opacity: 0, transform: 'translate3d(0, 20%, 0)' }),
  animate('500ms', style({ opacity: 1, transform: 'translate3d(0, 0, 0)' })),
];

export const fadeInLeft: AnimationMetadata[] = [
  style({ opacity: 0, transform: 'translate3d(-10%, 0, 0)' }),
  animate('500ms', style({ opacity: 1, transform: 'translate3d(0, 0, 0)' })),
];

export const fadeInRight: AnimationMetadata[] = [
  style({ opacity: 0, transform: 'translate3d(10%, 0, 0)' }),
  animate('500ms', style({ opacity: 1, transform: 'translate3d(0, 0, 0)' })),
];
```

```ts
// animations/animations/index.ts
export * from './fade';
```