DEV Community

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

Posted on

3 2

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

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay