brians69
brians69

Reputation: 485

Transform data in an Observable

I'm retrieving a list of items from an Observable on my Angular service. Inside every item, I have an array with a list of subjects. For every subject, I need to make another call to get its details (e.g. name, description, etc.).

Data structure:

- post1
  - subjects: ['books', 'cars', 'movies']

With that list, I have the id from every subject but I need to return an observable containing all subjects belonging to that post (and their details).

AppService.ts

getPost(id: string): Observable<Post> {
    return Observable.of({id: id, subjects: ['books', 'cars', 'movies']});
}

getSubjects(id: string): Observable<Subject[]> {
    return this.getPost(id).pipe(
        map((post: Post) => post.subjects),
        mergeMap((subjects: string[]) => subjects.map((id: string) => this.getSubject(id))),
        switchMap(list => list),
    );
}

getSubject(id: string): Observable<Subject> {
    switch (id) {
        case 'books':
            return Observable.of({id: 'books', name: 'Books'});
        case 'cars':
            return Observable.of({id: 'cars', name: 'Cars'});
        case 'movies':
            return Observable.of({id: 'movies', name: 'Movies'});
    }
}

It returns a stream of objects but I'd like to return an array with all subjects.

[DEMO]

Upvotes: 2

Views: 2043

Answers (1)

CozyAzure
CozyAzure

Reputation: 8468

That is because you confused .map() of Observables.map() and the .map() of Array.prototype.map().

This particular line of code:

 mergeMap((subjects: string[]) => subjects.map((id: string) => this.getSubject(id)))

does not return an array of Subjects; instead, it returns an array of Observables because the subject argument inside the function is an array, not an Observable. Hence, that particular .map() will return an array of the retyrn type of this.getSubject(id), which is an array of Observables; exactly the behaviour you are getting.

If you want to have all the Observables resolved into an array of Subjects, you will have to use Observable.forkJoin. .forkJoin() combines all the observables and fire them in parallel.

getSubjects(id: string): Observable<Subject[]> {
    return this.getPost(id).pipe(
        map((post: Post) => post.subjects),
        mergeMap((subjects: string[]) => Observable.forkJoin([...subjects.map((id: string) => this.getSubject(id))])),
    );
}

Now you can use it in your component:

getSubjects(id).subscribe(values=>{
    console.log(values)//returns an array of Subjects
})

Note that Observables.forkJoin will have to wait for all observables to complete before firing an emission.

Upvotes: 2

Related Questions