Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 5: Can't update template with async pipe

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 &amp; 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>
like image 496
Juri Avatar asked Feb 07 '18 18:02

Juri


1 Answers

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>
like image 193
Richard Matsen Avatar answered Nov 12 '22 18:11

Richard Matsen