Angular 5: Can't update template with async pipe
Asked Answered
A

1

7

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>
Arredondo answered 7/2, 2018 at 18:10 Comment(2)
BTW, you may want to add the angular-google-maps tag.Aside
Done, hope its not confused with AngularJS now.Arredondo
A
3

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>
Aside answered 7/2, 2018 at 20:26 Comment(6)
Thank you very much for this answer! This fixes the problem when trying to implement the promise version with | async. But the issue that the changeDetection is only triggered on a second event still persists. I tried wrapping the this.placePredictionService.getPlacePredictions(term) into a ngZone.run() but didn't help.Arredondo
Yes, you are correct. My bad, I assumed it was due to network lag.Aside
Do you have a tipp on how to fix it? wrapping in ngZone.run() didn't help.Arredondo
Wrapping the API Callback into the ngZone.run statement fixed the issue.Arredondo
Probably the same effect, as ngZone is the core of change detection.Aside
A million thanks to stackoverflow.com/users/4716245/richard-matsen and stackoverflow.com/users/7767044/juri .. I was stuck in a similar situation for like 8 hours and finally running the code inside ngZone did the trick. Lesson learned : When any event occurs outside Angular's Zone then it goes out ngZone. To react to that event ( and trigger change detection etc ) , one has to wrap the reaction code inside ngZone.run :)Rory

© 2022 - 2024 — McMap. All rights reserved.