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:
- The error is shown (I understand this only happens in dev environments)
- If you click "Sms" and back to "Email", the form is technically valid again and the "Save Settings" should no longer be disabled.
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 = "example@domain.com"
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>