Search code examples
angularangular-directiveangular-animations

Better way to Animate in Angular 8 using Directives


UPDATE: Found a much better alternative to what I was trying to code myself. See answer below.

I am using Angular's Browser Animations to animate a web application. In an attempt to animate objects as the user scrolls down the page, I figured I would create a directive that could call on the DOM for pixel reference points and use a window service to determine how far down the page the user has scrolled. I use this same window service to fade our navigation bar once the user has started scrolling down the page past a certain pixel width.

Where I am running into trouble is emitting an event from my animations directive. I would like to tie this event to a local variable on the individual element I am trying to animate

<div [appAnimateLocation]="200" [@slideFromLeft]="vendmfg" (showAnimation)="showAnimation($event, 'vendmfg')" class="container-fluid">
  <h6>Content to Animate</h6>
</div>

Currently the code works by passing a number of pixels to my directive. The directive then determines when the user has scrolled to meet certain criteria (this case, the top of the div is 200px above the bottom of the screen) and then should return true or false. Then a component method fires and explicitly sets a property to true. When this property is true, the slideFromLeft animation can fire.

My question is, is there a way to just set a local variable on every element I want to animate, without having to set explicit properties on the component? I'm talking about something like this (or something that would at least function like this)

<div [appAnimateLocation]="200" [@slideFromLeft]="var" (showAnimation)="let var as $event" class="container-fluid">
  <h6>Content to Animate</h6>
</div>

Is this possible? Or is there a better method to do this that I am overlooking? That way I could just repeat the animation portion of the div instead having to do the following (what I am currently doing):

<div [appAnimateLocation]="200" [@slideFromLeft]="vendmfg" (showAnimation)="showAnimation($event, 'vendmfg')" class="container-fluid">
  <h6>Content to Animate</h6>
</div>
<div [appAnimateLocation]="200" [@slideFromLeft]="anotherVar" (showAnimation)="showAnimation($event, 'anotherVar')" class="container-fluid">
  <h6>More Content to Animate</h6>
</div>

My files are as follows: animations.directive.ts

import {Directive, ElementRef, EventEmitter, HostListener, Inject, Input, Output} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {WINDOW} from './window.service';

@Directive({
  selector: '[appAnimateLocation]'
})
export class AppAnimateLocation {
  @Output() showAnimation = new EventEmitter();

  constructor(
    private el: ElementRef,
    @Inject(DOCUMENT) private document: Document,
    @Inject(WINDOW) private window: Window
  ){}

  @Input('appAnimateLocation') offset: number;

  @HostListener('window:scroll', []) onWindowScroll() {
    const screen = window.screen.availHeight;
    const scrollPosition = window.pageYOffset;
    const componentPosition = this.el.nativeElement.offsetTop;

    if (screen + scrollPosition >= componentPosition + this.offset) {
      this.showAnimation.emit(true);
    } else {
      this.showAnimation.emit(false);
    }

  }
}

animation.ts

import {animate, state, style, transition, trigger} from '@angular/animations';

export let fade =
  trigger('fade', [
    state('void', style({opacity: 0})),
    state('false', style({opacity: 0})),
    transition(':enter', [
      animate(1500)
    ]),
    transition('false => true', [
      animate(1500)
    ])
  ]);

windows.service

import { isPlatformBrowser } from '@angular/common';
import { ClassProvider, FactoryProvider, InjectionToken, PLATFORM_ID } from '@angular/core';

/* Create a new injection token for injecting the window into a component. */
export const WINDOW = new InjectionToken('WindowToken');

/* Define abstract class for obtaining reference to the global window object. */
export abstract class WindowRef {

  get nativeWindow(): Window | Object {
    throw new Error('Not implemented.');
  }

}

/* Define class that implements the abstract class and returns the native window object. */
export class BrowserWindowRef extends WindowRef {

  constructor() {
    super();
  }

  get nativeWindow(): Window | Object {
    return window;
  }

}

/* Create an factory function that returns the native window object. */
export function windowFactory(browserWindowRef: BrowserWindowRef, platformId: Object): Window | Object {
  if (isPlatformBrowser(platformId)) {
    return browserWindowRef.nativeWindow;
  }
  return new Object();
}

/* Create a injectable provider for the WindowRef token that uses the BrowserWindowRef class. */
export const browserWindowProvider: ClassProvider = {
  provide: WindowRef,
  useClass: BrowserWindowRef
};

/* Create an injectable provider that uses the windowFactory function for returning the native window object. */
export const windowProvider: FactoryProvider = {
  provide: WINDOW,
  useFactory: windowFactory,
  deps: [ WindowRef, PLATFORM_ID ]
};

/* Create an array of providers. */
export const WINDOW_PROVIDERS = [
  browserWindowProvider,
  windowProvider
];

and finally my component

import {Component, OnInit} from '@angular/core';
import {Router} from '@angular/router';
import {fade, slideFromLeft, slideFromRight} from '../animations';

@Component({
  selector: 'app-home-page',
  templateUrl: './home-page.component.html',
  styleUrls: ['./home-page.component.css'],
  animations: [
    fade,
    slideFromRight,
    slideFromLeft,
  ]
})
export class HomePageComponent implements OnInit {

  constructor(
    private router: Router
  ) { }

  vendmfg = false;

  ngOnInit() {
  }

  showAnimation(event, el: string){
    if(event === true) {
      if (el === 'vendmfg') {
        this.vendmfg = true;
      } 
    }
  }

}

Any help or advice is greatly appreciated.


Solution

  • As I continued to look into this, and trying to figure out how to animate when scrolling back up a page, I came across this stackblitz by Lucio Francisco which does what I was attempting to do in a way more concise manner. Explained by this Medium article. I hope that helps anyone else who is new to animations.