I know this question has been asked before but I have tried several answers and nothing is working. I am struggling with the infamous ExpressionChangedAfterItHasBeenCheckedError
. The code is working however the error message appears in the console.
Error:
ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'disabled: false'. Current value: 'disabled: true'.
After removing the [disabled]="itemForm.invalid"
condition, this new error appears.
ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'ng-valid: true'. Current value: 'ng-valid: false'.
Description:
I have a Material Table that displays multiple rows of data. A button "Edit" is shown next to each row. The row is passed in to the onRowEdit function as an argument on the click event.
(click)="onEditItem(row)"
The onEditItem function opens a Mat Dialog component and passes in the row from the template as the Dialog Data. The Dialog data is then used to populate a FormGroup to display the values inside of a form.
I was able to determine that the error is caused by passing in the row from the template directly. When a row is not passed in, or the values are hard-coded, it works fine without the error.
For example, the "Add" button onAddRow
opens up the exact same dialog and works without the error. The Add button is shown at the table header and does not pass in a row from the template, just initializes an empty object and passes it into the dialog data. Also, if the values are simply hard-coded in the onEditRow
function (without passing the row from the template) the dialog loads and populates the form with no errors.
Attempts to fix:
I attempted to use many techniques including forcing change detection, moving the logic to AfterView init, many other answers on this site here and here as well as various suggestions provided in blog posts here and here and more. Unfortunately I am still grappling with this issue and am puzzled as to why it works when data is hard coded, but not when it is passed in from the template.
Code
Table Component
TS
// ERROR WHEN A ROW IS PASSED IN FROM THE TEMPLATE DIRECTLY
public onEditItem(item): void {
const dialogData = {
editMode: true,
selectedItem: item
// WORKS WITHOUT THE ERROR IF ITEM VALUES ARE HARD-CODED INTO THE DIALOG DATA VARIABLE
// selectedItem: { Title: 'Example', Description: 'Example description' }
};
this.openItemDetailDialog(dialogData);
}
// WORKS
public onAddItem(): void {
const dialogData = {
editMode: false,
selectedItem: {}
}
this.openItemDetailDialog(dialogData);
}
private openItemDetailDialog(dialogData): void {
const dialogRef = this.dialog.open(ItemDetailComponent, {
height: '90%',
width: '90%',
data: dialogData,
disableClose: true,
});
}
HTML
<table mat-table [dataSource]="itemsDataSource">
<ng-container [matColumnDef]="column.name" *ngFor="let column of initCols;">
<th mat-header-cell *matHeaderCellDef>
{{ column.display }}
</th>
<td mat-cell *matCellDef="let element">
<ng-container>
{{ element[column.name] }}
</ng-container>
</td>
</ng-container>
<!-- Table actions -->
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>
<div mat-header>
<button type="button" mat-button
(click)="onAddItem()">
<mat-icon>add_circle</mat-icon>
</button>
</div>
</th>
<td mat-cell class="xs" *matCellDef="let row">
<ng-container>
<button type="button" mat-button
(click)="onEditItem(row)">
<mat-icon>edit</mat-icon>
</button>
</ng-container>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [ngClass]="uiService.getClassUnsaved(deletedItems, row)"></tr>
</table>
Dialog Component
TS
ngOnInit() {
// Get currently Selected Item to populate Item Form values
this.item = this.dialogData.selectedItem;
// Initialize Item values
const itemValues = this.itemService.initItemValues(this.item);
// Initialize Item Form
this.itemService.initItemForm(itemValues);
// Get current value of Item Form
this.itemFormSub = this.itemService.itemForm$
.subscribe(itemForm => {
// Set local class member variable to populate the FormGroup in the template
this.itemForm = itemForm;
this.isLoadingResults = false;
});
}
HTML
<h1 mat-dialog-title>Item Detail</h1>
<!-- Form -->
<form [formGroup]="itemForm" (ngSubmit)="onSubmit()">
<div mat-dialog-content>
<mat-form-field appearance="outline" floatLabel="always">
<mat-label>Call Number</mat-label>
<input matInput formControlName="Title" required="true">
</mat-form-field>
<!-- Actions --->
<div mat-dialog-actions>
<button type="submit" mat-raised-button color="primary"
[disabled]="itemForm.invalid">
Save Item
</button>
</div>
</form>
Service
// Create a BehaviorSubject to track and share updates to Record Form value
private itemForm: FormGroup;
itemFormSubject: BehaviorSubject<FormGroup> = new BehaviorSubject(this.itemForm);
itemForm$ = this.itemFormSubject.asObservable();
// Initialize Item values
public initItemValues(item: Item | any): Item {
const itemValues: Item = {
Title: item && item.Title ? item.Title : null,
Description: item && item.Description ? item.Description : null,
};
return itemValues;
}
// Initialize a new Item Form and pre-populate values
public initItemForm(item: Item): void {
// Initialize a new Item Form Group
const itemForm = this.fb.group({
Title: [item.Title, Validators.compose([
Validators.minLength(3),
Validators.maxLength(10),
Validators.pattern(/^[A-Za-z0-9]*$/)
])],
Description: [item.Description, Validators.compose([
Validators.minLength(3),
Validators.maxLength(100),
Validators.pattern(/^[A-Za-z0-9]*$/)
])]
});
// Update current value of Item Form Subject to share with subscribers
this.itemFormSubject.next(itemForm);
}
UPDATE 1:
As was suggested by a helpful person, I attempted to clone the item parameter before passing it into the dialog. Unfortunately, this did not work.
Attempt clone 1:
public onEditItem(item): void {
const selectedItem = this.uiService.deepCopy(item);
const dialogData = {
editMode: true,
selectedItem: selectedItem
};
this.openItemDetailDialog(dialogData);
}
Attempt clone 2:
public onEditItem(item): void {
const selectedItem = this.uiService.deepCopy(item);
const dialogData = {
editMode: true,
selectedItem: {
Title: item.Title,
Description: item.Description
}
};
this.openItemDetailDialog(dialogData);
}
UPDATE 2:
I think this might be an issue caused by when one of the Item properties is undefined. I tested it out by hard-coding the values and replacing them one by one with the Item properties. It works up until I got to a property that is undefined. For example if Description is undefined it throws the error. To resolve this issue, I made sure that all of the item properties are set before passing it into the dialog and populating the form controls.
selectedItem: {
Title: item.Title,
Description: item.Description // Might be caused by this property being undefined
}
UPDATE 3:
The issue is definitely caused by form controls that are required but are not being populated with an initial value. In update 2 above, Description is required but item.Description was undefined. So it works if I set a value for Description manually. However, it does not work if any required properties are initialized in the form with null values. I thought I was able to solve this by setting values but it turns out this is still a problem because in some cases the item does not have properties that are required when the form is loaded.
Also, cloning the item property does not seem to solve the issue.
UPDATE 4:
Another possible cause of the issue is in the template I set the required attribute in the "template-driven" way required="true"
on the form control but forgot to also set it to required in the "reactive" way Validators.required
. I set the required attribute in the template because I was running into this issue regarding the missing asterisk. I added the required validator in the TS component and the error doesn't seem to be appearing anymore. However, I am not completely convinced it is solved because I wasn't able to reproduce it again after setting it back to the way it was before. I suspect that this is some sort of intermittent issue that might appear at a later time.