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)));
}
-
Country
is just a simple interface with thename
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
andMatInputModule
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>
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],
});
-
countries$
which holds my countries list -
form
reactive form definition
In constructor definition:
constructor(
private formBuilder: FormBuilder,
private countryService: CountryService,
) {
-
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))
);
-
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))
);
}
}
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...