Debounce @HostListener event
Asked Answered
A

4

40

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.

Armory answered 19/6, 2017 at 15:53 Comment(0)
C
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

Chelsea answered 19/6, 2017 at 16:34 Comment(2)
Thanks, worked perfecly. Now just for knowledge purposes, how can I put it in a separate file?Armory
well I know its too late, but for reference i have implemented the code here stackblitz.com/edit/…Pyrogallol
E
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.

Emmi answered 28/2, 2019 at 20:34 Comment(2)
@A.MatíasQuezada you can also use a Symbol I think.Caper
@Caper yes but Symbol is not supported by IE yet and the polyfill has some tricky caveheats (github.com/zloirock/core-js#caveats-when-using-symbol-polyfill). Let's update it when they are supported by "the brosers of the internet"!Childe
E
10

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();
Eaglewood answered 15/5, 2021 at 9:1 Comment(0)
D
0

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);
Danseuse answered 19/10, 2023 at 22:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.