Angular 对象属性之间的 FormArray 交叉验证

Angular FormArray cross validation between object properties

我有一个 Formgroup,里面有一个 Formarray。

这是结构:

myForm = this.fb.group(
        {
            title: ["", [Validators.required, Validators.minLength(4)]],
            pairs: this.fb.array(
                this.fPairs.map(f =>
                    this.fb.group({
                        grade: [],
                        value: []
                    })
                )
            )
        }
    );

我的 FormArray 被映射,看起来像这样 onInit:

fPairs: Array<pairs> = [
        {grade: 0, value: 0},
        {grade: 0, value: 0},
        {grade: 0, value: 0},
        {grade: 0, value: 0}
    ];

我想要实现的,因为这个 FormArray 的每个对象的每个 属性 都是我表单中的一个输入字段,我需要一个验证来做到这一点:

索引 0 处的对象属性必须具有比下一个索引更大的值。

所以在 myForm

pairs[0].score > pairs[1].score
pairs[1].score > pairs[2].score
pairs[2].score > pairs[3].score

同样适用于 属性 "value"。

我怎样才能正确地为这个 formArray 实现一个真正的验证器(类型 ValidatorFn)?

到目前为止,我只设法创建了一个函数,它检查每个字段,将其与前一个和下一个进行比较,如果值不符合规则,我手动设置错误 setErrors()

此函数在 ValueChanges() 订阅中,因此当 formArray 中的值发生变化时,它会使用我的函数

进行检查

有没有更好的方法?

这里是一个stackblizt(valueChanges订阅不能正常工作,只有在下一个字段写入时才会刷新,你会在stackblitz中明白我的意思)

https://stackblitz.com/edit/angular-mat-formfield-flex-layout-x9nksb

谢谢

所以,过了一会儿(对延迟表示抱歉),我制作了一个 stackblitz 重现您的最小示例,并为您制作了一个验证器。代码在我回答的最后。

给大家简单解释一下:交叉控件验证必须在parent中进行。父级可以是表单组或表单数组。在您的情况下,这将是一个表单数组(包含所有分数的数组)。

然后将错误直接附加到您的表单数组中,在满足条件时使其无效。

如您所见,控件对它们的错误一无所知:我自愿这样做,所以我不为您工作。我的最终目标是向您展示如何比较两个字段并相应地设置表单错误。

现在,在验证器中,您可以根据需要从控件中 add/remove 错误,但我想我已经回答了您关于在两个单独字段上进行表单验证的第一个问题。

代码已自我解释,但如果您有任何疑问,请随时提问!

(PS :如果您想查看错误,它们将显示在 stackblitz 页面的页脚中)

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormArray, ValidatorFn } from '@angular/forms';

@Component({
  selector: 'my-app',
  template: `
<form novalidate [formGroup]="form" fxLayout="column" fxLayoutGap="24px">
    <div formArrayName="pairs" fxLayout="row" fxLayoutGap="12px" *ngFor="let pair of form.get('pairs').controls; let i = index">
        <ng-container [formGroupName]="i">
            <mat-form-field fxFlex="50%">
                <input matInput type="text" formControlName="grade" placeholder="Grade for {{ i }}">
      </mat-form-field>
      <mat-form-field fxFlex="50%">
        <input matInput type="text" formControlName="value"  placeholder="Score for {{ i }}">
      </mat-form-field>
    </ng-container>
  </div>
</form>

<p>The form is {{ form.invalid ? 'invalid' : 'valid' }}</p>
<p>The pairs group is {{ form.get('pairs').invalid ? 'invalid' : 'valid' }}</p>

<p>Errors on the form : {{ form.errors | json }}</p>
<p>Errors on the group : {{ form.get('pairs').errors | json }}</p>
`,
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  form: FormGroup;

  // Create a dataset
  data = [
    { grade: 6, value: 0 },
    { grade: 5, value: 0 },
    { grade: 4, value: 0 },
    { grade: 3, value: 0 },
  ];

  constructor(private fb: FormBuilder) { }

  ngOnInit(): void {
    // Create a form group
    this.form = this.fb.group({
      // Create a form array made of form groups
      pairs: this.fb.array(this.data.map(item => this.fb.group(item)))
    });

    // Add validators (optional, used to split the code logic)
    this.addValidators();
  }

  addValidators() {
    // Get the form array and append a validator (again code split)
    (this.form.get('pairs') as FormArray).setValidators(this.formArrayValidator());
  }

  // Form validator
  formArrayValidator(): ValidatorFn {
    // The validator is on the array, so the AbstractControl is of type FormArray
    return (group: FormArray) => {
      // Create an object of errors to return
      const errors = {};
      // Get the list of controls in the array (which are FormGroups)
      const controls = group.controls;
      // Iterate over them
      for (let i = 1; i < controls.length; i++) {
        // Get references to controls to compare them (split code again)
        const valueControl = controls[i].get('value');
        const previousValueControl = controls[i - 1].get('value');

        // if error, set array error
        if (valueControl.value > previousValueControl.value) {
          // array error (sum up of all errors)
          errors[i + 'greaterThan' + (i - 1)] = true;
        }
      }

      // return array errors ({} is considered an error so return null if it is the case)
      return errors === {} ? null : errors;
    }
  }
}