Angular2
Angular2

Reputation: 91

Angular2 delay to update view

I've noticed a weird behaviour with my components using Angular 2. My views take few seconds to be updated once my component changes the model included as my providers. Even if the data from the API is a single data.

For example:

I have as my provider the model called UserModel Inside my component I get data from API and then update this model which is also inside my view. After getting the response from server, it still takes few seconds to update my view, and sometimes it doesn't update, just after I click on any text controller on the same page and then my view is updated after any text gets focus.

Has anyone already seen this ? What could I be doing wrong ?

Card Component

public createCard(model:CardModel):Promise<any>{
    var context = this;
    return new Promise((resolve, reject) => {
    this.stripe.createToken(model)
        .then(function(token){
            model.token = token;
            context.saveCard("./card", model, true)
                    .then(data => resolve(data))
                    .catch(error => reject(error));
        })
        .catch(error => reject(error));
});

Stripe Service

public createToken(model:CardModel):Promise<any>{
    //I get callback and convert return it as promise
    return new Promise((resolve, reject) => {
            //this function is the one from stripe.js, it is not promise
            this.stripe.card.createToken(model, function(status, response){
            if(status == 200){
                resolve(response.id);
            }else{
                reject(response.error.message);
            }
        });
    });
}

If you notice the function createToken returns as callback because it is a Strip.js function, and then I convert it to Promise to return it to createCard. But once all functions are completed my zone is not changed. If I remove this.stripe.card.createToken and return a simple resolve() using timeout, it works fine. So I believe the issue is when have a async function returning callback inside a Promise. But I have no clue how to deal with it.

Upvotes: 9

Views: 2858

Answers (2)

Andre Maia
Andre Maia

Reputation: 152

It's been several years since this issue is posted here, but I had a similar problem recently because my promises were running "outside" Angular.

As mentioned in a comment this is similar to the digest cycle issue in Angular 1 where to "force" Angular to recognize anything it wasn't "seeing" we used $scope.$apply()

In Angular 2 NgZone solves these problems. It is defined by the documentation as: "An injectable service for executing work inside or outside of the Angular zone.": NgZone

So to solve the problem it is necessary to ensure that the code runs by calling ngZone like this:

this.ngZone.run(() => {
    // some async code here
});

You need import NgZone from @angular/core and add it on the constructor:

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

constructor(private ngZone: NgZone) {
    // some code here
}

So, the code in this 5 years old question can be like this: (notice the "this.ngZone.run" inside the Promises)

public createCard(model:CardModel):Promise<any>{
    var context = this;
    return new Promise((resolve, reject) => {
        this.ngZone.run(() => {
            this.stripe.createToken(model).then(function(token) {
                model.token = token;
                context.saveCard("./card", model, true)
                    .then(data => resolve(data))
                    .catch(error => reject(error));
            })
            .catch(error => reject(error));
        });
    });
}


public createToken(model:CardModel):Promise<any>{
    //I get callback and convert return it as promise
    return new Promise((resolve, reject) => {
        this.ngZone.run(() => {
            //this function is the one from stripe.js, it is not promise
            this.stripe.card.createToken(model, function(status, response){
                if(status == 200){
                    resolve(response.id);
                }else{
                    reject(response.error.message);
                }
            });
        });
    });
}

But I would prefer to refactor those Promises and use async/await and make it look more like this to use a single ngZone.run call:

createAndSaveCard() {
    this.ngZone.run(async() => {
        const token = await createToken();
        const card = await createCard(token);
        await this.saveCard();
    });
}

Upvotes: 0

evanjmg
evanjmg

Reputation: 3791

Seems you are using a third party library that does not work well with Angular 2. I had the same issue with StripeJS. The issue is related to Angular's Zone.js and Lifecycle - it's very similar to that bad digest cycle issue in Angular 1. Angular has detailed documentation on the solution here: https://angular.io/api/core/ChangeDetectorRef#example-live-demo

In order to solve it, you have to implement your own lifecycle loop and add your third party library into the change detection. My implementation for StripeJS (also remember to destroy the setInterval with clearInterval:

import {
  Component,
  Input,
  Output,
  EventEmitter,
  AfterContentInit,
  ChangeDetectorRef,
  ChangeDetectionStrategy,
  ViewContainerRef,
  OnDestroy
} from '@angular/core';

@Component({
  selector: 'v-payment-form', // <payment-form></payment-form>
  changeDetection: ChangeDetectionStrategy.OnPush,
  templateUrl: './payment-form.component.html'
})
export class PaymentFormComponent implements AfterContentInit, OnDestroy {
  checker: any;
  constructor(private changeDetectorRef: ChangeDetectorRef) {
    this.checker = setInterval(() => {
      // the following is required, otherwise the view will not be updated
      this.changeDetectorRef.markForCheck();
    }, 75);
  }
  addPaymentMethod() {
    Stripe.card.createToken(cardDetails)
    this.changeDetectorRef.detectChanges()
  }
  ngAfterContentInit() {
    // ..load stripe js here - I use scriptjs 
  }
  ngOnDestroy(): void {
    clearInterval(this.checker)
  }
}

Upvotes: 0

Related Questions