9

I've built a loading spinner component in Angular 2 that I'd like to trigger before http requests are made and disable when they're done. The problem is that each time the user changes an input (checks of of a box, types into the input box), the http request fires. This means a lot of requests, and the overlay is coming up constantly. I'd like to wait a set period of time (half a second?) after an input is triggered before triggering the http request, giving the user time to put in other inputs. I've read up a bit on debounce, but as far as I can see, that's for time waiting before making another request? And as far as I can see, that's just a buffer time between requests.

Basically, right now, I have a component that handles my inputs. When an input is changed (checkboxes right now), the following code is triggered:

@Output() filtersChanged = new EventEmitter();
emitFilters(): void {
    this.filtersChanged.emit(this.filters);
}

Which through an intermediary step, sets off my http request:

getEvents(filters): Observable<Event[]> {
    this.loadingSpinnerService.showLoadingSpinner();
    let params: URLSearchParams = new URLSearchParams();
    params.set('types', filters.types.join(','));
    params.set('dates', filters.dates.join(','));
    return this.http
        .get('//api.dexcon.local/getEvents.php', { search: params })
        .map((response: Response) => {
            return response.json().events;
        });
}

In Angular 1, I would have put it in a timeout which refreshed each time a user affected an input, so it'd trigger a set time after the final input was touched. Is this the best way to do it in Angular 2 as well? From my reading, debounce locks out a request from happening too close to a second request, but I'm wondering how to best prevent a request from happening after an action is taken for a given period of time.

ggorlen
  • 44,755
  • 7
  • 76
  • 106
Rohit
  • 3,018
  • 2
  • 29
  • 58
  • Please post the code that demonstrates what you try to accomplish, what you tried, and where you failed. – Günter Zöchbauer Feb 04 '17 at 15:08
  • I have no code specific to the throttling, I'm asking about theory, how it's supposed to be done. I haven't written code, because I can't figure out what methodology is appropriate here. Should I show a set timeout function from my Angular 1 code? Or should I just put up the code I have so far with Angular 2? – Rohit Feb 04 '17 at 15:11
  • SO question are supposed to demonstrate your effort. Perhaps http://stackoverflow.com/questions/32051273/angular2-and-debounce – Günter Zöchbauer Feb 04 '17 at 15:13
  • I'll put up my code, though I've seen and utilized tons of SO questions that showed no code and where simply about the right way/wrong way to do things, so I didn't think it strange to ask one myself. I saw that question before, but it didn't really address what I'm trying to do, unless I don't understand what it's done either. I can show the code I have now, but I can't show effort of me searching and failing to understand how to do this? – Rohit Feb 04 '17 at 15:16
  • Sure there are ton of mediocre or bad questions. Many of them don't get good answers for good reasons ;-) – Günter Zöchbauer Feb 04 '17 at 15:18
  • See [difference between throttling and debouncing a function](https://stackoverflow.com/q/25991367/6243352). I'm changing the title to "debouncing" because that's what you're really trying to accomplish here so web searchers can more easily find what they need. – ggorlen Dec 29 '20 at 16:35
  • Does this answer your question? [Angular and debounce](https://stackoverflow.com/questions/32051273/angular-and-debounce) – ggorlen Dec 29 '20 at 16:40

2 Answers2

6

The easiest way to accomplish what you're after is to set up a Subject (I'm assuming you have access to Rxjs here). Initialize one in your component:

inputSubject: Subject<string> = new Subject<string>();

Since a Subject is both an observer and an observable, you're going to want to set up a subscription to listen for changes. Here's where you can apply your debounce.

this.subscription = this.inputSubject.asObservable()
    .debounceTime(1000)
    .subscribe(x => this.filtersChanged.emit(this.filters));

Now, in your emitFilters() function instead of directly emitting, push the value onto the Subject.

this.inputSubject.next(newValue);

Don't forget to store your subscription as a member of your component class and dispose of it properly in your OnDestroy().

ngOnDestroy() {
    if (this.subscription) {
      this.subscription.unsubscribe();
    }
}
Jesse Carter
  • 20,062
  • 7
  • 64
  • 101
  • @RhoVisions Any thoughts on whether or not this will work out for your scenario? – Jesse Carter Feb 04 '17 at 21:50
  • I considered observables, but I wasn't sure, mostly because maybe I'm not understanding how debounce works? Does it do a "wait", or is a break between pushes? What I'm hoping to do is when a user hits an input, wait x time, if another input is hit, refresh the x timer, if not, do action. Does that make sense? – Rohit Feb 05 '17 at 04:14
  • Tested, and while there's still a lot more for me to figure out/learn on this, it worked. – Rohit Feb 05 '17 at 19:00
  • What you're describing is exactly what debounce does under the scenes. I'd definitely recommend that you embrace Observables and Rxjs if you're going to be working with Angular 2 they go together perfectly. Here's another example of leveraging Subjects directly from the Angular 2 cookbook for optimal component interaction through an intermediary service: https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#bidirectional-service – Jesse Carter Feb 05 '17 at 19:28
  • Thanks. I don't find myself scared of Observables, and they're definitely really useful, I'm just finding documentation less than stellar and few, easy-to-understand examples out there. Thanks for the notes. – Rohit Feb 05 '17 at 23:54
0

You should be able to solve this by decorating your getEvents method with a debounce function:

    function debounce(ms: number) {

        let timeoutId;

        return function (target: Object, propName: string, descriptor: TypedPropertyDescriptor<any>) {
            let originalMethod = descriptor.value;
            descriptor.value = function (...args: any[]) {
                if (timeoutId) return;
                timeoutId = window.setTimeout(() => {
                    timeoutId = null;
                }, ms);
                return originalMethod.apply(this, args);
            }
        }
    }

And then simply apply it to your method like this (set the decorator parameter to your desired debounce time in ms):

@debounce(300)
getEvents(filters): Observable < Event[] > {
    ...
}
Fredrik_Borgstrom
  • 2,504
  • 25
  • 32
  • I believe the return needs to be inside of the timeout. – Hanna Apr 19 '18 at 20:59
  • 1
    At first look glance, one would easily think so. And I think I could have written the code slightly cleaner. But, it is actually correct according to my intentions. Calling the decorated function, in this case getEvents, will run and return the result of getEvents. If the next call to getEvents is within the time frame defined by ms, then it will return void. Basically, it depends on which functionality you want. One could put the apply statement inside the timeout, but the way I've done it, any calls within the debouncing time are disregarded instead of fired at a later time. – Fredrik_Borgstrom Apr 23 '18 at 11:38