camel
camel

Reputation: 1473

Angular Expression has changed after it was checked

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

Answers (1)

Ahfa
Ahfa

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

Related Questions