C. Kearns
C. Kearns

Reputation: 1671

Angular 2 chained Http Get Requests with Iterable Array

Looking for help with an Observable http sequence, I want to make two http calls to an api, the second dependent on the first. The first returns an Array of Urls, and the second makes get requests for every url in that array and then returns the responses on the stream. If I hard code in one of the dependent request like so I get back one of the titles I am looking for:

search(character): Observable<any> {
let that = this
let queryUrl: string = character.url;
return this.http.get(queryUrl)
  .map((response: Response) => {
    this.characterResults = response.json().films
    return this.characterResults
        //Example response: 
        // ["https://api.com/films/1", "https://api.com/films/2", "https://api.com/films/3", "https://api.com/films/4"]
  })
  .flatMap((film) => {
    return that.http.get(film[0])
    .map((response: Response) => {
      return response.json().title
    })
  })
}

getApiData(character) {
    this.apiService.search(character)
    .subscribe((results) => { // on sucesss
          console.log(results)
        },
        (err: any) => { // on error
          console.log(err);
        },
        () => { // on completion
          console.log('complete')
        }
      );

but any attempt to iterate using forEach over that array and then make all the http calls gives me this error:

browser_adapter.js:84 EXCEPTION: TypeError: Cannot read property 'Symbol(Symbol.iterator)' of undefined

Ideally, I would like an elegant way to make those subsequent calls in parallel with the result array from the first call, but I cannot seem to figure out which of rxJs's dozens of methods could help me do it. Can anyone offer me any help?

Upvotes: 2

Views: 6057

Answers (1)

paulpdaniels
paulpdaniels

Reputation: 18663

Two ways, if you want each of the results to be streamed individually then flatMap is your friend. You can use it to flatten out just about anything, Arrays, Promises, or Observables. In your case you can chain the request together by doing:

search(character): Observable<any> {
  return this.http.get(character.url)
    .flatMap((response: Response) => response.json().films)
    .flatMap((film: string) => this.http.get(film), 
             (_, resp) => resp.json().title)
}

getApiData(character) {
    this.apiService.search(character)
      .subscribe((results) => { // on sucesss
          //Called for each url result
          console.log(results)
        },
        (err: any) => { // on error
          console.log(err);
        },
        () => { // on completion
          console.log('complete')
        }
      );

Or you can use forkJoin instead to return an array of results

return this.http.get(character.url)
  .flatMap((response: response) => 
             //Waits until all the requests are completed before emitting
             //an array of results
             Observable.forkJoin(response.json().files.map(film => 
               this.http.get(film).map(resp => resp.json().title)
             ));

Edit 1

To address the second argument of flatMap more completely. It's a function which receives the original value passed to the first selector method paired with a result from the flattening operation. That is if selector 1 receives a and returns an Observable of [1, 2, 3]. The second selector would be called 3 times with arguments (a, 1), (a, 2) and (a, 3).

If you want to get multiple values I would suggest that you use a secondary mapping operator to keep the flow clean, but it is up to you.

.flatMap((film: string) => this.http.get(film),
         (film: string, resp: Response) => resp.json())
//Typescript's destructuring syntax, extracts the values you want
//and creates a new object with only those fields
.map(({date, title}) => ({date, title}));

Upvotes: 5

Related Questions