Simple filter on array of RXJS Observable
Asked Answered
S

5

78

I am starting my project with Angular2 and the developers seem to recommend RXJS Observable instead of Promises.

I have achieved to retrieve a list of elements (epics) from the server. But how can I filter the elments by using for example an id?

The following code is an extraction from my app and shows now the final working solution. Let's hope it helps someone.

@Injectable()
export class EpicService {

  private url = CONFIG.SERVER + '/app/';  // URL to web API

  constructor(private http:Http) {}

  private extractData(res:Response) {
    let body = res.json();
    return body;
  }

  getEpics():Observable<Epic[]> {
    return this.http.get(this.url + "getEpics")
      .map(this.extractData)
      .catch(this.handleError);
  }

  getEpic(id:string): Observable<Epic> {
    return this.getEpics()
      .map(epics => epics.filter(epic => epic.id === id)[0]);
  }
}

export class EpicComponent {

  errorMessage:string;
  epics:Epic[];
  epic:Epic;

  constructor(
    private requirementService:EpicService) {
  }

  getEpics() {
    this.requirementService.getEpics()
      .subscribe(
        epics => this.epics = epics,
        error => this.errorMessage = <any>error);
  }

  // actually this should be eventually in another component
  getEpic(id:string) {
    this.requirementService.getEpic(id)
        .subscribe(
        epic => this.epic = epic,
        error => this.errorMessage = <any>error);
  }
}

export class Epic {
  id: string;
  name: string;
}

Thank you in advance for your help.

Sanious answered 23/6, 2016 at 12:32 Comment(1)
you should have left the original question as is, this way we will never know what "not" to doTiter
B
135

You'll want to filter the actual array and not the observable wrapped around it. So you'll map the content of the Observable (which is an Epic[]) to a filtered Epic.

getEpic(id: string): Observable<Epic> {
  return this.getEpics()
     .map(epics => epics.filter(epic => epic.id === id)[0]);
}

Then afterwards you can subscribe to getEpic and do whatever you want with it.

Bibelot answered 23/6, 2016 at 12:42 Comment(2)
Looks simple but: Error:(33, 42) TS2365: Operator '===' cannot be applied to types 'string' and 'number'. I will update question, to show the whole component and service relationship.Sanious
Well if it's that then you can just convert from string to number or number to string :)Bibelot
L
41

You can do this using the flatMap and filter methods of Observable instead of the JS array filter method in map. Something like:

this.getEpics() 
    .flatMap((data) => data.epics) // [{id: 1}, {id: 4}, {id: 3}, ..., {id: N}]
    .filter((epic) => epic.id === id) // checks {id: 1}, then {id: 2}, etc
    .subscribe((result) => ...); // do something epic!!!

flatMap will provide singular indices for filtering and then you can get on with whatever happens next with the results.

If TypeScript throws a error indicating you can't compare a string and a number regardless of your use of == in the filter just add a + before epic.id in the filter, per the Angular docs:

    .flatMap(...)
    .filter((epic) => +epic.id === id) // checks {id: 1}, then {id: 2}, etc
    .subscribe(...)

Example:

https://stackblitz.com/edit/angular-9ehje5?file=src%2Fapp%2Fapp.component.ts

Lathrop answered 4/4, 2017 at 22:53 Comment(5)
This is much cleaner than the accepted answer and makes full use of Rx. The proper way to perform this operation is to use flatMap as the author has pointed out.Inertia
If epics ist an array (like you suggest), one can not flatMap to data.epics. And you also can not filter the array like you wrote.Burble
@StefanRein I would argue that you should try the code out first then comment - stackblitz.com/edit/…Lathrop
@Lathrop Yes, I did copy your comment from SO as input. Your comment for the method getEpics() here on SO should be: // { epics: [{id: 1}, {id: 2}, {id: 3}, {id: 4}, {id: 5}] } and not // [{id: 1}, {id: 4}, {id: 3}, ..., {id: N}] like in the Stackblitz example.. that confused me, thanks. Should've looked int the Stackblitz example, too :)Burble
I've moved the comment down one lineLathrop
V
8

original answer with a fix: Observables are lazy. You have to call subscribe to tell an observable to send its request.

  getEpic(id:number) {
    return this.getEpics()
           .filter(epic => epic.id === id)
           .subscribe(x=>...);
  }

Update to Rxjs 6:

import {filter} from 'rxjs/operators';

getEpic(id:number) {
        return this.getEpics()
               .pipe(filter(epic => epic.id === id))
               .subscribe(x=>...);
      }
Veratridine answered 23/6, 2016 at 12:42 Comment(3)
The return type of subscribe is a Subscription, which implements ISubscription. Both do not have a filter method.Burble
@StefanRein, in the original question this.getEpics() returns observable of an Array. So, filter is a method of ArrayVeratridine
You are right. Just in your answer you are chaining the filter after the subscribe method, which has not a filter method, thus my commentBurble
P
0

You have to subscribe on Observables to get the data, since http calls are async in JavaScript.

getEpic(id: number, callback: (epic: Epic) => void) {
    this.getEpics().subscribe(
        epics: Array<Epic> => {
            let epic: Epic = epics.filter(epic => epic.id === id)[0];
            callback(epic);
        }
    );
}

You can call that method then like this:

this.someService.getEpic(epicId, (epic: Epic) => {
    // do something with it
});
Pressley answered 23/6, 2016 at 12:43 Comment(0)
E
0

To anyone trying to use flatMap, it is deprecated currently and rxJs recommends using mergeMap instead.

Edirne answered 13/4, 2023 at 8:19 Comment(1)
This does not provide an answer to the question. Once you have sufficient reputation you will be able to comment on any post; instead, provide answers that don't require clarification from the asker. - From ReviewTito

© 2022 - 2024 — McMap. All rights reserved.