39

I'm implementing a simple infinite-scroll directive in Angular2. I'm using @HostListener('window:scroll') to get the scroll event and parsing the data from the $target.

The question is, for every scroll event, everything will be checked once again with no need.

I checked the ionic infinite-scroll directive for inspiration but they don't use @HostListener, they need a more granular control, I guess.

I ended up on this issue while searching https://github.com/angular/angular/issues/13248 but couldn't find any way to do what I want.

I think if I create an Observable, subscribe to it with debounce and push (next) items to it, I will reach the behaviour I want, but I'm not being able to do that.

Sunil Garg
  • 14,608
  • 25
  • 132
  • 189
Victor Ivens
  • 2,221
  • 2
  • 22
  • 32

3 Answers3

74

I would leverage debounce method decorator like:

export function debounce(delay: number = 300): MethodDecorator {
  return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
    const timeoutKey = Symbol();

    const original = descriptor.value;

    descriptor.value = function (...args) {
      clearTimeout(this[timeoutKey]);
      this[timeoutKey] = setTimeout(() => original.apply(this, args), delay);
    };

    return descriptor;
  };
}

and use it as follows:

@HostListener('window:scroll', ['$event'])  
@debounce() 
scroll(event) {
  ...
}

Ng-run Example

Monfa.red
  • 4,532
  • 1
  • 12
  • 14
yurzui
  • 205,937
  • 32
  • 433
  • 399
  • Thanks, worked perfecly. Now just for knowledge purposes, how can I put it in a separate file? – Victor Ivens Jun 19 '17 at 17:49
  • well I know its too late, but for reference i have implemented the code here https://stackblitz.com/edit/angular-ivy-debounce?file=src%2Fapp%2Fdebounce-example%2Fdebounce-example.component.html – Bergin Jun 09 '21 at 12:47
13

I really like @yurzui's solution and I updated a lot of code to use it. However, I think it contains a bug. In the original code, there is only one timeout per class but in practice one is needed per instance.

In Angular terms, this means that if the component in which @debounce() is used is instantiated multiple times in a container, every instantiation will cancelTimeout the previous instantiation and only the last will fire.

I propose this slight variant to eliminate this trouble:

export function debounce(delay: number = 300): MethodDecorator {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {

    const original = descriptor.value;
    const key = `__timeout__${propertyKey}`;

    descriptor.value = function (...args) {
      clearTimeout(this[key]);
      this[key] = setTimeout(() => original.apply(this, args), delay);
    };

    return descriptor;
  };
}

Of course, it is possible to be more sophisticated about disambiguating the synthetic __timeout__ property.

A. Matías Quezada
  • 1,886
  • 17
  • 34
Mark Florence
  • 363
  • 4
  • 8
  • 1
    @A.MatíasQuezada you can also use a Symbol I think. – LppEdd Jun 04 '19 at 21:40
  • @LppEdd yes but Symbol is not supported by IE yet and the polyfill has some tricky caveheats (https://github.com/zloirock/core-js#caveats-when-using-symbol-polyfill). Let's update it when they are supported by "the brosers of the internet"! – A. Matías Quezada Jun 05 '19 at 23:49
8

An RXJS way of doing this can be achieved using fromEvent together with the throttleTime operator.

Instead of decorating your event handler with @HostListener, you create an observable from the event using fromEvent (e.g., in the ngOnInit method) and then throttling the emission of events using throttleTime.

...
import {fromEvent, Subscription} from 'rxjs';
import {tap, throttleTime} from 'rxjs/operators';


export class MyComponent implements OnInit, OnDestroy { 

  private eventSub: Subscription;

  ngOnInit() {
    this.eventSub = fromEvent(window, 'scroll').pipe(
      throttleTime(300), // emits once, then ignores subsequent emissions for 300ms, repeat...
      tap(event => this.scroll(event))
    ).subscribe();
  }

  scroll(event) {
    ...
  }

  ngOnDestroy() {
    this.eventSub.unsubscribe(); // don't forget to unsubscribe
  }
}

One advantage of using RXJS is that you can pass in custom schedulers to the throttleTime operator to achieve different behaviours. For example, you can throttle event emission by the animation frame rate (e.g., to throttle the emission of touch events).

import {animationFrameScheduler, ...} from 'rxjs';
...

this.eventSub = fromEvent(window, 'touchmove').pipe(
  throttleTime(0, animationFrameScheduler),
  tap(event => ...)
).subscribe();
Marten S
  • 482
  • 7
  • 8