Reputation: 2279
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
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
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) {
...
}
Upvotes: 74
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
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