【问题标题】:Angular: How to customize FormArray validation to check for duplicatesAngular:如何自定义 FormArray 验证以检查重复项
【发布时间】:2021-06-13 01:26:53
【问题描述】:

我正在实现一个验证器来检查哪些行具有相同的名称。

我想在输入框下方显示一条错误消息:“此行重复”。

现在我正在自定义 FormArray 验证,以便它逐行显示错误消息,但不知道下一步该做什么。

在 app.component.html 文件中:

<h4 class="bg-primary text-white p-2">
  Check for duplicate names
</h4>

<div [formGroup]="formArray">
  <div class="px-3 py-1" 
    *ngFor="let group of formArray.controls" [formGroup]="group">
    Name <input formControlName="name"/> 
    Content <input formControlName="content" class="w-20" disabled/>
  </div>
</div>

<hr>
<pre>
<b>Value :</b>
{{formArray.value | json}}
<b>Valid :</b> {{formArray.valid}}
<b>Errors :</b>
{{formArray.errors | json}}
</pre>

在 app.component.ts 文件中:

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

export function hasDuplicate(): ValidatorFn {
  return (formArray: FormArray): { [key: string]: any } | null => {
    const names = formArray.controls.map(x => x.get('name').value);
    const check_hasDuplicate = names.some(
      (name, index) => names.indexOf(name, index + 1) != -1
    );

    return check_hasDuplicate ? { error: 'Has Duplicate !!!' } : null;
  };
}

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  formArray = new FormArray(
    [
      this.createGroup({ name: 'abc', content: '' }),
      this.createGroup({ name: 'abc', content: '' }),
      this.createGroup({ name: 'bcd', content: '' })
    ],
    hasDuplicate()
  );

  createGroup(data: any) {
    data = data || { name: null, content: null };
    return new FormGroup({
      name: new FormControl(data.name),
      content: new FormControl(data.content)
    });
  }
}

Link to Stackblitz

我为 FormArray 中的每个 FormGroup 尝试了自定义验证器,但它必须通过 keyup 事件扫描并重置整个 FormGroups 验证器。我感觉不太好。

