2

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?

2
  • flatMap is supposed to return Observable, but your code doesn't return anything. Maybe that's the problem. Commented Jul 22, 2016 at 3:37
  • code has been updated, you were right about that Commented Jul 22, 2016 at 3:41

1 Answer 1

5

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}));
Sign up to request clarification or add additional context in comments.

7 Comments

thanks! The first case fit my use case and is very elegant, and works. I am not sure I understand the second flatMap though. .flatMap((film: string) => this.http.get(film), (_, resp) => resp.json().title) } what is (_, resp) doing? And I'm not sure I understand how you are mapping over the http return with just a comma, the syntax is confusing me.
flatMap takes two arguments, the first returns an object for flattening and the second takes the individual values from the flattened source and applies a mapping over them. In this case it takes the responses from each HTTP call and extracts the title from the response value.
Thanks paul! If I wanted to extract the title and a date from the object, and output an array of objects like : [ {title: 'resp.json().title', date: 'rsp.json().date'}, {title: 'title, date: 'date'}], that I could use *ngFor to display async in a component, would that be possible?
It is also giving me two of each response
and forkJoin gives error TypeError: Cannot read property 'Symbol(Symbol.iterator)' of undefined that I had before
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.