问题
I have an Angular Material Expansion Panel Query list:
@ViewChildren(MatExpansionPanel)
matExpansionPanelQueryList: QueryList<MatExpansionPanel>;
and a simple array:
questions: [];
The Expansion Panels are generated with an *ngFor
: simplified for example:
<ng-container *ngFor="let question of questions>
<mat-expansion-panel
...
When I extend the questions array, I want to open up the last expansion panel.
extendQuestion() {
this.matExpansionPanelQueryList.changes.subscribe(
change => {
change.last.open();
}
);
this.questions.push('some-new-item');
}
This works just fine - I insert an item to the array,
the ExpansionPanels get's re-rendered, a new Panel gets created and it actually opens up - my problem is that it generates the following error in the console:
ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has
changed after it was checked. Previous value: 'mat-expanded: false'. Current
value: 'mat-expanded: true'.
Is there any way to avoid this?
I have tried using changeDetectorRef
and markForCheck()
in the subscription, but the error message did not go away (and honestly, I am 100% sure what's the exact problem here).
Update: Stackblitz example of the issue (click the "+" button)
回答1:
From angularindepth:
This is a cautionary mechanism put in place to prevent inconsistencies between model data and UI so that erroneous or old data are not shown to a user on the page.
A running Angular application is a tree of components. During change detection Angular performs checks for each component which consists of the following operations performed in the specified order:
- update bound properties for all child components/directives
- call ngOnInit, OnChanges and ngDoCheck lifecycle hooks on all child components/directives
- update DOM for the current component
- run change detection for a child component
- call ngAfterViewInit lifecycle hook for all child components/directives
After each operation Angular remembers what values it used to perform an operation. They are stored in the oldValues property of the component view.
You might want to trigger the lifecycle hook of your component before the DOM update operation..
That should work for you:
You can add a constructor
for changedetection (cd) and call it after change.last.open();
import {ChangeDetectorRef, Component, OnInit, QueryList, ViewChildren} from '@angular/core';
import {MatDialog, MatExpansionPanel} from '@angular/material';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
questions = ['first', 'second'];
constructor(private cd: ChangeDetectorRef) {
}
@ViewChildren(MatExpansionPanel) matExpansionPanelQueryList: QueryList<MatExpansionPanel>;
extendQuestion() {
this.matExpansionPanelQueryList.changes.subscribe(
change => {
change.last.open();
this.cd.detectChanges();
}
);
this.questions.push('some-new-item');
}
}
回答2:
I have found the solution to the problem.
From the link that @iLuvLogix mentioned, forcing a change detection solved the issue:
constructor(private cd: ChangeDetectorRef) {}
this.matExpansionPanelQueryList.changes.subscribe(
change => {
change.last.open();
this.cd.detectChanges();
}
);
回答3:
You can use javascript to achieve the solution
Stackblitz Demo with javascript only
After updating the questions array, you can use javascript
,
and i am using setTimeout(()=>{}, 0)
to put this task at the end of the event-loop
so its executed after angular is done performing the DOM manipulation.
this.questions.push('some-new-item');
setTimeout(
()=>{
const lastExpansionPanel:HTMLElement =
document.getElementsByTagName('mat-expansion-panel')
[document.getElementsByTagName('mat-expansion-panel').length-1]
.querySelector('mat-expansion-panel-header');
lastExpansionPanel.click();
}, 0
);
来源:https://stackoverflow.com/questions/54823152/matexpansionpanel-expression-has-changed-after-it-was-checked-error-mat-expande