Expression has changed after it was checked

In our item component, we set the element role based on whether item is empty or not. If it's empty, then no role set; if not, role set as a listbox.

  empty(): boolean {
    return this.items && this.items.length === 0;
  }
  
  set role() {
    this.role = this.empty ? null : 'listbox'
  }

When page is loaded, we would get this error message:

EXCEPTION: Expression 'role' has changed after it was checked. 

The reason is that every round of change detection is followed immediately by another round to verify that no bindings have changed since the end of the first. In this example, role is changed by a call to set role after change detection happened. The problem is that set role changes the binding but does not trigger a new round of change detection, thus the error. It certainly could run another change detection cycle to synchronize the application state with the user interface. But then the same thing could happen again during state updates. Angular does this to prevent an infinite loop of change detection runs.

What could we do to break that pattern? Angular is notified about async events based on zone (provided by zone.js). There is a method named runOutsideAngular , which runs in a zone other than the Angular zone. No zone, no notification means no change detection. Here is how to inject NgZone and runs set role outside of the Angular zone:

constructor(public zone: NgZone) {
  zone.runOutsideAngular(() => {
    this._role = this.empty ? null : 'listbox';
  });
}

Now we update the role, but we’re doing so asynchronously and outside of the Angular zone. This guarantees that during change detection and the following check the getter role returns the same value. And when Angular reads the role value during the next change detection cycle, the value will be updated and the changes will be reflected.