Reputation: 1852
I have a reactive form that uses a toggle button to show different fields. If "Email" is selected then a text field is shown that requires email validation. If "Sms" is shown, a different field is shown which requires different validation (In real life the field has additional validation for a number, but I don't think it's necessary in the example).
On first page load, when you click "Sms", I get the following error:
ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'false'. Current value: 'true'.
This is happening on the submit button when checking if the form is valid. It is correct that the form is no longer valid; however, I don't think I'm handling this correctly for two reasons:
I have a stackblitz with the minimum amount of code needed to reproduce: https://angular-ivy-xgfrve.stackblitz.io/. Just click on "Sms" and you'll get the error. The "Save settings" button is appropriately disabled; however, if you click "Email" again it remains disabled, even though the form is now valid.
The code included in the Stackblitz is here:
app.component.ts
import { Component, OnInit, VERSION } from '@angular/core';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {
name = 'Angular ' + VERSION.major;
DELIVERY_MODES = {
sms: 'sms',
email: 'email'
};
deliveryForm: FormGroup;
constructor(private formBuilder: FormBuilder) {}
ngOnInit(): void {
const dummyData = new PlatformEnrollmentDelivery();
dummyData.hour = 10;
dummyData.minute = 30;
dummyData.modeTarget = "[email protected]"
this.deliveryForm = this.formBuilder.group({
deliveryConfiguration: this.initDeliveryModeFormGroup(dummyData),
setDefaults: this.formBuilder.group({
setDefault: false
})
});
}
initDeliveryModeFormGroup(delivery: PlatformEnrollmentDelivery): any {
const defaultMode = delivery.mode ? delivery.mode.toLowerCase() : this.DELIVERY_MODES.email;
return this.formBuilder.group({
mode: [defaultMode],
email: this.formBuilder.group(this.initEmailDeliveryMode(delivery)),
sms: this.formBuilder.group(this.initSmsDeliveryMode(delivery)),
deliveryTime: this.formBuilder.group(this.initDeliveryTime(delivery))
});
}
initEmailDeliveryMode(delivery: PlatformEnrollmentDelivery): object {
const emailControlValue = delivery.mode.toLowerCase() === this.DELIVERY_MODES.email ? delivery.modeTarget : '';
return {
emailControl: [emailControlValue]
};
}
initSmsDeliveryMode(delivery: PlatformEnrollmentDelivery): object {
const smsControlValue = delivery.mode.toLowerCase() === this.DELIVERY_MODES.sms ? delivery.modeTarget : '';
return {
smsControl: [smsControlValue]
};
}
initDeliveryTime(delivery: PlatformEnrollmentDelivery): object {
return {
timeControl: ["10:00"]
};
}
setDeliveryMethodType(type: string): void {
this.deliveryForm.controls.deliveryConfiguration.get('mode').patchValue(type);
switch (type) {
case this.DELIVERY_MODES.email:
this.deliveryForm.controls.deliveryConfiguration.get('email').get('emailControl')
.setValidators([Validators.required, Validators.email]);
this.deliveryForm.controls.deliveryConfiguration.get('sms').get('smsControl').clearValidators();
break;
case this.DELIVERY_MODES.sms:
this.deliveryForm.controls.deliveryConfiguration.get('sms').get('smsControl')
.setValidators([Validators.required]);
this.deliveryForm.controls.deliveryConfiguration.get('email').get('emailControl').clearValidators();
break;
}
}
}
export class PlatformEnrollmentDelivery {
id: number;
mode: string;
hour: number;
updated: Date;
minute: number
modeTarget: string;
constructor(
) {
this.mode = "email";
}
}
app.component.html
<form [formGroup]="deliveryForm">
<h4>Receive questions via:</h4>
<mat-button-toggle-group
[value]="deliveryForm.controls.deliveryConfiguration.get('mode').value"
formGroupName="deliveryConfiguration"
#group="matButtonToggleGroup"
id="modeReactive"
name="modeReactive"
(change)="setDeliveryMethodType(group.value)"
aria-label="Mode"
>
<mat-button-toggle [value]="DELIVERY_MODES.email">
{{ DELIVERY_MODES.email | titlecase }}
</mat-button-toggle>
<mat-button-toggle [value]="DELIVERY_MODES.sms">
{{ DELIVERY_MODES.sms | titlecase }}
</mat-button-toggle>
</mat-button-toggle-group>
<div formGroupName="deliveryConfiguration">
<div
*ngIf="
deliveryForm.controls.deliveryConfiguration.get('mode').value ===
DELIVERY_MODES.email
"
>
<mat-form-field formGroupName="email" appearance="fill">
<mat-label>Email address</mat-label>
<input matInput type="email" formControlName="emailControl" />
</mat-form-field>
</div>
<div
*ngIf="
deliveryForm.controls.deliveryConfiguration.get('mode').value ===
DELIVERY_MODES.sms
"
>
<div formGroupName="sms" class="mat-form-field-wrapper">
<div class="tel-input-wrapper">
<mat-label>Email address</mat-label>
<input
matInput
type="textfield"
name="phone"
formControlName="smsControl"
/>
<div class="mat-form-field-underline ng-tns-c182-0 ng-star-inserted">
<span class="mat-form-field-ripple ng-tns-c182-0"></span>
</div>
</div>
</div>
</div>
<div formGroupName="deliveryTime">
<mat-form-field appearance="fill">
<mat-label>Select a Delivery Time</mat-label>
<input
matInput
type="textfield"
name="phone"
formControlName="timeControl"
/>
</mat-form-field>
</div>
</div>
<button
[disabled]="!deliveryForm.valid"
mat-raised-button
button
color="primary"
type="submit"
>
Save Settings
</button>
</form>
Upvotes: 1
Views: 2111
Reputation: 15083
This error occurs if you update your UI after change detection has occurred.
Lets try to follow what happens in your code
formGroupName="deliveryConfiguration"
so this triggers change detection, we are still at valid form(change)="setDeliveryMethodType(group.value)"
which changes the form to invalid and the UI is to be updated again...A simple solution is to simply use the ChangeDetectorRef
. We inject it can call the detectChanges()
function
If we log the deliveryForm
object we get
{
"setDefaults": {
"setDefault": ""
},
"deliveryConfiguration": {
"mode": "email",
"email": {
"emailControl": "[email protected]"
},
"sms": {
"smsControl": ""
},
"deliveryTime": {
"timeControl": "10:00"
}
}
}
We note that from the above object, we can simply set the button control like below
<ng-container formGroupName="deliveryConfiguration">
<mat-button-toggle-group
[value]="deliveryForm.get('deliveryConfiguration.mode').value"
formControlName="mode"
#group="matButtonToggleGroup"
id="modeReactive"
name="modeReactive"
aria-label="Mode"
>
<mat-button-toggle [value]="DELIVERY_MODES.email">
{{ DELIVERY_MODES.email | titlecase }}
</mat-button-toggle>
<mat-button-toggle [value]="DELIVERY_MODES.sms">
{{ DELIVERY_MODES.sms | titlecase }}
</mat-button-toggle>
</mat-button-toggle-group>
</ng-container>
Note a few changes
formControlName="mode"
<ng-container formGroupName="deliveryConfiguration"> ... </ng-container>
Lets now use the .valueChanges
property to track the mode change
this.deliveryForm.get('deliveryConfiguration.mode').valueChanges.subscribe({
next: (res) => this.setDeliveryMethodType(res),
});
The final step we can clean up the setDeliveryMethodType
method
setDeliveryMethodType(type: string): void {
const emailControl = this.deliveryForm.get(
'deliveryConfiguration.email.emailControl'
);
const smsControl = this.deliveryForm.get(
'deliveryConfiguration.sms.smsControl'
);
switch (type) {
case this.DELIVERY_MODES.email:
emailControl.setValidators([Validators.required, Validators.email]);
smsControl.clearValidators();
break;
case this.DELIVERY_MODES.sms:
smsControl.setValidators([Validators.required]);
emailControl.clearValidators();
break;
}
emailControl.updateValueAndValidity();
smsControl.updateValueAndValidity();
this.cdr.detectChanges();
}
this.cdr.detectChanges();
does the trick however we can actually remove it altogether... The reason that removing this line works is that when we call updateValueAndValidity();
Angular triggers another change detection life cycle hence the error will not be thrown and everything should still work as expected. In the below demo I have removed this.cdr.detectChanges();
Demo working without ChangeDetectorRef
Upvotes: 1