Victor Ivens
Victor Ivens

Reputation: 2279

Debounce @HostListener event

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.

Upvotes: 40

Views: 23556

Answers (4)

Max
Max

Reputation: 1844

For those who use lodash in their project you can use this:

import { debounce } from 'lodash-es';
...
@HostListener('window:scroll') onScroll = debounce(() => {
  ...
}, 300);

or if you need throttle

import { throttle } from 'lodash-es';
...
@HostListener('window:scroll') onScroll = throttle(() => {
  ...
}, 300);

Upvotes: 0

yurzui
yurzui

Reputation: 214017

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

Upvotes: 74

Marten S
Marten S

Reputation: 495

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();

Upvotes: 10

Mark Florence
Mark Florence

Reputation: 363

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.

Upvotes: 13

Related Questions