ion-input value [(ngModel)] is not getting updated with relative component member variable change

Issue

I am new and getting experience with Ionic framework. While coding with ionic I got stuck at one issue which is given below

Ion page / template code

<ion-item>
  <ion-label floating>{{ addressLabel }}</ion-label>
  <ion-input name="address" id="addressField" type="text" required [(ngModel)]="address" #userAddress></ion-input>
</ion-item>
<div #locationMap id="locationMap"></div>

Map component code to update address value on marker drop

onMarkerDrop(event) {
    let latcurr = event.latLng.lat();
    let longcurr = event.latLng.lng();
    this.initializeCurrent(latcurr, longcurr);
    let latLng = {lat: event.latLng.lat(), lng: event.latLng.lng()};
    let infoWindow = new google.maps.InfoWindow();
    infoWindow.open(this.map, this.marker);
    infoWindow.close();
  }

  initializeCurrent(latcurr, longcurr) {
    this.currgeocoder = new google.maps.Geocoder();
    if (latcurr != '' && longcurr != '') {
        let myLatlng = new google.maps.LatLng(latcurr, longcurr);
        this.currgeocoder.geocode({'location': myLatlng}, (results, status) => {
          if (status == google.maps.GeocoderStatus.OK) {
              this.address = results[0].formatted_address; 

// From above I am getting new location address on marker drop but that new address is not getting reflected at address input box.

              console.log(this.address);
              setTimeout(() => { 
                this.userAddress.setFocus();
                this.keyboard.close();
              }, 500);
          } else {
              alert('Geocode was not successful for the following reason: ' + status);
          }
      });
    }
  }

I am not getting what I am doing wrong. Please guide me through the right way. Thank you.

Solution

This happens because address is modified by the callback function that is passed to google.maps.Geocoder.prototype.geocode. The callback is asynchronously dispatched by the geocode method outside of the Angular zone.

Angular uses zone.js, a side project of the Angular team, to determine a logical call context across function calls in an attempt to more efficiently handle change detection. By tracking the current zone a framework specific asynchronous programming abstraction, used solely by Angular, the framework reduces the number of times it needs to re-render the view in the 95% happy path.

Code running outside of this context will not trigger an update of the view.

This is a pain.

To work around it, you can need to wrap your code in an Angular zone. This will flow the change detection across the call graph.

Here is what that would look like

import {Component, NgZone} from '@angular/core';

@Component({
  // boilerplate
})
export class MapComponent {
  constructor(readonly ngZone: NgZone) {}

  initializeCurrent(latcurr, longcurr) {
    if (!latcurr || !longcurr) {
      return;
    }
    
    this.currgeocoder = new google.maps.Geocoder();

    const latlng = new google.maps.LatLng(latcurr, longcurr);

    this.currgeocoder.geocode({'location': latlng}, ([{formatted_address}], status) => {
      if (status === google.maps.GeocoderStatus.OK) {
        // Create a zone right here.
        this.ngZone.run(() => {
          // changes will be detected because we are in a zone.
          this.address = formatted_address;
        });
        // The scope of a user-created zone should be as limited as possible.
        console.log(this.address);
        setTimeout(() => {
          this.userAddress.setFocus();
          this.keyboard.close();
        }, 500);
      } 
      else {
        alert(`Geocode was not successful for the following reason: ${status}`);
      }
    });
  }
}

After encountering this, one often begins to attribute all sorts of unrelated bugs one makes to issues around zones when they are in fact unrelated.

The reason an explicit zone needs to be created above is that there is no logical connection between the framework and the mechanism or timing by which geocode calls the callback you supply it.

Most of the time, approaching 100% in a well written application, the zone will be propagated automatically and implicitly because your asynchronous code should be within Promises or Observables. These are canonical asynchronous primitives that the framework is aware of (it monkey-patches them to hook in…). There is always a zone in an Angular app, but you don’t usually care.

Be very careful to only use zones deliberately when you know you need one.

If you start using them superstitiously, your app will slow down, you will not understand what is happening, your code will decrease in quality dramatically, and you will go mad.

Be conservative.

Addendum:

As I mentioned, a well written app will not use explicit zones often, instead favoring standard APIs like Promises and semi-standard APIs like Observables.

While I tend to favor Promises, or at least it feels like I do in Angular since most Angular Observables should be Promises (XHR anyone?), the correct way to raise the level of abstraction in this case is actually to use Observables.

The reason is that that the callback is called multiple times.

So we can actually rewrite this code, removing the low-level, and often error prone use of zone by adapting the geocode API.

Here is how

import {Component} from '@angular/core';
import Rx from 'rxjs';
import {tap} from 'rxjs/operators';

@Component({
  // boilerplate
})
export class MapComponent {
  initializeCurrent(latcurr, longcurr) {
    if (!latcurr || !longcurr) {
      return;
    }
    
    this.currgeocoder = new google.maps.Geocoder();

    const latlng = new google.maps.LatLng(latcurr, longcurr);
    
    const addressObservable = new Rx.Observable(observer => {
      this.currgeocoder.geocode({'location': latlng}, ([{formatted_address}], status) => {
        if (status === google.maps.GeocoderStatus.OK) {
          observer.next(formatted_address);
        }
        else {
          observer.error(`Geocode was not successful for the following reason: ${status}`);
        }
      });
    });

    addressObservable.pipe(tap(() => {
        setTimeout(() => {
          this.userAddress.setFocus();
          this.keyboard.close();
        }, 500);
      }))
      .subscribe(address => {
          this.address = address;
      });
  }
}

What is remarkable about this is how much simpler the code becomes. When observables are used to represent asynchronous streams of information, they can be extraordinarily elegant. That is, after all, what they were designed represent. When they are used to represent singular, possibly even synchronous values, or synchronous streams, the degree of impedance mismatch is disgusting.

Framework designers, and even more specification pushers, should listen to Erik Meijer, the creator of Rx and use the right tool for the right job instead of giving Observables a bad name by pushing them (pun intended?) at tasks like representing the number 1.
Right now they are willfully ignoring him and creating absolute rubbish like the Reactive Streams specification.

But Observables are amazing at what they were designed to be amazing at.

In parting:

I’m saying all this because I honestly expected to hate the Rx version I just wrote.

Angular, the laughable hype around the Reactive Streams specification, and the way the technology was proposed for standardization into JavaScript (i.e "it must allow synchronous observation because it must be able to replace all libraries and control structures"), made me hate Rx, made me not want to use it for anything, and that is a sad thing.

Answered By – Aluan Haddad

This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply

(*) Required, Your email will not be published