Using component methods is generally unwise for known reasons, yet sometimes an application can have third party components that require using component methods in template to react to outputs.
In today's article we are going to explore a way of debouncing component's method execution, so it does not fire too often, without resolving to rxjs tools.
For the sake of simplicity we will be using a simple component that implements a counter.
import { Component } from '@angular/core';
@Component({
template: `<p>{{ calledTimes }}</p>
<p><button (click)="increment()">increment</button></p>`,
selector: 'app-root',
})
export class AppComponent {
private _calledTimes = 0;
public get calledTimes(): number {
return this._calledTimes;
}
public increment(): void {
this._calledTimes++;
}
}
Everything is rather straightforward: there is a property that is incremented each time a handler in template is called. To show things down we could use rxjs' Subject
s with debounceTime
, but such solution would be limited to a component that implements it and not transportable to other components which might need debouncing their method calls.
What we can do as an alternative, is to create a parameterized decorator which would accept a debounce time in milliseconds and utilize Promise
to postpone method execution.
export function debounce(ms: number) {
return (
target: unknown,
propertyKey: string,
descriptor: PropertyDescriptor
): void => {
let timer: ReturnType<typeof setTimeout>;
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
new Promise((resolve) => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
resolve(originalMethod.apply(this, ...args));
}, ms);
});
};
};
}
What is cool about this approach is that now we have a separate debouncing mechanism which we can apply to any method of any component. So if we wanted to limit increment to 2 seconds, we could simply decorate the method:
import { Component } from '@angular/core';
import { debounce } from './debounce/debounce.decorator';
@Component({
template: `<p>{{ calledTimes }}</p>
<p><button (click)="increment()">increment</button></p>`,
selector: 'app-root',
})
export class AppComponent {
private _calledTimes = 0;
public get calledTimes(): number {
return this._calledTimes;
}
@debounce(2000)
public increment(): void {
this._calledTimes++;
}
}
Simple as that :)
Top comments (4)
Warning : you forgot a return in front of descriptor.value ...
It was deliberate actually: had there been a return, that would change the type of the decorated method to async, which is something IDE would not reflect in intellisense, so this debouncing approach is only viable when decorating functions with void returns :)
Unless I am mistaken and IDEs follow changed return type of decorated methods.
Sorry, I just tested again. Try to call debounce on a function which as an argument. It will be lost and a bug will occur : cannot read property of undefined.
Following is working :
};
}
Good point π