Search code examples
javascripthtmlangulartypescriptgsap

Smooth scroll throws unexpected undefined property error


I've been trying to implement some kind of smooth scroll. I followed this codepen to create such scroll: https://codepen.io/osublake/pen/QqPqbN

I tried to translate into TypeScript/Angular, but it somehow still throws the following error - not when updateScroller() is being called inside of ngAfterViewInit, but when I try to scroll for the first time - pointing at const resized = this.scroller.resizeRequest > 0:

core.js:9110 ERROR TypeError: Cannot read property 'scroller' of undefined
    at updateScroller (app.component.ts:50)
    at ZoneDelegate.invokeTask (zone-evergreen.js:391)
    at Object.onInvokeTask (core.js:34182)
    at ZoneDelegate.invokeTask (zone-evergreen.js:390)
    at Zone.runTask (zone-evergreen.js:168)
    at invokeTask (zone-evergreen.js:465)
    at ZoneTask.invoke (zone-evergreen.js:454)
    at timer (zone-evergreen.js:2650)
export class AppComponent implements OnInit, AfterViewInit {

  scroller = {
    target: document.querySelector('.scrollable-container'),
    ease: 0.05,
    endY: 0,
    y: 0,
    resizeRequest: 1,
    scrollRequest: 0,
  };

  requestId = null;

  constructor(@Inject(PLATFORM_ID) private platformId) {
  }

  ngOnInit() {
    if (isPlatformBrowser(this.platformId)) {
      console.log('Hi, this is NØREBRO!');
    }
  }

  ngAfterViewInit(): void {
    setTimeout(() => {
      this.scroller.target = document.querySelector('.scrollable-container');

      TweenLite.set(this.scroller.target, {
        rotation: 0.01,
        force3D: true
      });

      this.updateScroller();
    });
    window.focus();
  }

  updateScroller() {
    const html = document.documentElement;
    const body = document.body;

    const resized = this.scroller.resizeRequest > 0;

    if (resized) {
      const height = this.scroller.target.clientHeight;
      body.style.height = height + 'px';
      this.scroller.resizeRequest = 0;
    }

    const scrollY = window.pageYOffset || html.scrollTop || body.scrollTop || 0;

    this.scroller.endY = scrollY;
    this.scroller.y += (scrollY - this.scroller.y) * this.scroller.ease;

    if (Math.abs(scrollY - this.scroller.y) < 0.05 || resized) {
      this.scroller.y = scrollY;
      this.scroller.scrollRequest = 0;
    }

    TweenLite.set(this.scroller.target, {
      y: -this.scroller.y
    });

    this.requestId = this.scroller.scrollRequest > 0 ? requestAnimationFrame(this.updateScroller) : null;
  }

  @HostListener('window:scroll', [])
  scroll() {
    this.scroller.scrollRequest++;
    console.log(this.requestId);
    if (!this.requestId) {
      console.log('uff');
      this.requestId = requestAnimationFrame(this.updateScroller);
    }
  }

}

Solution

  • It seems that this is undefined by repeated method calls. Without being able to debug your application, I assume that you need to add this as a scope to your this.updateScroller method.

    The scope (this) of a function is always determined by the execution context and with requestAnimationFrame the function is called outside the context of your class instance (component). There are several interesting articles about function scopes, e.g. see this.

    There should be two ways to solve your problem:

    a) Replace all requestAnimationFrame(this.updateScroller) calls with

    requestAnimationFrame(this.updateScroller.bind(this))
    

    b) Replace all requestAnimationFrame(this.updateScroller) calls with

    requestAnimationFrame(() => {
      this.updateScroller()
    })