Angular HttpClient - accessing value buried in response data

Viewed 227

I am accessing an online API and want to use the text value to populate a ngb-typeahead dropdown. There is a working example on the Angular Bootstrap website using Wikipedia, but the returned data from the Wikipedia API is different to the data I am getting from a geocoding API. The data I get is returned in this format:

{
 "suggestions": [
  {
   "text": "23 Queen Charlotte Drive, Aotea, Porirua, Wellington, 5024, NZL",
   "magicKey": "dHA9MCNsb2M9NDMwNzcyNzQjbG5nPTMzI2huPTIzI2xicz0xMDk6NDg1NDQwMzU=",
   "isCollection": false
  },
  {
   "text": "23 Queen Mary Avenue, Epsom, Auckland, 1023, NZL",
   "magicKey": "dHA9MCNsb2M9NDMwNDY4MjUjbG5nPTMzI2ZhPTE0NDE3OTIjaG49MjMjbGJzPTEwOTo0ODU0NDMyNA==",
   "isCollection": false
  },

I have been trying to access text in response data with the following:

return this.http
  .get<any>(GIS_URL, {params: GIS_PARAMS.set('text', term)}).pipe(
    map(response => response.suggestions)
  );

I have also read the Angular tutorial here on dealing with response data, but the difference in the example is that they are getting an array of Hero's whereas I am getting an object containing an array of suggestions.

The typeahead looks like:

Enter image description here

HTML

<fieldset class="form-inline">
  <div class="form-group">
    <label for="typeahead-http">Search for a wiki page:</label>
    <input id="typeahead-http" type="text" class="form-control mx-sm-3" [class.is-invalid]="searchFailed" [(ngModel)]="model" [ngbTypeahead]="search" placeholder="Wikipedia search" />
    <small *ngIf="searching" class="form-text text-muted">searching...</small>
    <div class="invalid-feedback" *ngIf="searchFailed">Sorry, suggestions could not be loaded.</div>
  </div>
</fieldset>
<hr>
<pre>Model: {{ model | json }}</pre>

Full code on StackBlitz is here.

I am new to Angular, so a verbose answer would be great.

2 Answers

You need to specify resultFormatter and inputFormatter on the typeahead input (refer to Typeahead).

Explanation

Your search method in the service returns a list of suggestion Objects which each look like:

{
  isCollection: ...
  magicKey: ...
  text: ...
}

However by default the typeahead control expects a list of strings, hence it displays your objects as [Object object].

You need to tell the typeahead control how to determine a string value from your object, you do this via resultFormatter and inputFormatter.

These inputs take a function, which has the object as an input and the string display value as its output.

formatter below is that function, it will be called for each item displayed in the list. If you expand it to a normal function you can put a breakpoint in it and see it being called in this manner.

Solution

<input id="typeahead-http" ... [inputFormatter]="formatter" [resultFormatter]="formatter"/>

TypeScript file:

formatter = (item:any) => item.text as string;

Updated StackBlitz

https://stackblitz.com/edit/so-typeahead?file=src%2Fapp%2Ftypeahead-http.ts

Follow-up questions

  1. item in the formatter:

    Consider:

     formatter = (item:any) => item.text as string;
    

    is shorthand for:

     function format(item: any){
       return item.text as string;
     }
    

    They typeahead control/directive iterates the items returned by search(..) and calls this method which each one. The results are displayed in the select list.

  2. map(response => response.suggestions)

    The response from the service is an object like:

     {  // object
       suggestions:
         [
           { ..., text: 'Place 1' },
           { ..., text: 'Place 2' }
         ]
     }
    

    That is an object containing a list named suggestions. The typeahead expects a list only, so the map transforms the object containing list => list only.

  3. Does the formatter that you have defined do both input and result?

    Yes, as it is assigned to both [inputFormatter] and [resultFormatter] in the template.

Alternative answer

The mapping is done entirely in the service:

return this.http
      .get<any>(GIS_URL, {params: GIS_PARAMS.set('text', term)}).pipe(
        map(response => response.suggestions.map(suggestion => suggestion.text)),
      );

Each response object is mapped to the list of suggestions. Each suggestion is mapped (using JavaScript map) to its text value.

You can use this solution provided you don't need access to any of the other suggestion properties outside of the service.

Related