DEV Community

Tomasz Flis
Tomasz Flis

Posted on

Async material autocomplete in Angular

Topic

While working on my company project, I get the task of making a country selector. The project is using Angular with Angular Material. This is how I made it.

Prerequisites

For the demo version, I will do a simple angular project with that field only.
To make Angular project type in the command line:

ng new async-autocomplete
Enter fullscreen mode Exit fullscreen mode

I also used the default Angular Material setup by typing.

ng add @angular/material
Enter fullscreen mode Exit fullscreen mode

Now my demo project is ready.

Http Service

To be able to make HTTP calls in my AppModule I imported HttpClientModule from @angular/common/HTTP.
In the app directory, I generated a service which is used for making HTTP call. I typed the command:

ng g service country
Enter fullscreen mode Exit fullscreen mode

which produced the country.service.ts file for me.
In that service, I used HttpClient in the constructor imported from @angular/common/http.
Method for getting countries list

getByName(name: string): Observable<string[]> {
    return this.http
      .get<Country[]>(`https://restcountries.eu/rest/v2/name/${name}`)
      .pipe(map(countryList => countryList.map(({ name }) => name)));
  }
Enter fullscreen mode Exit fullscreen mode
  • Country is just a simple interface with the name property.
  • Here is the documentation for the URL which I used.
  • map is an operator for mapping value inside observable (I am just pulling out country name)

The input

For the field I imported 3 modules in AppModule:

  • MatFormFieldModule and MatInputModule is used by the field
  • MatAutocompleteModule for autocompletion
  • ReactiveFormsModule because the field is used inside reactive form.

The HTML template is quite simple:

<form [formGroup]="form">

  <mat-form-field appearance="fill">
    <mat-label>Name</mat-label>
    <input matInput formControlName="name" [matAutocomplete]="auto">
  </mat-form-field>

</form>

<mat-autocomplete #auto="matAutocomplete">
  <mat-option *ngFor="let countryName of countries$ | async" [value]="countryName">
    {{countryName}}
  </mat-option>
</mat-autocomplete>
Enter fullscreen mode Exit fullscreen mode

There are two important things:

  • [matAutocomplete]="auto" is an attribute which connects field with autocompletion list
  • async pipe, which subscribes to observable and unsubscribe when the component is destroyed.

My component ts code has two properties:

  countries$: Observable<string[]>;
  form = this.formBuilder.group({
    name: [null],
  });
Enter fullscreen mode Exit fullscreen mode
  • countries$ which holds my countries list
  • form reactive form definition

In constructor definition:

  constructor(
    private formBuilder: FormBuilder,
    private countryService: CountryService,
  ) {
Enter fullscreen mode Exit fullscreen mode
  • formBuilder for reactive form creation
  • countryService for using the HTTP method defined in service.

On every input value change, I am switching to service to make GET call for a list and I am assigning it to my observable:

    this.countries$ = this.form.get('name')!.valueChanges.pipe(
      distinctUntilChanged(),
      debounceTime(1000),
      filter((name) => !!name),
      switchMap(name => this.countryService.getByName(name))
    );
Enter fullscreen mode Exit fullscreen mode
  • valueChanges which triggers every value change (It is an Observable)
  • distinctUntilChanged operator which emits only when the value is different than the previous one (avoid making requests for the same name one after another)
  • debounceTime operator to avoid spamming API with too many calls in a short time (It waits 1000ms and if the value is not emitted, then emits last value)
  • filter operator which checks if there is the value (avoid HTTP calls with no name)
  • switchMap operator which is changing from one observable (valueChanges) to another (getByName from service).

Full TS code:

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import { debounceTime, distinctUntilChanged, filter, switchMap } from 'rxjs/operators';
import { CountryService } from './country.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  countries$: Observable<string[]>;
  form = this.formBuilder.group({
    name: [null],
  });

  constructor(
    private formBuilder: FormBuilder,
    private countryService: CountryService,
  ) {
    this.countries$ = this.form.get('name')!.valueChanges.pipe(
      distinctUntilChanged(),
      debounceTime(1000),
      filter((name) => !!name),
      switchMap(name => this.countryService.getByName(name))
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

Link to repo.

Discussion (5)

Collapse
federicopenaranda profile image
Federico Peñaranda

I was testing the field, and if you put a filter so there are no resulting countries (like "xas" of something else) it's showing a 404 error in the request and shows nothing in the list, maybe you can improve it by catching the error in the service?, something like this:

getByName(countryName: string): Observable<string[]> {
return this.http.get<Country[]>(`https://restcountries.eu/rest/v2/name/${countryName}`)
    .pipe(
    map(countryList => countryList.map( ({ name }) => name )),
    catchError( (err) => {
        if (err.error.status === 404) {
            return of([`--- No results for: ${countryName} ---`]);
        }
    })
    );
}
Enter fullscreen mode Exit fullscreen mode

The result would be like this:
Empty list

Collapse
tomwebwalker profile image
Tomasz Flis Author

Good point. I didn't focus on error handling, but definitely on production; it should be.

Collapse
federicopenaranda profile image
Federico Peñaranda

Practical and easy to follow code, thanks!. Question, what's the use of "!" in "this.form.get('name')!"?, I tried the code and I get an TSlint error "Forbidden non null assertion (no-non-null-assertion)", I understad that it might be to check if the field exists, but maybe we can do it in a different way to prevent that warning?. Cheers.

Collapse
tomwebwalker profile image
Tomasz Flis Author

Well, small hack could be:

    const nameControl = this.form.get('name') as AbstractControl
    this.countries$ = nameControl.valueChanges.pipe(
Enter fullscreen mode Exit fullscreen mode

But In this situation, I am telling code that I am sure about my name control because it is hardcoded.

Collapse
agborkowski profile image
AgBorkowski

just skip it, as linter saying its a mistake ;) I'm fighting with the loading spinner,
stackblitz.com/edit/angular-materi... but wont work with angular 10, and markDetectionChange :)