Venfi Oranai
Venfi Oranai

Reputation: 61

Angular 17 - ExpressionChangedAfterItHasBeenCheckedError thrown despite calling detectChanges

I'm experiencing a slight issue with Angular and its change detection. I've got a very simple form that allows for additional input containers to be added. Yet, every time I click the add button I get an ExpressionChangedAfterItHasBeenCheckedError thrown in the console. When using a standard ngFor the error is thrown in the console, but the new input is still displayed. However, when using Angular's new @for option I get the error thrown in the console, but it also isn't displayed. In both scenarios, I made sure to call a detectChanges (I also tried markForCheck), but it made no difference.

public properties: Map<number, string> = new Map<number, string>();

public addProperty() {
  const id: number = this.properties.size ?
    Array.from(this.properties.keys()).reduce((a, b) => a > b ? a : b) + 1 : 1;

  this.properties.set(id, 'placeholder');
  this.changeDetectorRef.detectChanges();
}
<button class="btn btn-primary" (click)="addProperty()">+</button>

<div class="d-flex flex-column">
  <ng-container *ngFor="let attribute of properties.entries()">
    <span>{{ attribute[0] }}</span>
  </ng-container>
</div>

I'd greatly appreciate any insight into the issue, thanks in advance.

I've tried using both a ngFor and Angular's new @for option, the only difference between the two is that when using the @for the new data isn't displayed in addition to the console error. I've also attempted calling the change detector manually but it had no impact.

Upvotes: 3

Views: 180

Answers (1)

Naren Murali
Naren Murali

Reputation: 58011

Angular ngFor is designed for arrays in particular, so I guess change detection is getting confused due to map store by reference. You can convert it into an array before inputing to the ngFor which also gets rid of the change detection issue. Please find below a working example!

You can so access the properties.keys() or properties.values() if you specifically need just the key or the value!

import { CommonModule } from '@angular/common';
import { ChangeDetectorRef, Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import 'zone.js';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule],
  template: `
    <button class="btn btn-primary" (click)="addProperty()">+</button>
    <div class="d-flex flex-column">
      {{properties.entries() | json}}
      <ng-container *ngFor="let attribute of mapEntriesToArray;trackBy: trackByFn ">
        <span>{{ attribute[0] }}</span>
      </ng-container>
    </div>
  `,
})
export class App {
  public properties: Map<number, string> = new Map<number, string>();

  constructor(private changeDetectorRef: ChangeDetectorRef) {}

  get mapEntriesToArray() {
    return Array.from(this.properties.entries());
  }

  public addProperty() {
    const id: number = this.properties.size
      ? Array.from(this.properties.keys()).reduce((a, b) => (a > b ? a : b)) + 1
      : 1;

    this.properties.set(id, 'placeholder');
  }

  trackByFn(index: number, item: any) {
    return index;
  }
}

bootstrapApplication(App);

stackblitz

Upvotes: 1

Related Questions