DEV Community

Nguyễn Hữu Hiếu
Nguyễn Hữu Hiếu

Posted on

 

angular animation: how to run animation after component visible in the viewport

References

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

// 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();
    }
  }
}

Enter fullscreen mode Exit fullscreen mode
// 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;
  }
}
Enter fullscreen mode Exit fullscreen mode
// 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)' })),
];
Enter fullscreen mode Exit fullscreen mode
// animations/animations/index.ts
export * from './fade';
Enter fullscreen mode Exit fullscreen mode

Top comments (0)

An Animated Guide to Node.js Event Loop

Node.js doesn’t stop from running other operations because of Libuv, a C++ library responsible for the event loop and asynchronously handling tasks such as network requests, DNS resolution, file system operations, data encryption, etc.

What happens under the hood when Node.js works on tasks such as database queries? We will explore it by following this piece of code step by step.