Why the "ExpressionChangedAfterItHasBeenCheckedError" occurs in my angular app?

87 Views Asked by At

I just did a carousel like component and I used it like a child in a parent component that passes it an Observable. Here is the code, first the child:

<div #container *ngIf="name && (carouselData$ | async) as data" class="viewport-container">
  <h5>{{name}}</h5>
  <cdk-virtual-scroll-viewport orientation="horizontal" [itemSize]="container.offsetWidth/itemsPerPage"
  [minBufferPx]="container.offsetWidth*2" [maxBufferPx]="container.offsetWidth*2"
  [ngStyle]="{'min-height': ((container.offsetWidth / itemsPerPage) * (16 / 9)) + 'px'}" class="viewport">

    <mat-grid-list gutterSize="13" [ngStyle] ="{'width': (container.offsetWidth/itemsPerPage)*data.results.length +'px'}"
    rowHeight="9:16" [cols]="data.results.length" >
      <mat-grid-tile *cdkVirtualFor="let item of data.results">
        <img src="https://images.org/{{item.image_path}}" alt="...">
      </mat-grid-tile>
    </mat-grid-list>

  </cdk-virtual-scroll-viewport>
  <button mat-icon-button color="primary" aria-label="Previous Page" (click)="scroll('start')">
    <mat-icon>navigate_before</mat-icon>
  </button>
  <button mat-icon-button color="primary" aria-label="Next Page" (click)="scroll('end')">
    <mat-icon>navigate_next</mat-icon>
  </button>
</div>

The .ts of the component above:

export class CarouselComponent implements OnInit {

  constructor(private breakpoint$: BreakpointObserver) { }

  @ViewChild(CdkVirtualScrollViewport) viewPort!: CdkVirtualScrollViewport;

  @Input() name!: string;

  @Input() carouselData$!: Observable<any>;

  itemsPerPage = 0;

  ngOnInit(): void {
    this.setItemsPerPage();
  }

  scroll(direction: string) {
    const currentOffset = this.viewPort.measureScrollOffset();
    const pageSize = this.viewPort.elementRef.nativeElement.offsetWidth;
    const itemSize = pageSize/this.itemsPerPage;
    let offsetDelta = 0;

    switch (direction) {
      case 'start':
        offsetDelta = -pageSize + (itemSize - (currentOffset % itemSize));
        break;
      case 'end':
        offsetDelta = pageSize - (currentOffset % itemSize);
        break;
    }

    if (currentOffset % itemSize === 0 && direction === 'start') {
      offsetDelta = -pageSize;
    }

    const newOffset = currentOffset + offsetDelta;
    this.viewPort.scrollToOffset(newOffset, 'smooth');
  }

  setItemsPerPage() {
    this.breakpoint$.observe([
      Breakpoints.XSmall,
      Breakpoints.Small,
      Breakpoints.Medium,
      Breakpoints.Large
    ]).subscribe(result => {
      if (result.matches) {
        if (result.breakpoints[Breakpoints.XSmall]) {
          this.itemsPerPage = 2;
        }
        if (result.breakpoints[Breakpoints.Small]) {
          this.itemsPerPage = 3;
        }
        if (result.breakpoints[Breakpoints.Medium]) {
          this.itemsPerPage = 4;
        }
        if (result.breakpoints[Breakpoints.Large]) {
          this.itemsPerPage = 6;
        }
      }
    });
  }

}

And its parent:

<div>
  <app-carousel [name]="itemsData.name" [carouselData$]="itemsData.items$"></app-carouasel>
</div>

parent .ts:

export class ParentComponent implements OnInit {

  constructor(private itemsService: ItemsService) { }

  itemsData: any = { name: 'Trending', items$: undefined };
  

  ngOnInit(): void {
    this.getData();
  }

  getData(){
    this.itemsData.items$ = this.itemsService.getItemsData();
  }
}

I tried to detect the changes every time a breakpoint was reached, but it didn't work (You have to inject this thing: ChangeDetectorRef)

setItemsPerPage() {
    this.breakpoint$.observe([
      Breakpoints.XSmall,
      Breakpoints.Small,
      Breakpoints.Medium,
      Breakpoints.Large
    ]).subscribe(result => {
      if (result.matches) {
        if (result.breakpoints[Breakpoints.XSmall]) {
          this.itemsPerPage = 2;
        }
        if (result.breakpoints[Breakpoints.Small]) {
          this.itemsPerPage = 3;
        }
        if (result.breakpoints[Breakpoints.Medium]) {
          this.itemsPerPage = 4;
        }
        if (result.breakpoints[Breakpoints.Large]) {
          this.itemsPerPage = 6;
        }

        // Manually trigger change detection
        this.cdr.detectChanges();
      }
    });
  }

And the error is:

ERROR Error: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '285.5'. Current value: '278'.. Find more at https://angular.io/errors/NG0100
    at throwErrorIfNoChangesMode (core.mjs:7940:1)
    at bindingUpdated (core.mjs:12832:1)
    at Module.ɵɵproperty (core.mjs:14565:1)
    at CarouselComponent_div_0_Template (carousel.component.html:3:57)
    at executeTemplate (core.mjs:9717:1)
    at refreshView (core.mjs:9580:1)
    at refreshEmbeddedViews (core.mjs:10731:1)
    at refreshView (core.mjs:9604:1)
    at refreshComponent (core.mjs:10777:1)
    at refreshChildComponents (core.mjs:9376:1)

Also, trying more things, I was getting like an infinite loop of changes detection, but I am not sure. Help!!!

1

There are 1 best solutions below

1
Bhavy Ladani On

There are a couple of things that I see from your code you should try here:

Set a default value for itemsPerPage to avoid the error during the first change detection cycle. if nothing then set to zero.

Move setItemsPerPage logic to ngAfterViewInit. Also import it at the top. It should be like this.

ngAfterViewInit(): void {
  this.setItemsPerPage();
}

Try this and see if the error still arises.