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
I also used the default Angular Material setup by typing.
ng add @angular/material
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
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)));
}
-
Countryis just a simple interface with thenameproperty. - Here is the documentation for the URL which I used.
-
mapis 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:
-
MatFormFieldModuleandMatInputModuleis used by the field -
MatAutocompleteModulefor autocompletion -
ReactiveFormsModulebecause 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>
There are two important things:
-
[matAutocomplete]="auto"is an attribute which connects field with autocompletion list -
asyncpipe, 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],
});
-
countries$which holds my countries list -
formreactive form definition
In constructor definition:
constructor(
private formBuilder: FormBuilder,
private countryService: CountryService,
) {
-
formBuilderfor reactive form creation -
countryServicefor 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))
);
-
valueChangeswhich triggers every value change (It is an Observable) -
distinctUntilChangedoperator which emits only when the value is different than the previous one (avoid making requests for the same name one after another) -
debounceTimeoperator 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) -
filteroperator which checks if there is the value (avoid HTTP calls with no name) -
switchMapoperator which is changing from one observable (valueChanges) to another (getByNamefrom 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))
);
}
}
Link to repo.
Top comments (6)
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:
The result would be like this:

Good point. I didn't focus on error handling, but definitely on production; it should be.
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.
Well, small hack could be:
But In this situation, I am telling code that I am sure about my name control because it is hardcoded.
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 :)
Try npmjs.com/package/angular-ng-autoc...