Angular 4 ExpressionChangedAfterItHasBeenCheckedError
Asked Answered
F

3

28

in ParentComponent =>

ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: ''. Current value: '[object Object]'.
            at viewDebugError (vendor.bundle.js:8962)
            at expressionChangedAfterItHasBeenCheckedError (vendor.bundle.js:8940)

Parent component Html

<div>
  <app-child-widget [allItems]="allItems" (notify)="eventCalled($event)"></app-child-widget>
<div>

Parent component

export class ParentComponent implements OnInit {

  returnedItems: Array<any> = [];
  allItems: Array<any> = [];

  constructor(
  ) { }

  ngOnInit() {
     this.allItems = // load from server...
  }

  eventCalled(items: Array<any>): void {
    this.returnedItems = items;
  }
}

Child component

@Component({
  selector: 'app-child-widget',
  templateUrl: 'child.component.html',
  styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit {
  @Output() notify: EventEmitter<any> = new EventEmitter();
  @Input() private allItems: Array<any>;

  constructor() { }

  ngOnInit() {
    doSomething();
  }

  doSomething() {
    this.notify.emit(allItems);
  }
}
Fanchie answered 22/6, 2017 at 6:28 Comment(0)
Z
44

The article Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error explains this behavior in great details

Cause

Your problem is very similar to this one but instead of updating parent property through a service you're updating it through synchronous event broadcasting. Here is the quote from the linked answer:

During digest cycle Angular performs certain operations on child directives. One of such operations is updating inputs and calling ngOnInit lifecycle hook on child directives/components. What's important is that these operations are performed in strict order:

  • Update inputs
  • Call ngOnInit

So in your case Angular updated input binding allItems on child component, then called onInit on child component which caused an update to allItems of parent component. Now you have data inconsistency. Parent component has one value while the child another. If Angular continues synchronizing changes you'll get an infinite loop. That's why during next change detection cycle Angular detected that allItems was changed and thrown an error.

Solution

It seems that this is an application design flaw as you're updating details from both parent and child component. If it's not, then you can solve the problem by emitting the event asynchronously like this:

export class ChildComponent implements OnInit {
  @Output() notify: EventEmitter<any> = new EventEmitter(true);
                                                        ^^^^^^-------------

But you have to be very careful. If you use any other hook like ngAfterViewChecked that is being called on every digest cycle, you'll end up in cyclic dependency!

Zoes answered 22/6, 2017 at 6:36 Comment(4)
will the problem be solved by asynchronous event broadcasting ? Is there anyway i can do asynchronous event broadcasting ?Fanchie
simply use setTimeout, I updated the question. But the problem is actually that you're updating allItems inside the parent and a child, that's your app design flaw, think how you can redesign itZoes
new EventEmitter(true)Formulary
@yurzui, just checked, it uses setTimeout under the hood. It will schedule a macrotask. I'm wondering if there's any way to schedule a microtask. I tried with promise.resolve as it schedules a microtask but the error persisted, needs investigationZoes
A
0

In my case, I was changing the state of my data--this answer requires you to read AngularInDepth.com explanation of digest cycle -- inside the html level, all I had to do was change the way I was handling the data like so:

<div>{{event.subjects.pop()}}</div>

into

<div>{{event.subjects[0]}}</div>

Summary: instead of popping--which returns and remove the last element of an array thus changing the state of my data-- I used the normal way of getting my data without changing its state thus preventing the exception.

Alible answered 5/2, 2018 at 19:12 Comment(1)
The two are not equivalent. One returns the last and the other the first. But point taken.Wheeling
B
0

When Getting this error, you can use a rxjs timer to delay the call for a short time.

This example uses the @angular/material stepper to load a default step from the query params in the url.

import { timer } from 'rxjs'

@Component({
  template: `
    <mat-horizontal-stepper #stepper>
      <mat-step></mat-step>
      <mat-step></mat-step>
      <mat-step></mat-step>
    </mat-horizontal-stepper>
  `
})
export class MyComponent {
  @ViewChild('stepper', { static: true })
  private stepper: MatHorizontalStepper

  public ngOnInit() {
    timer(10).subscribe(() => {
      this.activatedRoute.queryParams.subscribe(params => {
        this.stepper.selectedIndex = parseInt(params.step || 0)
      })
    })
  }
}
Byword answered 3/12, 2020 at 17:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.