ArunM
ArunM

Reputation: 2314

ExpressionChangedAfterItHasBeenCheckedError - Shared Service

Why do I get the following warning in console ? Everything seems to be working as expected but Angular complains. What would be a solution for this issue ?

StackBlitz is here

I know a possible solution is to communicate event via parent child communication instead of using service but that is not an option for me since this is an isolation of a problem in a larger code base.

Error Message

ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'ngIf: false'. Current value: 'ngIf: true'.
    at viewDebugError (core.js:20439)
    at expressionChangedAfterItHasBeenCheckedError (core.js:20427)
    at checkBindingNoChanges (core.js:20529)
    at checkNoChangesNodeInline (core.js:23400)
    at checkNoChangesNode (core.js:23389)
    at debugCheckNoChangesNode (core.js:23993)
    at debugCheckDirectivesFn (core.js:23921)
    at Object.eval [as updateDirectives] (AppComponent.html:6)
    at Object.debugUpdateDirectives [as updateDirectives] (core.js:23910)
    at checkNoChangesView (core.js:23288)

app.component.html

<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
  <h1  *ngIf="mainSectionContent?.buttontext?.length > 0">
    Welcome to {{ title }}!
  </h1>
  <app-employee></app-employee>
</div>

AppComponent

export class AppComponent implements OnInit {

  title = 'expression-changed';
  mainSectionContent:MainSectionContent;
  contentAnnounce$:Observable<MainSectionContent>;

  constructor(private mainContentService:MaincontentService) { }

  ngOnInit(): void {
    this.contentAnnounce$ = this.mainContentService.contentAnnounce$;
    this.contentAnnounce$.subscribe(mainSectionContent => 
      {
        this.mainSectionContent = mainSectionContent
      }
      );
  }
}

EmployeeComponent

export class EmployeeComponent implements OnInit {

  constructor(private mainSectionContentService:MaincontentService) { }

  ngOnInit() {
    this.mainSectionContentService.announceContent({
      mainheading:'Employee Manger',
      mainsubheading:'To manage PrivilegeManager Employees',
      sectionheading:'Employee List',
      buttontext:'Create Employee'
    });
  }
}

MaincontentService

@Injectable({
  providedIn: 'root'
})
export class MaincontentService {

  private contentAnnounce = new Subject<MainSectionContent>();
  contentAnnounce$ = this.contentAnnounce.asObservable();

  constructor() { }

  announceContent(content:MainSectionContent){
    this.contentAnnounce.next(content);
}  
}

Upvotes: 1

Views: 1610

Answers (4)

Galdor
Galdor

Reputation: 1985

You could move the critical part to a new child component like

app-component.html

<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
  <app-welcome [title]="title"></app-welcome>
  <app-employee></app-employee>
</div>

app-component.ts

export class AppComponent implements OnInit {

  title = 'expression-changed';
}

welcome-component.html

<h1  *ngIf="mainSectionContent?.buttontext?.length > 0">
  Welcome to {{ title }}!
</h1>

welcome-component.ts

export class WelcomeComponent implements OnInit {

  @Input() title!: string;
  mainSectionContent:MainSectionContent;
  contentAnnounce$:Observable<MainSectionContent>;

  constructor(private mainContentService:MaincontentService) { }

  ngOnInit(): void {
    this.contentAnnounce$ = this.mainContentService.contentAnnounce$;
    this.contentAnnounce$.subscribe(mainSectionContent => 
      {
        this.mainSectionContent = mainSectionContent
      }
      );
  }
}

Then the error should disappear.

Upvotes: 0

Vixson
Vixson

Reputation: 739

Ok this ExpressionChangedAfterItHasBeenCheckedError: error has been going on for a while now and it seems the only and best hack-around is to use the:

setTimeout(() => {...changes...});

---or---

Promise.resolve(null).then(() => {...changes...});

But to archive that you have to implement this on every component or area, you want to make those changes. That could be a real hassle.

So I think the best is to bind class display: none on that HtmlElement you want to hide.


app.component.scss | css

.d-none {
  display: none
}

app.component.html

<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
  <h1 [class.d-none]="mainSectionContent?.buttontext?.length > 0">
    Welcome to {{ title }}!
  </h1>
  <app-employee></app-employee>
</div>

app.component.ts | Anywhere

ngOnInit() {
  this.mainSectionContentService.announceContent({
    mainheading:'Employee Manger',
    mainsubheading:'To manage PrivilegeManager Employees',
    sectionheading:'Employee List',
    buttontext:'Create Employee'
  });
}

Upvotes: 1

Anshul Gupta
Anshul Gupta

Reputation: 83

I used to face the same problem, but this works in a angular native way, you can use ChangeDetectorRef of angular/core class follow the code below:

constructor(private cdf: ChangeDetectorRef){}

And add this line after you received the data in controller, in your case:

this.mainSectionContent = mainSectionContent
this.cdf.detectChanges();

What this does is asks the angular service to recheck the changes on DOM.

Upvotes: 3

Dmitriy Snitko
Dmitriy Snitko

Reputation: 960

The case is that ngOnInit of EmployeeComponent is called after *ngIf="mainSectionContent?.buttontext?.length > 0" already checked.

First parent component is checked, then goes child EmployeeComponent, that changes value, already used to render parent component in same event loop iteration.

This looks like a hack, but you have to call announceContent after first loop passes. You can try call it just after the first event loop finishes:

  ngOnInit() {
    setTimeout(() => {
        this.mainSectionContentService.announceContent({
        mainheading:'Employee Manger',
        mainsubheading:'To manage PrivilegeManager Employees',
        sectionheading:'Employee List',
        buttontext:'Create Employee'
        });
      }, 
    0);
  }

Upvotes: 5

Related Questions