0
votes

In my Angular app I have a function that takes in some user-selected filters and then returns filtered data based on those filter values. If there are no filters applied, then the unfiltered data is returned.

The filter values are passed from one component to another via Angular's EventEmitter(), so in the receiving component template, I have this:

<div class="page-content">
    <grid [records]="records"
        (sendFilterValues)="onFilterReceived($event)">
    </grid>
</div>

The problem I'm running into (and I've been troubleshooting this for days now) is that because the filters come in via an EventEmitter() from another component, this causes the function that received these selections and then makes the network call to fire multiple times.

The console.log statement in the middle of the function specifically tells me the filters coming into this function cause it to fire 10 times.

What ends up happening therefore is, to the user, they see the data shift several times in the view, as multiple calls go out -- some for the unfiltered data, and some for the filtered data. I want to control this so only one network request is made.

So what I'm trying to do, since this is an observable, is use some kind of RxJS operator to make just one call. However, I can't seem to get it to work. As far as I can tell the operator I'm applying isn't having any effect.

Here is my code:

import { last } from 'rxjs/operators';

public onFilterReceived(values)
{
    let filters = {};

    if (values) {
        filters = values;
    }

    this.route.params.subscribe(
        (params: any) => {
            this.page = params['page'];
        }
    );

    let fn = resRecordsData => {
        this.records = resRecordsData;
    };

    console.count('onFilterReceived() is firing'); // fires 10 times

    this.streamFiltersService.getByFilters(
        this.page - 1, this.pagesize, this.currentStage, this.language = filters['language'], this.location = filters['location'],
        this.zipcode = filters['zip'], this.firstName = filters['firstName'], this.lastName = filters['lastName'],
        this.branch = filters['branch'], fn).last;
} 

While I have the import in my component, my IDE is suggesting it's never actually read. However, when I add the . immediately after the code below, like so:

this.streamFiltersService.getByFilters(
    this.page - 1, this.pagesize, this.currentStage, this.language = filters['language'], this.location = filters['location'],
    this.zipcode = filters['zip'], this.firstName = filters['firstName'], this.lastName = filters['lastName'],
    this.branch = filters['branch'], fn).

... various RxJS operators show up as options, including the last operator.

By the way, this 10 times firing goes back to the originating filter component, where my console.log from below shows me this fires 10 times -- and this gets passed on to the receiving component. If there's another way I can control this to only emit once, that would also likely solve my problem. Here's that code:

async onFilterValuesReceived({ 'zip' : zipArray, 'firstName' : firstName,
'lastName' : lastName, 'language' : language, 'location' : location, 'branch' : branch })
{
    let filtersObj = { 'zip' : zipArray, 'firstName' : firstName,
    'lastName' : lastName, 'language' : language, 'location' : location, 'branch' : branch };

    this.sendFilterValues.emit(await filtersObj);

    console.count('sending these...'); // fires 10 times
}

So, if that's the case, why is this operator having no effect here? Am I using the wrong operator, or using it in the wrong way? Or is something else at issue? I get zero errors pertaining to the use of these operators, but neither do I see them having any effect on the execution of the function.

By the way, this is what the code looks like for the network call in the service layer:

public getByFilters(page, pagesize, stage?, language?, location?, zipcode?, firstName?, lastName?, branch?, fn?: any)
{
    return this.apiService.get({
      req: this.strReq, reqArgs: { page, pagesize, stage, language, location, zipcode, firstName, lastName, branch }, callback: fn });
}

... which in turn calls this from the root service layer:

public get(args: {
    req: string,
    reqArgs?: any,
    reqBody?: any,
    callback?: IRequestCallback
}): Observable<Response>
{
    args['reqType'] = 'get';
    return this.runHttpRequest(args);
}

Is the structure here at the API service level the problem? We're using this on all of our GET requests of this type. When I try and use the takeLast operator here I get a TS error:

[ts] Type '(this: Observable, count: number) => Observable' is not assignable to type 'Observable'. Property '_isScalar' is missing in type '(this: Observable, count: number) => Observable'.

I have been racking my brain on this issue for days and days now and would appreciate any input as to how I can resolve it.

I'm also completely open to a non-RxJS option at this point, if there's another way this can be solved.

2

2 Answers

0
votes

Since you say you're open to non-RxJS options, I'll offer up debounce for your consideration. In short, if you debounce the request, it will only fire once after the events have stopped triggering.

There are many libraries that allow this, but I will show an example with lodash - https://lodash.com/docs/4.17.10#debounce

import * as _ from "lodash";

private sendRequest = _.debounce(() => {
   this.streamFiltersService.getByFilters(
        this.page - 1, this.pagesize, this.currentStage, this.language = filters['language'], this.location = filters['location'],
        this.zipcode = filters['zip'], this.firstName = filters['firstName'], this.lastName = filters['lastName'],
        this.branch = filters['branch'], 
        (resRecordsData) => {
           this.records = resRecordsData;
        })
}, 200);

public onFilterReceived(values)
{
    let filters = {};

    if (values) {
        filters = values;
    }

    this.route.params.subscribe(
        (params: any) => {
            this.page = params['page'];
        }
    );

    console.count('onFilterReceived() is firing'); // fires 10 times

    this.sendRequest();
} 

Only once 200 ms have passed since the last time sendRequest was called will the request actually execute.

0
votes

As another answer mention, you need a debounce. To accomplish this using RxJS use debounceTime() operator inside observable's pipe:

this.streamFiltersService.getByFilters(
    this.page - 1,
    this.pagesize,
    ...
).pipe(debounceTime(200)).subscribe((resRecordsData) => this.records = resRecordsData ); 

And then, you don't need any extra library.

I also recommend you to use async pipeline so you can ignore unsubscribing to prevent memory leaks.

this.records$ = this.streamFiltersService.getByFilters(
    this.page - 1,
    this.pagesize,
    ...
).pipe(debounceTime(200)); 
<app-whatever-component [records]="records$ | async"><app-whatever-component>