Reputation: 91
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
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
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