Angular Reactive Form手动验证提供ExpressionChangedAfterItHasBeenCheckedError

时间:2018-05-02 12:41:43

标签: angular angular-reactive-forms

我正在使用Angular 5.0.0和Material 5.2.2。

此表格中有两个子问题。用户以一种形式提交两次。这必须保持这种方式,因为我在这里展示了我原始形式的一个非常简化的版本。

在第一次提交后,在第二个子问题中,我验证是否有最小的一个选中复选框。如果不是,我会this.choicesSecond.setErrors({'incorrect': true});。这个 以正确的方式禁用submit按钮。但这会产生错误:

  

`ExpressionChangedAfterItHasBeenCheckedError:表达式已更改   检查后。上一个值:'true'。当前值:'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);
  }

}

2 个答案:

答案 0 :(得分:2)

问题来自this.choicesSecond.setErrors({'incorrect': true});,当您单击提交时,您创建组件并同时更改其值。由于通过角度进行的附加检查,这在开发模式中失败。 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 }};
    }
}

对于步骤,当您使用角度材质时,可以使用mat-stepper

答案 1 :(得分:0)

解决方案@ibenjelloun适用于两项调整(参见他的解决方案下的评论):

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

  2. &#39; choicesSecond&#39;的setControl需要在第一次提交onSubmit方法而不是ngOnInit挂钩后完成。您还需要执行setValidatorsupdateValueAndValidity

  3. 由于@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);
      }
    
    }