【问题讨论】:

    标签: angular validation


    【解决方案1】:

    听起来你想要实现的是:

    • 观察每个FormControl(嵌套在FormGroup中),看看它的值是否与给定FormArray中任何其他FormControl(嵌套在FormGroup中)的值相同

    在高层次上,您需要实现两件事:

    • 获取单个 FormControl 的值并将其与所有其他值的列表进行比较(即,正如您在 hasDuplicate 验证器函数中所做的那样)
    • 将错误分配给包含重复的FormControls 的个人FormGroups

    创建将位于FormArray 上的验证器的问题在于,您返回的错误将分配给FormArray 本身,而不是单个FormGroups。例如,如果hasDuplicate() 返回错误,您将得到这样的表单结构:

    formArray: {
        error: 'Has Duplicate !!!',
        controls: [
            formGroup1: { error: null },
            formGroup2: { error: null },
            formGroup3: { error: null }
            ...
        ]
    }
    

    您希望能够逐个附加错误的是:

    formArray: {
        error: null,
        controls: [
            formGroup1: { error: 'Has Duplicate !!!' },
            formGroup2: { error: 'Has Duplicate !!!' },
            formGroup3: { error: null }
            ...
        ]
    }
    

    为此,您必须创建影响 FormGroup 的验证器函数,以便当它们的验证条件返回 true 时,它​​将正确更新错误的 FormGroup 的错误属性,并且 不是FormArray

    但是FormGroup 怎么会知道另一个FormGroups 的值来知道它是否是重复的,因为它没有这些信息?原来验证器只是函数,所以你可以给它们传递参数,例如

    export function groupIsDuplicate( formArray: FormArray ): ValidatorFn {
      return ( formGroup: FormGroup ): Record<string, any> | null => {
        const names: string[] =
          formArray
            ?.controls
            ?.map(
              ( formGroup: FormGroup ) => formGroup?.get( 'name' )?.value
            );
    
        const isDuplicate: boolean =
          names
            ?.filter(
              ( name: string ) => name === formGroup?.get( 'name' )?.value
            )
            ?.length > 1;
    
        return isDuplicate ? { error: 'Has Duplicate !!!' } : null
      }
    }
    

    如您所见,我们可以通过将 FormArray 作为参数传递给验证器来访问它。您甚至可以传入formArray.value,因为在这个特定示例中,我们只对FormArray 值感兴趣(但在其他用例中,您可能还对FormArray 的其他属性感兴趣,例如它的错误)。

    现在,如果您在创建 FormArray 时将其分配给 createGroup() 函数中的每个 FormGroup,则用于获取值列表的 formArray 的值不会更新,如果FormArray 更改。您的验证器会卡在查看原始数组,如果您以后想添加或删除 FormGroups,这将是不理想的。

    您需要做的是确保每次更新 FormArray 时都重新应用此验证器,以便您可以使用最新的名称集传入更新的 formArray 参数。

    ngOnInit() {
       // ...
       this.watchForFormArrayChanges()
    }
    
    watchForFormArrayChanges() {
      this.formArray
        ?.valueChanges
        ?.pipe(
          startWith(
            this.formArray?.value
          )
        )
        ?.subscribe(
          () => this.setDuplicateValidation( this.formArray )
        )
    }
    
    setDuplicateValidation( formArray: FormArray ) {
      formArray
        ?.controls
        ?.forEach(
          ( formGroup: FormGroup ) => {
            formGroup?.clearValidators()
            formGroup?.setValidators( [ controlIsDuplicate( formArray ) ] )
          }
        )
    }
    

    这是一种非常严厉的方法,会不断重置所有验证器,以响应FormArray 更改中的任何值。如果FormGroup 中的FormControl 的值发生变化等,您可以通过仅更新验证器来提高上述性能。但就问题而言,这应该让您更接近您最初需要的位置

    【讨论】:

    • 我在下面写道:“我为 FormArray 中的每个 FormGroup 尝试了自定义验证器,但它必须通过 keyup 事件扫描和重置整个 FormGroups 验证器。我感觉不太好。”。我明白你说的话。但我需要一个更具开创性的想法。
    • 我建议的解决方案不使用 keyup 事件来触发更改。它使用 formArray.valueChanges,这是一个可观察的(一系列事件),只要嵌套在其中的任何内容发生变化,就会发出。 tektutorialshub.com/angular/valuechanges-in-angular-forms
    • 如果你想保留你的数组级验证器,还有另一种方法,我不推荐,但仍然是手动设置和清除每个 FormGroups 上的错误使用setErrors(),例如formGroup.setErrors( { error: 'Has Duplicate !!!' } ) 设置和formGroup.setErrors() 清除
    • 感谢您的建议。我正在尝试编写验证器而不是使用 valueChanges。我注意到当 FormArray 中的内容发生变化时,它也会调用类似于 valueChanges 的 Validator。使用 setErrors() 是个好主意。
    【解决方案2】:

    这就是我的做法。但是它仍然使用相当多的循环来检查和设置formarray中的formcontrols。

    处理大数据时这仍然很糟糕。它只比为每个表单控件设置验证器快一点。

    在我看来,以下两种方式是相似的。

    方式一:

    在 app.component.html 文件中:

    <div [formGroup]="formArray">
      <div class="px-3 py-1" *ngFor="let group of formArray.controls" [formGroup]="group">
        Name <input formControlName="name"/>
        Content <input formControlName="content" class="w-20" disabled/>
        <small class="d-block text-danger" 
          *ngIf="group.get('name').errors?.duplicated">
          This line is duplicated
        </small>
      </div>
    </div>
    

    在 app.component.ts 文件中:

    import { Component } from '@angular/core';
    import { FormGroup, FormControl, FormArray, ValidatorFn, ValidationErrors } from '@angular/forms';
    
    @Component({
      selector: 'my-app',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      formArray = new FormArray(
        [
          this.createGroup({ name: 'abc', content: '' }),
          this.createGroup({ name: 'abc', content: '' }),
          this.createGroup({ name: 'bcd', content: '' }),
          this.createGroup({ name: 'bcd', content: '' }),
          this.createGroup({ name: '', content: '' })
        ],
        this.hasDuplicate('name')
      );
    
      createGroup(data: any) {
        data = data || { name: null, content: null };
        return new FormGroup({
          name: new FormControl(data.name),
          content: new FormControl(data.content)
        });
      }
    
      duplicates = [];
    
      hasDuplicate(key_form): ValidatorFn {
        return (formArray: FormArray): { [key: string]: any } | null => {
          if (this.duplicates) {
            for (var i = 0; i < this.duplicates.length; i++) {
              let errors = this.formArray.at(this.duplicates[i]).get(key_form).errors as Object || {};
              delete errors['duplicated'];
              this.formArray.at(this.duplicates[i]).get(key_form).setErrors(errors as ValidationErrors);
            }
          }
    
          let dict = {};
          formArray.value.forEach((item, index) => {
            dict[item.name] = dict[item.name] || [];
            dict[item.name].push(index);
          });
          let duplicates = [];
          for (var key in dict) {
            if (dict[key].length > 1) duplicates = duplicates.concat(dict[key]);
          }
          this.duplicates = duplicates;
    
          for (const index of duplicates) {
            formArray.at(+index).get(key_form).setErrors({ duplicated: true });
          }
    
          return duplicates.length > 0 ? { error: 'Has Duplicate !!!' } : null;
        };
      }
    }
    

    Link to Stackblitz

    方式 2:

    在 app.component.ts 文件中:

    import { Component, OnInit } from '@angular/core';
    import { FormGroup, FormControl, FormArray, ValidationErrors } from '@angular/forms';
    
    @Component({
      selector: 'my-app',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent implements OnInit {
      formArray = new FormArray([
        this.createGroup({ name: 'abc', content: '' }),
        this.createGroup({ name: 'abc', content: '' }),
        this.createGroup({ name: 'bcd', content: '' }),
        this.createGroup({ name: 'bcd', content: '' }),
        this.createGroup({ name: '', content: '' })
      ]);
    
      createGroup(data: any) {
        data = data || { name: null, content: null };
        return new FormGroup({
          name: new FormControl(data.name),
          content: new FormControl(data.content)
        });
      }
    
      duplicates = [];
    
      ngOnInit() {
        setTimeout(() => {
          this.checkDuplicates('name');
        });
        this.formArray.valueChanges.subscribe(x => {
          this.checkDuplicates('name');
        });
      }
    
      checkDuplicates(key_form) {
        for (const index of this.duplicates) {
          let errors = this.formArray.at(index).get(key_form).errors as Object || {};
          delete errors['duplicated'];
          this.formArray.at(index).get(key_form).setErrors(errors as ValidationErrors);
        }
        this.duplicates = [];
    
        let dict = {};
        this.formArray.value.forEach((item, index) => {
          dict[item.name] = dict[item.name] || [];
          dict[item.name].push(index);
        });
        for (var key in dict) {
          if (dict[key].length > 1)
            this.duplicates = this.duplicates.concat(dict[key]);
        }
        for (const index of this.duplicates) {
          this.formArray.at(index).get(key_form).setErrors({ duplicated: true });
        }
      }
    }
    

    Link to Stackblitz

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-03-06
      • 1970-01-01
      • 2018-11-21
      • 2018-07-28
      • 1970-01-01
      • 2020-01-07
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多