Search code examples
javascriptangulartypescriptasynchronousobservable

Filter an observable based on value comping from an API call


I'm new to RxJS, any help will be appreciated!

In my component html template, I would like to generate a radio button list.

The list is populated with values from an observable, via the async pipe.

This is the template part for the radio button list:

<div *ngIf="entities$ | async as entities">
    <mat-radio-group
        aria-labelledby="entities-radio-group-label"
        class="entities-radio-group"
        <mat-radio-button *ngFor="let entity of entities" [value]="entity">
          {{entity.description}}          
        </mat-radio-button>
      </mat-radio-group>
</div>

On the component class I have an observable that retrieve the entities data, using a service. I also filter the entities, by entity type.

  entities$: Observable<Entity[]> = this.eventsService.entities$
  .pipe(
    map(items => items.filter(item => item.type.toLowerCase().indexOf("set") > -1)),
    )

A retrieved entities object have this structure:

[
{"id":"34534534643364",
 "type":"SET",
 "description":"some description",
 "link":"/conf/sets/34534534643364"},
{"id":"5474745744457547",
 "type":"SET",
 "description":"other description",
 "link":"/conf/sets/5474745744457547"}
 ]

This part works and I can display the "SET" type entities.

But I also need to filter the entities list based on additional value that I need to retrieve with an API call. For each entity in this source observable I need to issue a request that uses the specified entity link

This is how a request looks like (I'm using a service)

 restService.call<any>(entityUrl)
 .pipe(finalize(()=>this.loading=false))
 .subscribe(
    apidata => console.log(`data: ${JSON.stringify(apidata)}`),
    error => this.alert.error('Failed to retrieve entity: ' + error.message)
    );

This returns an observable and the data is basically an object like this:

{
    "id": "34534534643364",
    "name": "some name",
    "description": null,
    "type": {
        "value": "LOGICAL",
        "desc": "Logical"
    },
    "content": {
        "value": "IEP",
        "desc": "This it the value I need"
    },
    "status": {
        "value": "ACTIVE",
        "desc": "Active"
    }
}

I need to use the value of "desc", to preform the additional filtering.

I attempted to use a function to preform the additional filtering, and add it to the source observable.

The Observable:

  entities$: Observable<Entity[]> = this.eventsService.entities$
  .pipe(
    tap((items) => console.log("started pipe", items)),
    map(items => items.filter(item => item.type.toLowerCase().indexOf("set") > -1)),
    tap((items) => console.log("after set filtered pipe", items)),
    map(items => items.filter(item => this.descFilter(item.link))),
    tap((items) => console.log("after descFilter: ", items)),
    tap(() => this.clear())
    );

The function:

 descFilter(link: string): boolean{
    let testedEnt:any = [];
    resObs$ = this.restService.call<any>(link)
    .pipe(finalize(()=>this.loading=false))
    .subscribe(
        next => {console.log(`checkTitleSet api call result:, ${JSON.stringify(next)}`);
                 testedEnt = next.content.desc;
                 console.log("desc from inside next: ",testedEnt);}, // this retrieved the value
       error => this.alert.error('Failed to retrieve entity: ' + error.message)
     );

    console.log("desc: ",testedEnt); // does not retrieve the value (the request did not complete) 

    if (testedEnt === "some string"){return true;} else {return false;}
    }

It did not work, because the api needs also time to process.

I also thought of an additional option:

Use only the api results for my template radio button group. I was able to create an array of observables (all the api results) But I don't know of to use this array in my template.

Any advice will be appreciated!


Solution

  • If I understand correctly, the initial situation is as follows:

    1. You fetch several items from your API
    2. For each item you have to fetch a filter object from the API, which decides if the item will be filtered out or not

    The main problem with your code is: In order to chain observables you cannot use the map operator. Instead I suggest to apply switchMap in combination with forkJoin.

    I suggest to modify your async pipe as follows (for better understanding, please see my comments in the code):

    entities$: Observable<Entity[]> = this.eventsService.entities$
        .pipe(
            tap((items) => console.log("started pipe", items)),
            map(items => items.filter(item => item.type.toLowerCase().indexOf("set") > -1)),
            tap((items) => console.log("after set filtered pipe", items)),
            switchMap(items => {
    
                // Create an array of filter observables that include an api-call
                // and return the item itself if the condition is met, or NULL otherwise
                const filterObs = items.map(item => this.descFilter(item));
                
                // Use forkJoin to execute the array with the http-requests
                return filterObs.length ? forkJoin(filterObs) : of([]);
            }),
            // Filter out the NULL values:
            map(items => items.filter(i => i !== null)),
            tap((items) => console.log("after descFilter: ", items)),
            tap(() => this.clear())
        );
    

    Also the descFilter() method needs to be changed, otherwise you cannot integrate it into the async pipe:

    /* Based on whether the condition is met, the item itself or NULL will be returned */
    descFilter(item: Entity): Observable<Entity | null> {
        return this.restService.call<any>(item.link).pipe(
            map(res => res.content.desc === "some string" ? item : null)
        );
    }
    

    Important: Eventually you might have to subscribe to entities$ to get the code running.