Angular 4 - Scroll Animation

Viewed 42005

I'm creating a webpage having full page width/height div's. While scrolling down I've two types of methods.

Scroll on Click

//HTML
<a (click)="goToDiv('about')"></a>

//JS
    goToDiv(id) {
        let element = document.querySelector("#"+id);
        element.scrollIntoView(element);
      }

Scroll on HostListener

  @HostListener("window:scroll", ['$event'])
  onWindowScroll($event: any): void {
    this.topOffSet = window.pageYOffset;
    //window.scrollTo(0, this.topOffSet+662);
  }

1. How to add a scrolling animation effects?

Just like :

$('.scroll').on('click', function(e) {
    $('html, body').animate({
        scrollTop: $(window).height()
    }, 1200);
});

2. And how to use HostListener to scroll to next div?

4 Answers

I spent days trying to figure this out. Being a newbie I tried many things and none of them work. Finally, I have a solution so I will post it here.

There are 2 steps:

  1. Animate when things appear.
  2. Make things appear when scrolling.

Part 1: I found out these two great tutorials for newbies:

  1. The most basic one
  2. The one that actually animates when stuff appears

Part 2: I simply find the solution in this answer


Part 1 Step by Step:

  1. Add the line import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; to /src/app/app.module.ts and then also:
@NgModule({
  // Other arrays removed
  imports: [
    // Other imports
    BrowserAnimationsModule
  ],
})
  1. In the component.ts you want to animate, add: import { trigger,state,style,transition,animate } from '@angular/animations'; And then:
@Component({
  // Here goes the selector and templates and etc.
  animations: [
    trigger('fadeInOut', [
      state('void', style({
        opacity: 0
      })),
      transition('void <=> *', animate(1000)),
    ]),
  ]
})
  1. Finally, in the HTML item you want to animate, add [@fadeInOut].

If everything was done correctly, you should now have an animation (but it happens as soon as the webpage loads and not when you scroll.

Part 2 Step by Step:

  1. Create a file .ts like for example appear.ts and copy-paste this code:
import {
    ElementRef, Output, Directive, AfterViewInit, OnDestroy, EventEmitter
  } from '@angular/core';
  import { Observable, Subscription, fromEvent } from 'rxjs';
  import { startWith } from 'rxjs/operators';
  //import 'rxjs/add/observable/fromEvent';
  //import 'rxjs/add/operator/startWith';



  @Directive({
    selector: '[appear]'
  })
  export class AppearDirective implements AfterViewInit, OnDestroy {
    @Output()
    appear: EventEmitter<void>;

    elementPos: number;
    elementHeight: number;

    scrollPos: number;
    windowHeight: number;

    subscriptionScroll: Subscription;
    subscriptionResize: Subscription;

    constructor(private element: ElementRef){
      this.appear = new EventEmitter<void>();
    }

    saveDimensions() {
      this.elementPos = this.getOffsetTop(this.element.nativeElement);
      this.elementHeight = this.element.nativeElement.offsetHeight;
      this.windowHeight = window.innerHeight;
    }
    saveScrollPos() {
      this.scrollPos = window.scrollY;
    }
    getOffsetTop(element: any){
      let offsetTop = element.offsetTop || 0;
      if(element.offsetParent){
        offsetTop += this.getOffsetTop(element.offsetParent);
      }
      return offsetTop;
    }
    checkVisibility(){
      if(this.isVisible()){
        // double check dimensions (due to async loaded contents, e.g. images)
        this.saveDimensions();
        if(this.isVisible()){
          this.unsubscribe();
          this.appear.emit();
        }
      }
    }
    isVisible(){
      return this.scrollPos >= this.elementPos || (this.scrollPos + this.windowHeight) >= (this.elementPos + this.elementHeight);
    }

    subscribe(){
      this.subscriptionScroll = fromEvent(window, 'scroll').pipe(startWith(null))
        .subscribe(() => {
          this.saveScrollPos();
          this.checkVisibility();
        });
      this.subscriptionResize = fromEvent(window, 'resize').pipe(startWith(null))
        .subscribe(() => {
          this.saveDimensions();
          this.checkVisibility();
        });
    }
    unsubscribe(){
      if(this.subscriptionScroll){
        this.subscriptionScroll.unsubscribe();
      }
      if(this.subscriptionResize){
        this.subscriptionResize.unsubscribe();
      }
    }

    ngAfterViewInit(){
      this.subscribe();
    }
    ngOnDestroy(){
      this.unsubscribe();
    }
  }
  1. Import it using import {AppearDirective} from './timeline/appear';and add it to the imports as:
@NgModule({
  declarations: [
    // Other declarations
    AppearDirective
  ],
  // Imports and stuff
  1. Somewhere in the class do:
hasAppeared : boolean = false;
onAppear(){
    this.hasAppeared = true;
    console.log("I have appeared!");   // This is a good idea for debugging
  }
  1. Finally, in the HTML add the two following:
(appear)="onAppear()" *ngIf="hasAppeared" 

You can check this is working by checking the console for the message "I have appeared!".

The @bryan60 answer works, but I was not comfortable with it, and I preferred to use TimerObservable which seems less confusing for other teammates and also easier to customize for future uses.

I suggest you have a shared service for times you're touching DOM, or working with scroll and other HTML element related issues; Then you can have this method on that service (otherwise having it on a component does not make any problem)

  // Choose the target element (see the HTML code bellow):
  @ViewChild('myElement') myElement: ElementRef;

  this.scrollAnimateAvailable:boolean;

animateScrollTo(target: ElementRef) {
    if (this.helperService.isBrowser()) {
      this.scrollAnimateAvailable = true;
      TimerObservable
        .create(0, 20).pipe(
        takeWhile(() => this.scrollAnimateAvailable)).subscribe((e) => {
        if (window.pageYOffset >= target.nativeElement.offsetTop) {
          window.scrollTo(0, window.pageYOffset - e);
        } else if (window.pageYOffset <= target.nativeElement.offsetTop) {
          window.scrollTo(0, window.pageYOffset + e);
        }

        if (window.pageYOffset + 30 > target.nativeElement.offsetTop && window.pageYOffset - 30 < target.nativeElement.offsetTop) {
          this.scrollAnimateAvailable = false;
        }

      });
    }

  }



 scrollToMyElement(){
   this.animateScrollTo(this.myElement)
  }

You need to pass the element to this method, here is how you can do it:

<a (click)="scrollToMyElement()"></a>
<!-- Lots of things here... -->
<div #myElement></div>
Related