I'm learning Angular5 & RxJS at the moment. I'm trying to get a simple app to run:
- Search bar to enter term
- keyup triggers function that then calls a service
- service returns api call wrapped in observable
- observable is subscribed to with async pipe and template updates with results from the api call
I tried two options and have one issue with each:
a) Subscribing in the component and update template data:
this.placePredictionService.getPlacePredictions(term).subscribe( data =>
{
this.results = data;
});
The template does update on the {{results}} binding, but only on the second function call. The template then gets updated with the results from the first call. Why?
b) Returning an observable and updating template with async pipe
private results$: Observable<any[]>;
this.results$ = this.placePredictionService.getPlacePredictions(term);
This way, nothing happens in the template. What don't I get? Where is my understanding lacking? Thank you very much for giving hints on what to look into.
Solutions to the 2 Problems: Thanks @Richard Matsen!
a) Problem was, that the calls of the Google Maps API weren't within the Angular Zone, therefore change detection wasn't triggered automatically. Wrapping the API Call in the service in the ngZone.run() function did the trick:
this.autocompleteService.getPlacePredictions({input: term}, data => {
this.ngZone.run(() => {
this.predictions.next(data);
});
});
b) Using a subject to not cause a new stream with every new keystroke solved the issue of the async pipe not working properly, see comment below for code.
The full component, service & template are like this:
app.component.ts
import { Component } from '@angular/core';
import { MapsAPILoader } from '@agm/core';
import { PlacePredictionService } from './place-prediction.service';
import { Observable } from 'rxjs/Observable';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
private searchTerm: string;
private results$: Observable<any[]>;
testResult = [{description: 'test'},{description: 'test'}];
constructor(
private mapsAPILoader: MapsAPILoader,
private placePredictionService: PlacePredictionService
){}
onSearch(term: string){
this.searchTerm = term;
if (this.searchTerm === '') return;
this.results$ = this.placePredictionService.getPlacePredictions(term);
}
}
place-prediction.service.ts
import { Injectable } from '@angular/core';
import { MapsAPILoader } from '@agm/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/bindCallback';
@Injectable()
export class PlacePredictionService {
private autocompleteService;
constructor(
private mapsAPILoader: MapsAPILoader
) {
this.mapsAPILoader.load().then( () => {
this.autocompleteService = new google.maps.places.AutocompleteService();
});
}
// Wrapper for Google Places Autocomplete Prediction API, returns observable
getPlacePredictions(term: string): Observable<any[]>{
return Observable.create(observer => {
// API Call
this.autocompleteService.getPlacePredictions({input: term}, (data) => {
let previousData: Array<any[]>;
// Data validation
if(data) {
console.log(data);
previousData = data;
observer.next(data);
observer.complete();
}
// If no data, emit previous data
if(!data){
console.log('PreviousData: ');
observer.next(previousData);
observer.complete();
// Error Handling
} else {
observer.error(status);
}
});
});
}
}
app.component.html
<h1>Google Places Test</h1>
<p>Angular 5 & RxJS refresher</p>
<input
type="search"
placeholder="Search for place"
autocomplete="off"
autocapitalize="off"
autofocus
#search
(keyup)="onSearch(search.value)"/>
<p>{{ searchTerm }}</p>
<ul>
<li *ngFor="let result of results$ | async "> {{result.description}}</li>
</ul>
angular-google-maps
tag. – Aside