Angular Expression has changed after it was checked

Viewed 630

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

1 Answers

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

Related