Reputation: 1473
I'm getting the well known error in my Angular app, but not sure why it happens and how to fix it. I was trying a couple of ways including adding setTimeout, delay(0), switching to different hook but any of them seems to work in my case.
Problem description:
I have a list of products and on click single product can be added to the cart with selected products //product.list.component.ts
addToProductCart(product: IProduct) {
this.productsService.addProductToSelectedProducts(product);
}
The service looks like below:
//product.service.ts
@Injectable({
providedIn: 'root'
})
export class ProductsService {
selectedProducts: BehaviorSubject<IProduct[]> = new BehaviorSubject<IProduct[]>([]);
product = this.selectedProducts.asObservable();
constructor(private http: HttpClient) { }
getProductsList(): Observable<IProduct[]> {
return this.http.get<IProduct[]>(`${environments.environment.baseUrl}/products`);
}
patchProductLikes(id: number, payload: Partial<IProduct>): Observable<number> {
return this.http.patch<number>(`${environments.environment.baseUrl}/products/${id}`, payload);
}
addProductToSelectedProducts(product: IProduct) {
this.selectedProducts.next([...this.selectedProducts.value, product]);
}
clearSelectedProducts(): void {
this.selectedProducts.next([]);
}
removeSelectedProduct(products: IProduct[]): void {
this.selectedProducts.next(products);
}
}
When product is selected on my header the product count is increased and displayed on cart icon:
//header.component.html
<span (click)="openDialog()" #openCartButton>
<mat-icon matBadge="{{selectedProductsCount}}"matBadgePosition="above after">
shopping_cart
</mat-icon>
</span>
//header.component.ts
openDialog() {
this.dialog.open(CartDetailsComponent, {
width: '450px',
height: '650px',
data: {
positionRelativeToElement: this.openCartButton
}
});
}
getSelectedProductsCount(): void {
this.productsService.product.subscribe((products) => {
this.selectedProductsCount = products.length;
});
}
If header cart icon is clicked the dialog with selected product is opened, and if there are no selected products then empty cart placeholder should be displayed:
//cart-details.component.html
<div *ngIf="products.length > 0 else emptyCart">
<h5 mat-dialog-title>Total order</h5>
<div mat-dialog-content class="product" [@loadProducts]="'in'">
<ul>
<li *ngFor="let groupedProducts of selectedProducts | keyvalue" class="product__product-item">
<div *ngFor="let prod of groupedProducts.value | productPipe; let i = index" class="product-details-container">
<div>
<img [src]="prod.image" alt="Product photo" class="product-details-container__product-img">
</div>
<div class="product-info">
<p>{{prod.name}}
<span class="product-info__price">${{prod.price}}</span>
</p>
<p>
{{prod.productMaterial}}
</p>
<p>
{{prod.color}}
</p>
<p @deleteProduct>Amount: {{groupedProducts.value.length}} </p>
<p>Total: ${{prod.price * groupedProducts.value.length}}</p>
<div class="product-actions-container">
<a (click)="deleteProduct(prod)" class="delete-product">Delete</a>
<a (click)="viewProductDetails(prod)" class="view-details">View details</a>
</div>
</div>
</div>
</li>
<span>SUM: ${{totalSum}}</span>
</ul>
</div>
</div>
<ng-template #emptyCart>
<div class="empty-bag-container">
<mat-icon svgIcon="empty-bag" class="empty-bag-container__empty-bag-icon"></mat-icon>
<h4 class="empty-bag-container__empty-bag-heading">
YOUR BAG IS EMPTY
</h4>
<span class="empty-bag-container__empty-bag-details"> Looks like you haven’t made your choice yet.
Check out 100+ styles for everyone!</span>
</div>
</ng-template>
//cart-details.component.ts
export class CartDetailsComponent implements OnInit, OnDestroy {
private positionRelativeToElement: ElementRef;
isOpen = false;
totalSum = 0;
totalPrices: number[] = [];
private destroySubject: Subject<boolean> = new Subject<boolean>();
selectedProductsCount: number;
selectedProducts: Record<string, IProduct[]>;
productSumPrice: number;
products: IProduct[] = [];
constructor(public dialogRef: MatDialogRef<CartDetailsComponent>,
private productsService: ProductsService,
@Inject(MAT_DIALOG_DATA) public data: { positionRelativeToElement: ElementRef }) {
this.positionRelativeToElement = data.positionRelativeToElement;
}
ngOnInit() {
const matDialogConfig = new MatDialogConfig();
const rect: DOMRect = this.positionRelativeToElement.nativeElement.getBoundingClientRect();
matDialogConfig.position = { right: `10px`, top: `${rect.bottom + 2}px` };
this.dialogRef.updatePosition(matDialogConfig.position);
this.getSelectedProducts();
this.calculatePrices();
}
ngOnDestroy() {
this.destroySubject.next(true);
}
close() {
this.dialogRef.close();
}
deleteProduct(product: IProduct) {
const prodId: number = product.id;
this.selectedProducts[prodId] = this.selectedProducts[prodId].slice(0, this.selectedProducts[prodId].length - 1);
const index: number = this.products.map(x => {
return x.id;
}).indexOf(product.id);
this.products.splice(index, 1);
this.productsService.removeSelectedProduct(this.products);
this.calculatePrices();
}
viewProductDetails(product: IProduct): void {
console.log(product);
}
animateCurrentItem(product: IProduct) {
console.log(product, 'animation');
}
calculatePrices() {
if (this.products.length > 0) {
this.totalPrices = [];
Object.values((this.selectedProducts))
.map((prod) => {
if (prod.length > 0) {
(prod as IProduct[]).map((p) => {
this.totalPrices.push(Number(p.price));
});
}
});
if (this.totalPrices.length > 0) {
this.totalSum = this.totalPrices.reduce((prev, cur) => {
return prev + cur;
});
} else {
this.totalSum = 0;
this.productsService.clearSelectedProducts();
}
}
}
getSelectedProducts() {
this.productsService.product
.pipe(
delay(0),
startWith([]),
takeUntil(this.destroySubject),
)
.subscribe((products) => {
if (products.length > 0) {
this.products = products;
this.productSumPrice = _.sumBy(products, (prod) => parseFloat(prod.price));
this.selectedProductsCount = _.sum(Object.values(_.countBy(products, product => product.id)));
this.selectedProducts = _.groupBy(products, 'id');
}
});
}
}
And here the error occurs. If cart is empty (meaning products.length === 0) the <ng-template #emptyCart>
is displayed but with the error:
ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'loading-background: false'. Current value: 'loading-background: true'.
The error is about loading-background in ngx-ui-loader lib witch I use in app.module:
//app.module
(...)
import { NgxUiLoaderModule, NgxUiLoaderHttpModule, NgxUiLoaderConfig, SPINNER, POSITION, PB_DIRECTION } from 'ngx-ui-loader';
imports: [
...
NgxUiLoaderModule.forRoot(ngxUiLoaderConfig),
NgxUiLoaderHttpModule,
]
Any idea what cause the issue and how to fix it and avoid in the future? I was traying to reproduce it on stackblitz but with no luck :). Although maybe it will help understand my issue ;P https://stackblitz.com/edit/angular-h3xyop?file=src%2Fapp%2Fproduct-list%2Fproduct-list.component.ts
Upvotes: 2
Views: 1345
Reputation: 180
This is because of your view changed after rendering. You need to use changeDetectorRef to detechChanges. add in constructor
construct(private ref: changeDetectorRef)
{}
and after change you add
this.ref.detectChanges();
https://angular.io/api/core/ChangeDetectorRef
Upvotes: 1