我的反应形式有三个组件级别深。父组件创建一个没有任何字段的新表单并将其传递给子组件。
首先外部形式是有效的。随后,子组件添加带有验证器(失败)的新表单元素,从而使外部表单无效。
我在控制台中收到 ExpressionChangedAfterItHasBeenCheckedError 错误。我想修复这个错误。
不知何故,只有当我添加第三层嵌套时才会发生这种情况。同样的方法似乎适用于两层嵌套。
笨蛋:https://plnkr.co/edit/GymI5CqSACFEvhhz55l1?p=preview
父组件
@Component({
selector: 'app-root',
template: `
myForm.valid: <b>{{myForm.valid}}</b>
<form>
<app-subform [myForm]="myForm"></app-subform>
</form>
`
})
export class AppComponent implements OnInit {
...
ngOnInit() {
this.myForm = this.formBuilder.group({});
}
}
子组件
@Component({
selector: 'app-subform',
template: `
<app-address-form *ngFor="let addressData of addressesData;"
[addressesForm]="addressesForm">
</app-address-form>
`
})
export class SubformComponent implements OnInit {
...
addressesData = [...];
constructor() { }
ngOnInit() {
this.addressesForm = new FormArray([]);
this.myForm.addControl('addresses', this.addressesForm);
}
子组件
@Component({
selector: 'app-address-form',
template: `
<input [formControl]="addressForm.controls.addressLine1">
<input [formControl]="addressForm.controls.city">
`
})
export class AddressFormComponent implements OnInit {
...
ngOnInit() {
this.addressForm = this.formBuilder.group({
addressLine1: [
this.addressData.addressLine1,
[ Validators.required ]
],
city: [
this.addressData.city
]
});
this.addressesForm.push(this.addressForm);
}
}
要了解问题,您需要阅读 您需要了解的有关
ExpressionChangedAfterItHasBeenCheckedError
错误 的所有内容。
对于您的特定情况,问题是您在
AppComponent
中创建表单并在 DOM 中使用 {{myForm.valid}}
插值。这意味着 Angular 将为更新 DOM 的 AppComponent
运行 create 并运行 updateRenderer 函数。然后使用子组件的
ngOnInit
生命周期钩子将带有控件的子组添加到此表单中:
export class AddressFormComponent implements OnInit {
@Input() addressesForm;
@Input() addressData;
ngOnInit() {
this.addressForm = this.formBuilder.group({
addressLine1: [
this.addressData.addressLine1,
[ Validators.required ] <-----------
]
this.addressesForm.push(this.addressForm); <--------
控件变得无效,因为您不提供初始值并且指定了所需的验证器。因此,整个形式变得无效,并且表达式
{{myForm.valid}}
的计算结果为 false
。但是当 Angular 对 AppComponent
运行变化检测时,它的评估结果为 true
。这就是错误所说的。
一个可能的修复方法是从一开始就将表单标记为无效,因为您计划添加所需的验证器,但 Angular 似乎没有提供这样的方法。您最好的选择可能是异步添加控件。事实上,这就是 Angular 在源代码中所做的事情:
const resolvedPromise = Promise.resolve(null);
export class NgForm extends ControlContainer implements Form {
...
addControl(dir: NgModel): void {
// adds controls asynchronously using Promise
resolvedPromise.then(() => {
const container = this._findContainer(dir.path);
dir._control = <FormControl>container.registerControl(dir.name, dir.control);
setUpControl(dir.control, dir);
dir.control.updateValueAndValidity({emitEvent: false});
});
}
所以对于你来说它将是:
const resolvedPromise = Promise.resolve(null);
@Component({
...
export class AddressFormComponent implements OnInit {
@Input() addressesForm;
@Input() addressData;
addressForm;
ngOnInit() {
this.addressForm = this.formBuilder.group({
addressLine1: [
this.addressData.addressLine1,
[ Validators.required ]
],
city: [
this.addressData.city
]
});
resolvedPromise.then(() => {
this.addressesForm.push(this.addressForm); <-------
})
}
}
或者在
AppComponent
中使用一些变量来保存表单状态并在模板中使用它:
{{formIsValid}}
export class AppComponent implements OnInit {
myForm: FormGroup;
formIsValid = false;
constructor(private formBuilder: FormBuilder) {}
ngOnInit() {
this.myForm = this.formBuilder.group({});
this.myForm.statusChanges((status)=>{
formIsValid = status;
})
}
}
import {ChangeDetectorRef} from '@angular/core';
....
export class SomeComponent {
form: FormGroup;
constructor(private fb: FormBuilder,
private ref: ChangeDetectorRef) {
this.form = this.fb.group({
myArray: this.fb.array([])
});
}
get myArray(): FormArray {
return this.form.controls.myArray as FormArray;
}
addGroup(): void {
const newGroup = this.fb.group({
prop1: [''],
prop2: ['']
});
this.myArray.push(newGroup);
this.ref.detectChanges();
}
}
我在 Angular 9 和以上解决方案中遇到了相同的场景和相同的问题,效果很好。我稍微调整了一下:同步添加不带验证器的控件并异步添加所需的验证器...因为我立即需要控件,否则我会收到错误
cannot find formControl ...
。
这是我基于上面接受的答案的解决方案:
const resolvedPromise = Promise.resolve(null);
@Component({
selector: 'password-input',
templateUrl: './password-input.component.html',
styleUrls: ['./password-input.component.css']
})
export class PasswordInputComponent implements OnInit {
@Input() parentFormGroup : FormGroup;
@Input() controlName : string = 'password';
@Input() placeholder : string = 'Password';
@Input() label : string = null;
ngOnInit(): void {
this.parentFormGroup.addControl(this.controlName, new FormControl(null));
resolvedPromise.then( () => {
this.parentFormGroup.get(this.controlName).setValidators(Validators.required)
this.parentFormGroup.get(this.controlName).updateValueAndValidity();
});
}
按照Max Koretskyi的解决方案,我们可以在
ngOnInit
上使用异步/等待模式。
这是一个例子:
@Component({
selector: 'my-sub-component',
templateUrl: 'my-sub-component.component.html',
})
export class MySubComponent implements OnInit {
@Input()
form: FormGroup;
async ngOnInit() {
// Avoid ExpressionChangedAfterItHasBeenCheckedError
await Promise.resolve();
this.form.removeControl('config');
this.form.addControl('config', new FormControl("", [Validator.required]));
}
}
Matdialog 组件内具有 min 属性的 Angular Material Datepicker 可能会发生这种情况。
我的解决方案是实现自定义 minDateValidator:
public minDateValidator(minDate: Date): ValidatorFn {
return (formGroup: FormGroup) => {
const startDateControl = formGroup.get('start');
const startMoment = this.toMoment(startDateControl.value);
const minMoment = this.toMoment(minDate);
if (minMoment.isAfter(startMoment)) {
return { minDate: true };
}
return null;
};
}
用途:
return this.formBuilder.group(
{
start: [''],
b: ['', [Validators.required]],
c: ['', [Validators.required]],
d: ['', [Validators.required]],
e: [''],
},
{
validators: [
minDateValidator(this.data.earliestSelectableDate),
],
updateOn: 'blur',
},
);
模板:
<input
class="calendar-input"
formControlName="start"
matInput
[matDatepicker]="startPicker"
(click)="startPicker.open()" />
<mat-error>
<span *ngIf="row.errors?.minDate">
{{ 'common.minDate' | translate }}
</span>
</mat-error>