I'm learning Angular5 & RxJS at the moment. I'm trying to get a simple app to run:
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>
A manual call to ChangeDetectorRef.detectChanges
fixes the event lagging.
I guess the api call is outside of Angular's automatic change detection, so it needs to be triggered each time new results arrive.
place-prediction.service.ts
@Injectable()
export class PlacePredictionService {
predictions = new Subject();
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) {
// API Call
this.autocompleteService.getPlacePredictions({input: term}, (data) => {
this.predictions.next(data);
});
}
}
app.component.ts
import { Component, ChangeDetectorRef } from '@angular/core';
...
export class AppComponent {
private searchTerm: string;
private results = [];
constructor(
private cdr: ChangeDetectorRef,
private mapsAPILoader: MapsAPILoader,
private placePredictionService: PlacePredictionService
){}
ngOnInit() {
this.placePredictionService.predictions.subscribe(data => {
this.results = data;
this.cdr.detectChanges();
});
}
onSearch(term: string) {
this.searchTerm = term;
if (this.searchTerm === '') { return; }
this.placePredictionService.getPlacePredictions(term);
}
}
app.component.html
<ul>
<li *ngFor="let result of results"> {{result.description}}</li>
</ul>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With