Angular 反应式表单手动验证给出 ExpressionChangedAfterItHasBeenCheckedError
Angular Reactive Form manual validate gives ExpressionChangedAfterItHasBeenCheckedError
我正在使用 Angular 5.0.0 和 Material 5.2.2。
此表格中有两个子问题。用户以一种形式提交两次。这必须保持这种方式,因为我在这里展示了我原始形式的一个非常精简的版本。
第一次提交后,在第二个子问题中,我验证是否至少有一个选中的复选框。如果不是我做一个 this.choicesSecond.setErrors({'incorrect': true});
。这个
以正确的方式禁用 submit
按钮。但这给出了一个错误:
`ExpressionChangedAfterItHasBeenCheckedError: Expression has changed
after it was checked. Previous value: 'true'. Current value: 'false'.
我认为这与 change detection
有关。如果我使用 this.changeDetectorRef.detectChanges()
进行额外的更改检测,则错误消失但提交按钮不再被禁用。
我做错了什么?
模板:
<mat-card>
<form *ngIf="myForm" [formGroup]="myForm" (ngSubmit)="onSubmit(myForm.value)" novalidate>
<div *ngIf="!subQuestion">
<mat-card-header>
<mat-card-title>
<h3>Which fruit do you like most?</h3>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<mat-radio-group formControlName="choiceFirst">
<div *ngFor="let fruit of fruits; let i=index" class="space">
<mat-radio-button [value]="fruit">{{fruit}}</mat-radio-button>
</div>
</mat-radio-group>
</mat-card-content>
</div>
<div *ngIf="subQuestion">
<mat-card-header>
<mat-card-title>
<h3>Whichs fruits do you like?</h3>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div *ngFor="let choiceSecond of choicesSecond.controls; let i=index">
<mat-checkbox [formControl]="choiceSecond">{{fruits[i]}}</mat-checkbox>
</div>
</mat-card-content>
</div>
<mat-card-actions>
<button mat-raised-button type="submit" [disabled]="!myForm.valid">Submit</button>
</mat-card-actions>
</form>
</mat-card>
组件:
export class AppComponent {
myForm: FormGroup;
fruits: Array<string> = ["apple", "pear", "kiwi", "banana", "grape", "strawberry", "grapefruit", "melon", "mango", "plum"];
numChecked: number = 0;
subQuestion: boolean = false;
constructor(private formBuilder: FormBuilder, private changeDetectorRef: ChangeDetectorRef) { }
ngOnInit() {
this.myForm = this.formBuilder.group({
'choiceFirst': [null, [Validators.required]],
});
let choicesFormArray = this.fruits.map(fruit => { return this.formBuilder.control(false) });
this.myForm.setControl('choicesSecond', this.formBuilder.array(choicesFormArray));
this.onChangeAnswers();
}
onChangeAnswers() {
this.choicesSecond.valueChanges.subscribe(value => {
let numChecked = value.filter(item => item).length;
if (numChecked === 0 ) this.choicesSecond.setErrors({'incorrect': true});
});
}
get choicesSecond(): FormArray {
return this.myForm.get('choicesSecond') as FormArray;
};
onSubmit(submit) {
if (!this.subQuestion) {
this.subQuestion = true;
let numChecked = this.choicesSecond.controls.filter(item => item.value).length;
if (numChecked === 0 ) this.choicesSecond.setErrors({'incorrect': true});
// this.changeDetectorRef.detectChanges()
}
console.log(submit);
}
}
问题来自 this.choicesSecond.setErrors({'incorrect': true});
,当您单击提交时,您创建了组件并同时更改了它的值。由于 angular 完成的附加检查,这在开发模式下失败。 Here is a good article about this error.
对于表单验证,您可以使用自定义验证器,an example in this post :
minLengthArray(min: number) {
return (c: AbstractControl): {[key: string]: any} => {
if (c.value.length >= min)
return null;
return { 'minLengthArray': {valid: false }};
}
}
对于步骤,当您使用 angular material 时,您可以使用 mat-stepper。
@ibenjelloun 的解决方案经过两项调整后效果很好(另请参阅他的解决方案下的评论):
if (c.value.length >= min)
需要 if (c.value.filter(item => item).length >= min)
仅过滤选中的复选框。
'choicesSecond' 的 setControl 需要在 onSubmit
方法中第一次提交后完成,而不是 ngOnInit
挂钩。在这里你还需要做 setValidators
和 updateValueAndValidity
.
正如@ibenjelloun 所建议的那样,从第一个子问题到第二个子问题的额外按钮的解决方案更好,因为只有通过这种方式才能实现 back
按钮。
这是最后一个有效的组件:
export class AppComponent {
myForm: FormGroup;
fruits: Array<string> = ["apple", "pear", "kiwi", "banana", "grape", "strawberry", "grapefruit", "melon", "mango", "plum"];
subQuestion: boolean = false;
constructor(private formBuilder: FormBuilder) { }
ngOnInit() {
this.myForm = this.formBuilder.group({
'choiceFirst': [null, [Validators.required]],
});
}
minLengthArray(min: number) {
return (c: AbstractControl): { [key: string]: any } => {
if (c.value.filter(item => item).length >= min)
return null;
return { 'minLengthArray': { valid: false } };
}
}
get choicesSecond(): FormArray {
return this.myForm.get('choicesSecond') as FormArray;
};
onSubmit(submit) {
if (!this.subQuestion) {
let choicesSecondFormArray = this.fruits.map(fruit => { return this.formBuilder.control(false) });
this.myForm.setControl('choicesSecond', this.formBuilder.array(choicesSecondFormArray));
this.myForm.get('choicesSecond').setValidators(this.minLengthArray(1));
this.myForm.get('choicesSecond').updateValueAndValidity();
this.subQuestion = true;
}
console.log(submit);
}
}
我正在使用 Angular 5.0.0 和 Material 5.2.2。
此表格中有两个子问题。用户以一种形式提交两次。这必须保持这种方式,因为我在这里展示了我原始形式的一个非常精简的版本。
第一次提交后,在第二个子问题中,我验证是否至少有一个选中的复选框。如果不是我做一个 this.choicesSecond.setErrors({'incorrect': true});
。这个
以正确的方式禁用 submit
按钮。但这给出了一个错误:
`ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'true'. Current value: 'false'.
我认为这与 change detection
有关。如果我使用 this.changeDetectorRef.detectChanges()
进行额外的更改检测,则错误消失但提交按钮不再被禁用。
我做错了什么?
模板:
<mat-card>
<form *ngIf="myForm" [formGroup]="myForm" (ngSubmit)="onSubmit(myForm.value)" novalidate>
<div *ngIf="!subQuestion">
<mat-card-header>
<mat-card-title>
<h3>Which fruit do you like most?</h3>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<mat-radio-group formControlName="choiceFirst">
<div *ngFor="let fruit of fruits; let i=index" class="space">
<mat-radio-button [value]="fruit">{{fruit}}</mat-radio-button>
</div>
</mat-radio-group>
</mat-card-content>
</div>
<div *ngIf="subQuestion">
<mat-card-header>
<mat-card-title>
<h3>Whichs fruits do you like?</h3>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div *ngFor="let choiceSecond of choicesSecond.controls; let i=index">
<mat-checkbox [formControl]="choiceSecond">{{fruits[i]}}</mat-checkbox>
</div>
</mat-card-content>
</div>
<mat-card-actions>
<button mat-raised-button type="submit" [disabled]="!myForm.valid">Submit</button>
</mat-card-actions>
</form>
</mat-card>
组件:
export class AppComponent {
myForm: FormGroup;
fruits: Array<string> = ["apple", "pear", "kiwi", "banana", "grape", "strawberry", "grapefruit", "melon", "mango", "plum"];
numChecked: number = 0;
subQuestion: boolean = false;
constructor(private formBuilder: FormBuilder, private changeDetectorRef: ChangeDetectorRef) { }
ngOnInit() {
this.myForm = this.formBuilder.group({
'choiceFirst': [null, [Validators.required]],
});
let choicesFormArray = this.fruits.map(fruit => { return this.formBuilder.control(false) });
this.myForm.setControl('choicesSecond', this.formBuilder.array(choicesFormArray));
this.onChangeAnswers();
}
onChangeAnswers() {
this.choicesSecond.valueChanges.subscribe(value => {
let numChecked = value.filter(item => item).length;
if (numChecked === 0 ) this.choicesSecond.setErrors({'incorrect': true});
});
}
get choicesSecond(): FormArray {
return this.myForm.get('choicesSecond') as FormArray;
};
onSubmit(submit) {
if (!this.subQuestion) {
this.subQuestion = true;
let numChecked = this.choicesSecond.controls.filter(item => item.value).length;
if (numChecked === 0 ) this.choicesSecond.setErrors({'incorrect': true});
// this.changeDetectorRef.detectChanges()
}
console.log(submit);
}
}
问题来自 this.choicesSecond.setErrors({'incorrect': true});
,当您单击提交时,您创建了组件并同时更改了它的值。由于 angular 完成的附加检查,这在开发模式下失败。 Here is a good article about this error.
对于表单验证,您可以使用自定义验证器,an example in this post :
minLengthArray(min: number) {
return (c: AbstractControl): {[key: string]: any} => {
if (c.value.length >= min)
return null;
return { 'minLengthArray': {valid: false }};
}
}
对于步骤,当您使用 angular material 时,您可以使用 mat-stepper。
@ibenjelloun 的解决方案经过两项调整后效果很好(另请参阅他的解决方案下的评论):
if (c.value.length >= min)
需要if (c.value.filter(item => item).length >= min)
仅过滤选中的复选框。'choicesSecond' 的 setControl 需要在
onSubmit
方法中第一次提交后完成,而不是ngOnInit
挂钩。在这里你还需要做setValidators
和updateValueAndValidity
.
正如@ibenjelloun 所建议的那样,从第一个子问题到第二个子问题的额外按钮的解决方案更好,因为只有通过这种方式才能实现 back
按钮。
这是最后一个有效的组件:
export class AppComponent {
myForm: FormGroup;
fruits: Array<string> = ["apple", "pear", "kiwi", "banana", "grape", "strawberry", "grapefruit", "melon", "mango", "plum"];
subQuestion: boolean = false;
constructor(private formBuilder: FormBuilder) { }
ngOnInit() {
this.myForm = this.formBuilder.group({
'choiceFirst': [null, [Validators.required]],
});
}
minLengthArray(min: number) {
return (c: AbstractControl): { [key: string]: any } => {
if (c.value.filter(item => item).length >= min)
return null;
return { 'minLengthArray': { valid: false } };
}
}
get choicesSecond(): FormArray {
return this.myForm.get('choicesSecond') as FormArray;
};
onSubmit(submit) {
if (!this.subQuestion) {
let choicesSecondFormArray = this.fruits.map(fruit => { return this.formBuilder.control(false) });
this.myForm.setControl('choicesSecond', this.formBuilder.array(choicesSecondFormArray));
this.myForm.get('choicesSecond').setValidators(this.minLengthArray(1));
this.myForm.get('choicesSecond').updateValueAndValidity();
this.subQuestion = true;
}
console.log(submit);
}
}