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 的解决方案经过两项调整后效果很好(另请参阅他的解决方案下的评论):

  1. if (c.value.length >= min) 需要 if (c.value.filter(item => item).length >= min) 仅过滤选中的复选框。

  2. 'choicesSecond' 的 setControl 需要在 onSubmit 方法中第一次提交后完成,而不是 ngOnInit 挂钩。在这里你还需要做 setValidatorsupdateValueAndValidity.

正如@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);
  }

}