72

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.

Luka Jacobowitz
  • 22,795
  • 5
  • 39
  • 57
Johannes
  • 2,732
  • 5
  • 23
  • 32
  • 6
    you should have left the original question as is, this way we will never know what "not" to do – Ayyash Jan 06 '19 at 12:11

5 Answers5

132

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.

Luka Jacobowitz
  • 22,795
  • 5
  • 39
  • 57
  • 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. – Johannes Jun 23 '16 at 13:17
  • Well if it's that then you can just convert from string to number or number to string :) – Luka Jacobowitz Jun 23 '16 at 13:20
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

mtpultz
  • 17,267
  • 22
  • 122
  • 201
  • 2
    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. – damianesteban May 23 '18 at 03:04
  • 1
    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. – Stefan Rein Jul 10 '18 at 12:45
  • @StefanRein I would argue that you should try the code out first then comment - https://stackblitz.com/edit/angular-9ehje5?file=src%2Fapp%2Fapp.component.ts – mtpultz Jul 10 '18 at 23:36
  • @mtpultz 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 :) – Stefan Rein Jul 11 '18 at 07:50
  • 1
    I've moved the comment down one line – mtpultz Jul 11 '18 at 17:58
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=>...);
      }
Vignesh
  • 1,045
  • 2
  • 17
  • 34
Andrei Zhytkevich
  • 8,039
  • 2
  • 31
  • 45
  • The return type of subscribe is a Subscription, which implements ISubscription. Both do not have a filter method. – Stefan Rein Jul 10 '18 at 12:47
  • 1
    @StefanRein, in the original question `this.getEpics()` returns observable of an *Array*. So, `filter` is a method of *Array* – Andrei Zhytkevich Jul 10 '18 at 13:03
  • 1
    You are right. Just in your answer you are chaining the filter after the subscribe method, which has not a filter method, thus my comment – Stefan Rein Jul 10 '18 at 13:06
1

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
});
Maximilian Riegler
  • 22,720
  • 4
  • 62
  • 71
0

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

Akhouad
  • 11
  • 4
  • 1
    This does not provide an answer to the question. Once you have sufficient [reputation](https://stackoverflow.com/help/whats-reputation) you will be able to [comment on any post](https://stackoverflow.com/help/privileges/comment); instead, [provide answers that don't require clarification from the asker](https://meta.stackexchange.com/questions/214173/why-do-i-need-50-reputation-to-comment-what-can-i-do-instead). - [From Review](/review/late-answers/34212833) – pitamer Apr 17 '23 at 08:30