Geoapify provides an API which allows searching a location by the query string. In this article, we provide you an example of how to create a location autocomplete field by using Geoapify Geocoding API.
In our example, we use Angular platform together with Angular Material framework.
Geoapify Geocoding API Playground contains a working example of the location autocomplete described in this article.
Pre-requirements
- Angular Material installed. Read about installation here.
Step 1. Create a new component for a location autocomplete
Create a new component with Angular Cli command:
ng generate component Autocomplete
Step 2. Required imports
To make the component from this tutorial work, it's required to import the following modules into your module:
@NgModule({
declarations: [..., AutocompleteComponent],
imports: [
CommonModule,
HttpClientModule,
MatInputModule,
MatFormFieldModule,
FormsModule,
ReactiveFormsModule,
MatAutocompleteModule,
MatTooltipModule,
...
],
exports: [...]
})
Step 3. HTML template
We use the following components:
- mat-form-input
- mat-input
- mat-autocomplete
- mat-tooltip
- formControl from ReactiveFormsModule.
Add the following code into your component html template:
<mat-form-field floatLabel="never">
<input matInput type="text" [matAutocomplete]="auto"
[formControl]="inputFieldFormControl" placeholder="Enter location here" />
<mat-autocomplete #auto="matAutocomplete" autoActiveFirstOption>
<mat-option *ngFor="let option of searchOptions | async" [value]="option.shortAddress"
(onSelectionChange)="optionSelectionChange(option, $event)"
[matTooltip]="option.fullAddress" matTooltipShowDelay="1000">
<span class="mat-body">{{ option.shortAddress }}</span>
</mat-option>
</mat-autocomplete>
</mat-form-field>
Here is an idea of how the location autocomplete will work:
- The string value will be stored in the "inputFieldFormControl" reactive form field.
- The "inputFieldFormControl" field fires an event when its value was changed.
- When an event fired we send an HTTP Get request to Geocoding API to retrieve place suggestions and store them in "searchOptions".
- mat-autocomplete loads options from the "searchOptions" asynchronously.
Step 4. AutocompleteComponent class
Here is the code of the AutocompleteComponent class:
import { Component, Output, EventEmitter, OnDestroy } from '@angular/core';
import { MatOptionSelectionChange } from '@angular/material';
import { Subject, Subscription } from 'rxjs';
import { FormControl } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-autocomplete',
templateUrl: './autocomplete.component.html',
styleUrls: ['./autocomplete.component.scss']
})
export class AutocompleteComponent implements OnDestroy {
@Output()
locationChange: EventEmitter<PlaceSuggestion> = new EventEmitter<PlaceSuggestion>();
searchOptions: Subject<PlaceSuggestion[]> = new Subject<PlaceSuggestion[]>();
inputFieldFormControl: FormControl = new FormControl();
private valueChangesSub: Subscription;
private choosenOption: PlaceSuggestion;
private userInputTimeout: number;
private requestSub: Subscription;
constructor(private http: HttpClient) {
this.valueChangesSub = this.inputFieldFormControl.valueChanges.subscribe((value) => {
if (this.userInputTimeout) {
window.clearTimeout(this.userInputTimeout);
}
if (this.choosenOption && this.choosenOption.shortAddress === value) {
this.searchOptions.next(null);
return;
}
if (!value || value.length < 3) {
// do not need suggestions until for less than 3 letters
this.searchOptions.next(null);
return;
}
this.userInputTimeout = window.setTimeout(() => {
this.generateSuggestions(value);
}, 300);
});
}
ngOnDestroy() {
this.valueChangesSub.unsubscribe();
}
private generateSuggestions(text: string) {
const url = `https://api.geoapify.com/v1/geocode/autocomplete?text=${text}&limit=5&apiKey=${YOUR_API_KEY}`;
if (this.requestSub) {
this.requestSub.unsubscribe();
}
this.requestSub = this.http.get(url).subscribe((data: GeoJSON.FeatureCollection) => {
const placeSuggestions = data.features.map(feature => {
const properties: GeocodingFeatureProperties = (feature.properties as GeocodingFeatureProperties);
return {
shortAddress: this.generateShortAddress(properties),
fullAddress: this.generateFullAddress(properties),
data: properties
}
});
this.searchOptions.next(placeSuggestions.length ? placeSuggestions : null);
}, err => {
console.log(err);
});
}
private generateShortAddress(properties: GeocodingFeatureProperties): string {
let shortAddress = properties.name;
if (!shortAddress && properties.street && properties.housenumber) {
// name is not set for buildings
shortAddress = `${properties.street} ${properties.housenumber}`;
}
shortAddress += (properties.postcode && properties.city) ? `, ${properties.postcode}-${properties.city}`: '';
shortAddress += (!properties.postcode && properties.city && properties.city !== properties.name) ? `, ${properties.city}`: '';
shortAddress += (properties.country && properties.country !== properties.name) ? `, ${properties.country}` : '';
return shortAddress;
}
private generateFullAddress(properties: GeocodingFeatureProperties): string {
let fullAddress = properties.name;
fullAddress += properties.street ? `, ${properties.street}` : '';
fullAddress += properties.housenumber ? ` ${properties.housenumber}` : '';
fullAddress += (properties.postcode && properties.city) ? `, ${properties.postcode}-${properties.city}`: '';
fullAddress += (!properties.postcode && properties.city && properties.city !== properties.name) ? `, ${properties.city}`: '';
fullAddress += properties.state ? `, ${properties.state}`: '';
fullAddress += (properties.country && properties.country !== properties.name) ? `, ${properties.country}` : '';
return fullAddress;
}
public optionSelectionChange(option: PlaceSuggestion, event: MatOptionSelectionChange) {
if (event.isUserInput) {
this.choosenOption = option;
this.locationChange.emit(option);
}
}
}
export interface PlaceSuggestion {
shortAddress: string;
fullAddress: string;
data: GeocodingFeatureProperties;
}
interface GeocodingFeatureProperties {
name: string;
country: string;
state: string;
postcode: string;
city: string;
street: string;
housenumber: string;
}
inputFieldFormControl
As described above, the field holds the value of the search string, which is observed by this.inputFieldFormControl.valueChanges.subscribe().
To keep the code clean we save the created subscription in the variable valueChangesSub and unsubscribe on destroy.
userInputTimeout
To avoid too many unnecessary requests and decrease the application load, we perform HTTP request only when a user stops to type.
This implemented by using userInputTimeout, which sets every time when the user enters a new value.
searchOptions
Contain the values returned by Geocoding API and displayed by autocomplete control.
When we set this.searchOptions.next(null), the autocomplete control is hidden.
PlaceSuggestion & GeocodingFeatureProperties
We use the interfaces to simplify work with JSON object returned by the Geocoding API. We export PlaceSuggestion to be able to use the interface in other components, services, and modules.
locationChange
Is an Output() of the Autocomplete component. We this.locationChange.emit(option) when a new place suggestion was selected.
generateShortAddress() & generateFullAddress()
As Geocoding API returns value with address components, but not a formatted address, we need to generate an address string of required format. generateShortAddress() & generateFullAddress() are examples of how an address string could be generated.
Step 5. Add the Location Autocomplete component into your code
The new component could be added into your code in the following way:
<app-autocomplete (locationChange)="autocompleteChanged($event)"></app-autocomplete>
When a new value was chosen in the location autocomplete, the event is fired:
autocompleteChanged(value: PlaceSuggestion) {}