I have a Angular 9, with Ivy on, component with more than one mat-table. One table has rows with input fields bound to a reactive form. I have the table bound to the form array. This works great... however, when I load the formarray with values I get the fun "ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value for 'ng-valid': 'true'. Current value: 'false'" error.
Also, if I set changeDetection: ChangeDetectionStrategy.OnPush on the component, it works fine, but I lose change detection obviously...
Here is my setup (trimmed down):
The reactive form:
quoteForm = new FormGroup({
QuoteNumber:new FormControl(''),
ContractTerm:new FormControl(''),
ContractStartDate:new FormControl(''),
QuoteName: new FormControl('', Validators.required),
...
Items: this.fb.array([])
});
async ngOnInit(): Promise<void> {
...
this.QuoteItems = await this.CIM.GetQuoteItems(this.CurrentQuoteNumber);
const itemsArray = this.quoteForm.get('Items') as FormArray;
...
this.QuoteItems.forEach(item => {
let NewObj = this.fb.group({
isNew:false,
...
});
...
itemsArray.push(NewObj);
});
}
this.ItemdataSource=new MatTableDataSource(this.quoteForm.get('Items').value);
this.ItemdataSource.paginator = this.paginator.toArray()[0];
}
The table HTML (trimmed):
<table mat-table [dataSource]="ItemdataSource" multiTemplateDataRows formArrayName="Items">
...
<tr mat-header-row *matHeaderRowDef="ItemColumns"></tr>
<tr mat-row *matRowDef="let row; columns: ItemColumns;" class="element-row" [class.expanded-row]="expandedElement === row" (click)="expandedElement = expandedElement === row ? null : row"></tr>
<tr mat-row *matRowDef="let row; columns: ['SerialNumbers']" class="detail-row"></tr>
</table>
</div>
<mat-paginator [pageSizeOptions]="[20, 40, 100]" showFirstLastButtons></mat-paginator>
Not sure how to troubleshoot this? I get the error after I load the form with items. I could maintain two lists... one for the table and one for the form, but what a PIA that would be? What am I missing?
If I'm not missing something important in what you describe, you should try
detectChanges
after your async code evaluatesSo, your component would be something like:
import { ..., ChangeDetectorRef } from '@angular/core';
@Component({
...
changeDetection: ChangeDetectionStrategy.OnPush
})
export class YourComponent ... {
constructor(private cdRef: ChangeDetectorRef) { }
async ngOnInit(): Promise<void> {
...
this.QuoteItems = await this.CIM.GetQuoteItems(this.CurrentQuoteNumber);
const itemsArray = this.quoteForm.get('Items') as FormArray;
...
this.QuoteItems.forEach(item => {
let NewObj = this.fb.group({
isNew:false,
...
});
...
itemsArray.push(NewObj);
});
}
this.ItemdataSource = new MatTableDataSource(this.quoteForm.get('Items').value);
this.ItemdataSource.paginator = this.paginator.toArray()[0];
this.cdRef.detectChanges();
}
What's happening?
In dev mode, angular checks / compares the view right after it is rendered, to see if any data changed after last change detection and render. So, the error is triggered because data changed after view content was last rendered.
By setting onPush strategy, you are telling angular to trigger change detection ONLY when an @Input is changed, otherwise this node is ignored. So that fixes the error but you still need to trigger change detection manually, in order to display updated data.