DEV Community

Tomasz Flis
Tomasz Flis

Posted on

10 3

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.

Tiugo image

Fast, Lean, and Fully Extensible

CKEditor 5 is built for developers who value flexibility and speed. Pick the features that matter, drop the ones that don’t and enjoy a high-performance WYSIWYG that fits into your workflow

Start now

Top comments (6)

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

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

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 :)

Collapse
 
gmerabishvili profile image
Giorgi Merabishvili

Neon image

Next.js applications: Set up a Neon project in seconds

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Get started →

👋 Kindness is contagious

Explore a trove of insights in this engaging article, celebrated within our welcoming DEV Community. Developers from every background are invited to join and enhance our shared wisdom.

A genuine "thank you" can truly uplift someone’s day. Feel free to express your gratitude in the comments below!

On DEV, our collective exchange of knowledge lightens the road ahead and strengthens our community bonds. Found something valuable here? A small thank you to the author can make a big difference.

Okay