Angular: FormControl.setValidators...为什么没有getValidators?

创建于 2016-12-14  ·  72评论  ·  资料来源: angular/angular

我正在提交... (用“x”勾选)

[ ] bug report => search github for a similar issue or PR before submitting
[x] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

当前行为
我们通过 TypeScript FormBuilder 在 ReactiveForm 中有一个带有基本验证器的 AbstractControl,例如 (required, maxLength)。 对于这些基本验证器,我们添加了一个自定义验证器,它根据下拉值动态更改验证策略。 目前我们在另一个组件中使用setValidators()方法(AbstractControl 通过@Input()包含)。 主要问题是它覆盖了现有的验证器。
例子:

应用组件

this.formBuilder.group({
      zip: ['', [Validators.required, Validators.maxLength(10)]] 
    });

应用模板

<zip ...
        [control]="form.controls.zip">
</zip>

压缩组件

@Input() control: AbstractControl;

this.control.setValidators([
      // redundant to formBuilder
      Validators.required,
      Validators.maxLength(10),
      // custom validation based on a dropdown value (via. valueChange Detection)
      validateZipFn(countryCode)]
    );

预期行为
为了保持灵活性,我们不想覆盖所有验证器。 下面的代码将使用getValidators()方法说明行为。

应用组件

this.formBuilder.group({
      zip: ['', [Validators.required, Validators.maxLength(10)]]
    });

应用模板

<zip ...
        [control]="form.controls.zip">
</zip>

压缩组件

@Input() control: AbstractControl;

let listOfAllValidationRules = this.control.getValidators().push(validateZipFn(countryCode)]);
this.control.setValidators(listOfAllValidationRules);

改变行为的动机/用例是什么?
通过动态验证更加灵活。

  • 角度版本: ~2.1.2

  • 浏览器:全部

  • 语言: TypeScript / ES6

forms feature medium

最有用的评论

我同意,拥有一个可以通过当前控件上的方法获得的验证器的只读列表将非常有帮助。

所有72条评论

@Icepick没有什么可以返回的。 验证器存储为单个对象,而不是数组。

@DzmitryShylovich这是否意味着这无法实现? 就个人而言,我只需要附有元数据的验证器的只读副本。

我正在构建一个指令,其中传递了一个验证消息对象。这只是一个对象,验证规则名称是键,值是要显示的消息。 我想比较这个对象和 NgControl 的设置验证器,以便在缺少特定消息时警告用户。

出于与@Bidthedog 类似的原因,我也希望看到这一点。 例如,如果Validator.requried在控件上,那么我们希望以编程方式在表单上指示它。 我们只需要只读即可。 目前我能做的最好的事情是在组件上定义一个数组并将其传递给表单控件定义。 它会很快变得丑陋!

这在路线图上吗?如果是,此功能是否有预计到达时间?

确切地。 我现在的表单服务中基本上有三个令人讨厌的重复定义。 下面的代码片段是我整理的一个相当大的 6 页表单的一小段摘录。 这是第一页(详细信息)和第 2 页(医疗)的开头:

创建/定义:

  private createForm() {
    this.form = this.fb.group({
      details: this.fb.group({
        NHSNumber: [null, [Validators.required, Validators.pattern(/^\d{3}-\d{3}-\d{4}$/)]],
        titleId: [null, [Validators.required]],
        firstName: [null, [Validators.required, Validators.maxLength(10)]],
        middleNames: null,
        lastName: [null, [Validators.required]],
        DOB: [null, [Validators.required]],
        genderId: [null, [Validators.required]],
        regionId: [null, [Validators.required]],
        maritalStatusId: [null, [Validators.required]],
        occupationTypeId: [null],
        occupationId: null,
        telephoneNo: null,
        mobileNo: null,
        houseNumber: null,
        streetName: null,
        town: null,
        city: null,
        county: null,
        postcode: null,
        GPId: null,
        nextOfKinId: null,
        livesWith: null
        //vehicleLicenseTypeId?: number[]
      }),
      medical: this.fb.group({
        gpId: [null, [Validators.required]]
      })
    });
  }

填充:

  private populateForm() {
    let m = this.model;
    this.form.setValue({
      details: {
        NHSNumber: this.getTextValue(m.NHSNumber),
        titleId: this.getLookupValue(m.titleId),
        firstName: this.getTextValue(m.firstName),
        middleNames: this.getTextValue(m.middleNames),
        lastName: this.getTextValue(m.lastName),
        DOB: this.getDateValue(m.DOB),
        genderId: this.getLookupValue(m.genderId),
        regionId: this.getLookupValue(m.regionId),
        maritalStatusId: this.getLookupValue(m.maritalStatusId),
        occupationTypeId: this.getLookupValue(m.occupationTypeId),
        occupationId: this.getLookupValue(m.occupationId),
        telephoneNo: this.getTextValue(m.telephoneNo),
        mobileNo: this.getTextValue(m.mobileNo),
        houseNumber: this.getTextValue(m.houseNumber),
        streetName: this.getTextValue(m.streetName),
        town: this.getTextValue(m.town),
        city: this.getTextValue(m.city),
        county: this.getTextValue(m.county),
        postcode: this.getTextValue(m.postcode),
        GPId: this.getLookupValue(m.GPId),
        nextOfKinId: this.getLookupValue(m.nextOfKinId),
        livesWith: this.getTextValue(m.livesWith)
      },
      medical: {
        gpId: this.getLookupValue(m.GPId) || 1
      }
    });

验证规则。 不完整,但你可以看到这将如何变得混乱,快速

  private formValidationMessages = {
    details: {
      NHSNumber: {
        required: '\'NHS Number\' is required',
        pattern: '\'NHS Number\' must be in the format \'NNN-NNN-NNNN\' where N is a digit'
      },
      titleId: {
        required: '\'Title\' is required'
      },
      firstName: {
        required: '\'First Name\' is required'
      }
    },
    medical: {
    }
  };

我开始在一个对象集中定义所有表单规则,我正在考虑迭代/投影到上面列出的函数中(尽管我还没有做到这一点):

  private formValidationMessages = {
    details: {
      NHSNumber: {
        required: '\'NHS Number\' is required',
        pattern: '\'NHS Number\' must be in the format \'NNN-NNN-NNNN\' where N is a digit'
      },
      titleId: {
        required: '\'Title\' is required'
      },
      firstName: {
        required: '\'First Name\' is required'
      }
    },
    medical: {
    }
  };

当然,最重要的是,我有数据模型定义,然后是后端的所有层; 我们必须做些什么来减少代码膨胀?

我也只是因为无法检索为 FormControl 配置的验证器而受到打击。

我通过添加一个实用服务来解决这个问题,该服务在将配置对象传递到 FormBuilder 之前对其进行包装,并跟踪控件名称与其给定验证器之间的映射。

消除对这项额外服务的需求自然会很棒😄

好吧,自从我在这里发布以来,我已经将所有表单配置合并到一个大对象中,并编写了一些辅助方法来根据配置在表单和模型之间进行映射。 这一切似乎都太复杂了,但这可能只是我的学习曲线。

你好,
只是想知道是否有关于此功能的任何消息。 在我看来,能够根据需要在视觉上标记某些项目是任何现代表单 UI 所期望的基本功能。 然而,由于无法检索字段是否需要的信息,因此无法使用响应式表单来实现此功能。

这不是不可能的,这只是很多工作。

@Bidthegod你的意思是用 Angular 实现这个功能需要做很多工作,或者在 Angular 组件中检索验证器需要做很多工作。

如果可以检索组件中的验证器,欢迎提供有关如何操作的任何输入!

或者,如果某些组件类型(输入、选择)更容易,它也很有用。

@Toub我最终做的是构建一个“配置”对象结构,其中包含每个单独的表单字段及其所有设置(显示、验证器和规则等)。 然后我编写了一些辅助方法来查询配置,它构建了 FormGroup。 每个表单都有一个表单服务,并且继承自一个基本组件。

我不会说这是一个简单的修复,而且它有大量的代码,但它最终变得相当优雅并且很容易创建一个相当复杂的表单。 将一些代码放在一个要点中供您查看,但目前我没有时间对其进行适当的模块化。 这里缺少很多文件 - 包括我编写的所有自定义表单输入组件。

https://gist.github.com/Bidthedog/1dc7d10cda1759061c09f44f7b48cbf3

@Bidthedog它不符合我的需求,但感谢您的回答,因为要点可能对其他人有用。

嘿伙计们,我创建了一个简单的帮助器来返回验证器(仅用于所需,但可以为其他人扩展):

getValidators(_f) {
  return Object.keys(_f).reduce((a, b) => {
    const v = _f[b][1];
    if (v && (v === Validators.required || v.indexOf(Validators.required) > -1)) {
      if (!a[b]) { a[b] = {}; }
      a[b]['required'] = true;
    }
    return a;
  }, {});
}

const _f = {
  id: [],
  name: [null, Validators.required],
  phone: [],
  email: [null, [Validators.required, Validators.email]]
};
this.frmMain = this._fb.group(_f);
console.log(this.getValidators(_f));    // {name: {"required": true}, email: {"required": true}}

符合我的需求👍

我同意,拥有一个可以通过当前控件上的方法获得的验证器的只读列表将非常有帮助。

对此功能 +1

+1 只读列表

+1

我需要该功能来动态添加验证器,而不会丢失已经存在的验证器。 就像是:
Object.keys(this.myForm.controls).forEach(key => { if (map.get(key)) { this.myForm.get(key).setValidators(this.myForm.get(key).getValidators().push(Validators.required)) } });
充分解释: https ://stackoverflow.com/questions/46852063/how-to-dynamically-add-validators-to-the-forms-in-angular-2-4-using-formbuilde。
这一定是常见的

使用 composeValidators 动态添加验证器实际上并不太难(https://github.com/angular/angular/blob/master/packages/forms/src/directives/shared.ts#L139)。 我认为删除验证器是更大的问题。 你可以用一个布尔值来做,但是你的验证器不再是一个纯函数。

此功能应适用于 FormGroups 和 FormControls 以确定所需的验证器

      export const hasRequiredField = (abstractControl: AbstractControl): boolean => {
        if (abstractControl.validator) {
            const validator = abstractControl.validator({}as AbstractControl);
            if (validator && validator.required) {
                return true;
            }
        }
        if (abstractControl['controls']) {
            for (const controlName in abstractControl['controls']) {
                if (abstractControl['controls'][controlName]) {
                    if (hasRequiredField(abstractControl['controls'][controlName])) {
                        return true;
                    }
                }
            }
        }
        return false;
    };

@mtinner那真是一个很棒的发现。 这让我得到了最初的设定。 我认为仍然值得添加到 API 以允许返回可观察的验证器。 这样找到一个有条件的所需控件将非常简单。 我不确定是否可以应用此处建议的解决方法来捕获这些实例。

在 Angular Connect 2017 的精彩演示中, @kara最后说“自定义表单控件很棒且不可怕”。 很好,但是缺少自定义控件的这个重要功能

+1

没有时间做公关,但这是我想出的:

import {FormControl} from '@angular/forms';
import {AsyncValidatorFn, ValidatorFn} from '@angular/forms/src/directives/validators';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
import {AbstractControlOptions} from '@angular/forms/src/model';

export class DescendingFormControl extends FormControl {

    private asyncValidationChangesSubject = new Subject<AsyncValidatorFn | AsyncValidatorFn[]>();
    private validationChangesSubject = new Subject<ValidatorFn | ValidatorFn[] | null>();

    public readonly validationChanges: Observable<ValidatorFn | ValidatorFn[] | null>;
    public readonly asyncValidationChanges: Observable<AsyncValidatorFn | AsyncValidatorFn[]>;

    constructor(formState?: any, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null) {
        super(formState, validatorOrOpts, asyncValidator);
        this.validationChanges = this.validationChangesSubject.asObservable();
        this.asyncValidationChanges = this.asyncValidationChangesSubject.asObservable();
    }

    public setValidators(newValidator: ValidatorFn | ValidatorFn[] | null): void {
        super.setValidators(newValidator);
        this.validationChangesSubject.next(newValidator);
    }

    public setAsyncValidators(newValidator: AsyncValidatorFn | AsyncValidatorFn[]): void {
        super.setAsyncValidators(newValidator);
        this.asyncValidationChangesSubject.next(newValidator);
    }
}

如果有人愿意在我有空闲时间之前完成整个公关工作,那就去吧!

+1

+1

+1

问题:它被困住了

存在一系列现有验证器,但我们无法使用...
查看 Validators 静态方法compose ,这是在表单控件上定义 Validators 数组时在后台使用的方法:

 static compose(validators: (ValidatorFn|null|undefined)[]|null): ValidatorFn|null {
    if (!validators) return null;

    // Here it is
    const presentValidators: ValidatorFn[] = validators.filter(isPresent) as any; 

    if (presentValidators.length == 0) return null;

    return function(control: AbstractControl) {
      return _mergeErrors(_executeValidators(control, presentValidators));
    };
  }

https://github.com/angular/angular/blob/master/packages/forms/src/validators.ts#L342

如您所见, presentValidators是我们正在寻找的东西,但它_trapped_ 在一个闭包中,因此无法访问,并且您从compose获得的唯一值是一个包含所有触发错误的对象给定的值。

因此,推断验证器的唯一方法是手动触发表单控件的错误,该控件将引用验证器名称。

hack:在不同的控件上触发错误

您不想在绑定的表单控件上触发错误。 相反,您应该创建一个新的表单控件,不受任何与模板的绑定,并将相同的 Validator 函数应用于他,然后通过设置一个值来触发您想要的错误:

// Your form control, actually binded to a form in your template

emailControl = new FormControl('', [Validators.required, Validators.email])

// Elsewhere in your code
// You use dummy form controls, on which you apply your original form control validators and various values, to trigger the errors.

const errorsWhenEmpty = new FormControl('', this.emailControl.validator, this.emailControl.asyncValidator).errors
const errorsWhenOneChar = new FormControl('x', this.emailControl.validator, this.emailControl.asyncValidator).errors

// Or you can simply reuse the first dummy control with `setValue()`

这远非理想,但这是我发现的最不肮脏的方式,直到我们能够亲自接触验证者。

我建议只从 FormControl 类扩展并在子类的字段中保存传递给构造函数的 Validator 函数的副本,如下所示:

export class ExtFormControl extends FormControl {
  private _syncValidators: ValidatorFn | ValidatorFn[];

  private _asyncValidators: AsyncValidatorFn | AsyncValidatorFn[];

  constructor(formState?: any, validatorOrOpts?: ValidatorFn | ValidatorFn[] | null,
              asyncValidators?: AsyncValidatorFn | AsyncValidatorFn[] | null) {
    super(formState, validatorOrOpts, asyncValidators);
    this._syncValidators = validatorOrOpts;
    this._asyncValidators = asyncValidators;
  }


  getSyncValidators(): ValidatorFn[] {
    if (typeof this._syncValidators === 'function') {
      return [this._syncValidators];
    } else {
      return this._syncValidators;
    }
  }

  getAsyncValidators(): AsyncValidatorFn[] {
    if (typeof this._asyncValidators === 'function') {
      return [this._asyncValidators];
    } else {
      return this._asyncValidators;
    }
  }

  setValidators(newValidator: ValidatorFn | ValidatorFn[] | null): void {
    super.setValidators(newValidator);
    this._syncValidators = newValidator;
  }

  setAsyncValidators(newValidator: AsyncValidatorFn | AsyncValidatorFn[] | null): void {
    super.setAsyncValidators(newValidator);
    this._asyncValidators = newValidator;
  }

}

我完全赞成验证器的只读列表。 在某些情况下,使用 ReactiveForms,人们希望通过 FormBuilder 代码而不是模板来配置像 maxLength 这样的约束。 但目前有必要在 HTML 中放置 maxlength,以便能够定义一些行为(例如将焦点移动到下一个字段,信用卡输入中的典型案例),或者根据需要自动标记标签等。

我需要以编程方式设置验证器,这些验证器会随着表单的填写而更新。能够读取 FormControl 上的验证器真是太好了。 我同意@moniuch的观点,即使用 ReactiveForms 可以做到这一点。 只是表达我希望它得到实施。

任何更新,伙计们?

嗯,我在19851年看到这个化合物是一个有趣的问题
我们看不到设置验证器或对它们进行优先级排序。 :(

我通过检查控件的“验证器”属性为我解决了这个问题。 它使用ACTIVE验证器返回对象。 如果设置了验证器,并且验证器为真的条件得到满足。
在我的示例中,它甚至是NESTED COMPONENT中使用的REACTIVE FORM

父组件

PARENT 组件中 ReactiveFormGroup 中的验证器

return this.formBuilder.group({
      username: ['', [Validators.required, Validators.min(3), Validators.email]],
     ...
});

这些名称仅用于测试目的

<app-input name="txtfield" 
[parentForm]="loginForm" parentFormControlName="txtfield" 
></app-input>

嵌套组件

 <div [formGroup]="parentForm" class="form-control-group">
   ...
        <input #inputField  name="{{parentFormControlName}}" 
            formControlName="{{parentFormControlName}}" 
            matInput 
            placeholder="{{placeholderTxt}}"
        >
  ...
    </div>

在嵌套组件中,我只在 AfterViewIinit 之后检查一次,因为我只需要“必需”验证器。

ngAfterViewInit() {
    // Lookup if this field is required or not
    if (this.parentForm.controls[this.parentFormControlName].validator != null) {
    // MAYBE CHECK FOR required field in returned OBJECT!!
      console.log('Validators of ' + this.parentFormControlName + ' = ', 
           JSON.stringify(
             this.parentForm.controls[this.parentFormControlName]
                      .validator(this.parentForm.controls['username']);)
            );

    } else {
      console.log('No validator set for component ' + this.parentFormControlName); 
    }

调试器停止时在浏览器控制台中的输出:

[DEBUG] STATUS of username =  {"required":true}
>this.parentForm.controls[this.parentFormControlName].validator(this.parentForm.controls[this.parentFormControlName])
{required: true}
required: true
__proto__: Object

否则 - 意味着如果没有设置验证器 - 它显示“null”!

其他情况

如果输入文本字段具有默认值

如果 input-text-field 具有默认值,则不会显示所需的值为 true,但如果该字段是必需的且未填充,我只想突出显示该字段(= 行粗体)。

超出要求的其他值

您可以随时检查任何其他验证器状态,如果您使用“DoCheck”或“OnChanges”观察更改以获取“电子邮件”或“最小”验证器......如果您需要这个。

@Angular团队,确实有必要让 API 检查当前验证器列表并在运行时删除特定验证器。 这解决了很多问题。

为什么“control.errors”和“controls.validators”做同样的事情?
.errors ”应该给出所有错误而不是达到错误状态。
" .validators " 应该为这个控件返回所有注册的验证器。
就如此容易?

我也非常希望看到这个功能。 但! 我看到的一个大问题是验证器(通常)是纯函数——这意味着它们可以嵌套,甚至是深度嵌套。

在我们的项目中,我们有许多与领域相关的验证器,然后,它们是由其他验证器(等等)构成的。 例如,我们的 IBAN 验证器在底层使用exactLengthpattern和 IBAN 校验位验证器。 遵循这种模式会导致更具体的验证错误,并允许我们使用更详细地描述问题的错误消息(也就是更好的可用性)。

因此,在考虑一种检索所有验证器的新方法时,问题是:只会返回“顶级验证器”吗? 另外,这些验证器的名称是什么? 总的来说,在我看来,对于当前的验证实施,没有简单的解决方案......

TL;DR:如果不更改 Angular 表单模块中的大量内容并破坏现有代码,这是不可能的。

所以伙计们,相关的源代码是这样的:
包/表单/src/model.ts
包/表单/src/validators.ts
可能还有packages/forms/src/directives/validators.ts

我发现了以下问题:

  • 首先,验证器只是一个愚蠢的函数对象,无法识别它。 即使你能得到一个函数数组,那你会怎么做? 如果正则表达式验证器只是一个函数,您将如何删除正则表达式验证器? 我们需要一些方法来确定这个是正则表达式验证器,如果我们想单独添加或删除它们,这个是必需的验证器。 所以,你可以实现addValidator函数,但你不能实现removeValidator函数。
  • 其次,当您setValidators时,最终会调用 compose 函数。 它将所有验证器函数集中到一个函数中,并通过调用内部 _executeValidators 函数来实际调用它们。 所以最后我们没有原始的验证器数组,我们只有一个巨大的超级验证器,所有错误列表合并。

由此我得出结论,如果不对验证器的概念进行一些重大更改,就不可能实现getValidatorsremoveValidator 。 似乎可以实现addValidators ,但是如果我只能添加更多验证器并且不能删除现有的验证器,谁需要它?

那么,如果您想在您的应用程序中实现有条件的动态验证,您应该怎么做,例如,如果设置了此复选框,则在不丢失所有现有电子邮件验证器的情况下使此电子邮件字段成为必填项? 我找到了两种方法:

  • 您可以将字段的默认验证器存储在变量中的某个位置。 当需要电子邮件时,您将使用setValidator函数并将 Validator.required + 所有默认验证器传递给它。
  • 您可以创建一个表单组验证器,而不是使用表单控件所需的验证器,该验证器将监视复选框和您的字段中的更改。 当复选框设置为 true 时,它​​的行为就像需要电子邮件字段一样。

@ganqqwerty ...正确。 这就是为什么addValidator(...)removeValidator(...)不存在的原因。 没有身份证明。 验证器本身不知道在哪个控件上运行。

@ganqqwerty我并不是说这很容易实现,但我认为您的推理在这里不成立。

您将如何删除正则表达式验证器

通过传递对验证器的引用。 与删除事件侦听器的方式相同。

所以最后我们没有原始的验证器数组

这只是一个内部设计决策,目前未在公共 API 的任何地方公开。 选择保留原始数组而不是一遍又一遍地组合函数实际上可能会带来性能优势,因为您不会有额外的函数,无论如何都必须将验证器数组保持在闭包中。

如果设置了此复选框,则在不丢失所有现有电子邮件验证器的情况下使此电子邮件字段成为必需:[...] 您可以创建一个表单组验证器,而不是使用表单控制所需的验证器,该验证器将监视复选框和您的字段中的更改.

这不是预期的行为吗? 如果具有有效的电子邮件地址,您的带有电子邮件字段的表单控件本身就是有效的。 如果复选框的值为true ,则如果存在电子邮件,则您的 _group_ 有效。 我不认为这是验证器的解决方法,而是它们在表单的正确级别上的正确使用。

感谢您向我澄清这一点! 这是否意味着可以安排实施此类功能?

通过传递对验证器的引用。

因此,如果我们保留对验证器的引用,就可以将其删除。 如果我们只是提供一个匿名函数作为验证器,我们将无法删除它。 嗯,确实看起来像removeEventListener

如果没有此功能,就不可能从特定控件删除特定验证器

这就是我现在需要的,我无法解决这个问题。 有时超级基本的东西是不可能用角度来做的,这太疯狂了。

this.myAbstractFormControl.removeValidatorAndKeepTheRestAsIs(Validators.required)

@andreElrico您删除验证器的用例是什么? 我对此思考得越多,就越不明白在表单上不断更改验证器的意义。 如果您需要根据表单中的不同字段动态更改验证器,那么您放置的验证器是错误的:您想要的是表单组验证器,而不是表单控件验证器。

我试图使用通过例如在两个场景中动态输入提供的相同 formGroup。 我可以在每个字段上要求的一种情况,第二种情况是我删除所需的 IF 存在并将 atLeastOneField 验证器应用于 formGroup。

这是我正在处理的堆栈闪电战

我不明白为什么有些人会保护 Angular 团队就现有验证处理产生的设计缺陷。
Angular Form-Validation 应该是CRUD

@andreElrico正如我所说,如果您的表单有两个输入具有相互依赖的验证,那么您的验证器应该在表单组中,而不是在表单控制中。

例如,如果您有两个密码字段,则不应在每次更改第一个控件时更改第二个控件的验证器。 相反,验证器应该进入一个包含两个密码字段的组。 每个密码字段都有验证,密码必须至少为 8 个字符长,但组有验证器,它们需要相同。 就其本身而言,每个密码字段都不知道另一个密码字段的存在。 该小组将他们联系在一起。

如果你有类似的东西

( ) a
( ) b
( ) other [ _____ ]

如果用户选择“其他”,则输入字段旁边是必需的,那么再次验证器“必需”不应在输入字段上切换。 相反,整个表单组需要验证:单选按钮控件需要是“a”、“b”或“other”之一,如果是“other”,则必须存在输入字段。

您不应该在单选按钮上有一个侦听器,然后根据单选按钮的值切换输入字段的“必需”验证器。

我不明白为什么你认为验证应该有一个 CRUD API。 我不是在为任何事情辩护,我说的是合理的,有论据的。 我并不是说不应该实现获取、删除和切换验证器的方法,我是说这里几乎每个人都有一个 CRUD 验证的用例,这实际上是反模式。 控件是一个独立的单元,它在其生命周期内不会更改其验证规则。 当控件依赖时,它们会被放入一个组中并验证该组。

感谢@lazarljubenovic的评论和时间。
我觉得你还没有仔细看过我的 stackblitz。 我在一种情况下在 formGroup 中应用“包装”逻辑。 我的表单只有两个不同的场景,需要验证器动态更改。 在我处理 formGroup 的那个“阶段”,我的验证器已经“组合”了,所以对我来说没有 CRUD:D。

我可以解决这个问题,并希望有一天能解决这个问题。
167 👍 似乎反映了对这个功能的高度渴望。

如果你有时间,我很想了解更多:

“控件是一个独立的单元,它在其生命周期内不会更改其验证规则。”

你有这方面的资源吗?

“当控件依赖时,它们会被放入一个组中并验证该组。”

这实际上也有其局限性。

也与此问题无关:
如果该组接管了控件的验证,则错误将被设置到该组,因此不会在控件上直观地反映出来。 所以我们必须手动设置和删除子控件的错误。 (这是*的一大痛苦)。

祝大家周末愉快。

我当然理解@lazarljubenovic的观点。

但是,我处理需要复杂组合的高度动态、多部分的企业表单……大量的隐藏/显示、验证需求依赖链等。

我们处理这种复杂性的方法是订阅任何负责触发依赖关系的表单控件、组或数组上的 valueChanges,然后对每个场景使用 setValidators 或 clearValidators。 由于无法针对特定的验证器,我们存储可能在任何给定时间应用的每组验证规则并重新应用它们。 能够检索所有验证器、添加单个验证器或针对特定验证器进行删除肯定会很好,这似乎仍然可以在考虑不变性的情况下进行架构。

“控件是一个独立的单元,它在其生命周期内不会更改其验证规则。”

你有这方面的资源吗?

不; 这真的只是我对表单设计的解释。

如果该组接管了控件的验证,则错误将被设置到该组,因此不会在控件上直观地反映出来。

老实说,即使在那些错误应该去哪里的 UI 方面也是值得怀疑的。 当两个字段不一起玩,但在表格中相距很远时,你应该在哪里写错误? 在我看来,您应该退后一步,首先修复表单的 UX。

当然,表单是一头野兽,它们不可能适合所有用例。 当我有一个带有_really_ 疯狂控件的非常复杂的表单时,我根本不使用@angular/forms 。 我认为@angular/forms是一个很棒的实用程序,它涵盖了 95% 的用例,并且让我能够轻松快速地构建一个表单。

就像我喜欢使用Array#mapArray#filterArray#reduce一样,有时带有索引变量的老式for循环是更复杂迭代的最佳解决方案.

我想再次明确一点,我不是_反对_添加这些方法; 我只是觉得大多数人会滥用它们。 也许它会是一支步枪。 再说一次,我认为不应仅仅因为有人滥用它就将有用的东西添加到框架中。 您也可以用花露水刷牙,因此我们不会禁止上厕所。

当我有一个非常复杂的表单和非常疯狂的控件时,我根本不使用@angular/forms。 我认为 @angular/forms 是一个很棒的实用程序,它涵盖了 95% 的用例,并且让我能够轻松快速地构建一个表单。

这是Angular Forms的官方市场定位吗? 我认为它更像是对大型项目有用的通用工具。

当我有一个非常复杂的表单和非常疯狂的控件时,我根本不使用@angular/forms。 我认为 @angular/forms 是一个很棒的实用程序,它涵盖了 95% 的用例,并且让我能够轻松快速地构建一个表单。

这是Angular Forms的官方市场定位吗? 我认为它更像是对大型项目有用的通用工具。

“反应式表单”旨在用于复杂、高度定制的用例……例如能够完全控制您的验证;)

你可以完全控制它。 您只需要正确确定要验证的内容:组或控件。

这有什么吸引力吗? 我真的很想动态更改我的验证器,只使用一个 formGroup 作为输入,以保持一切解耦!!! 反应形式很棒,但这是我唯一的坚持。

Sooooooooo 我应该如何对动态添加验证器的代码进行单元测试?

看来明显的单元测试不起作用,因为我无法_读取_任何控件应用的验证器。

_这行不通_
expect(component.formControl.validators.length).toEqual(2);

更新
实际上,您可以这样做。 然而,读取验证器显然会更便宜:

component.myCtrl.setValue('');
expect(component.myCtrl.errors.required).toBeTrue();

我是 Reactive Forms 的忠实粉丝,并且喜欢我可以用它们做什么,但是像无法向 FormControl 询问其验证器这样的小事情似乎是一种疏忽。 鉴于 FormControl 上有如此多的状态属性,是什么阻止了至少使.required属性可用? 想象一下,如果这是日常生活中的情况......

我:你好,我是来赴约的。
接待员:对不起,我们不能见您。
我:为什么不呢? 我现在有个约会。
接待员:耸耸肩,我不知道。

我有点概括,但如果我处理 FormControl 属性值,我也应该能够访问这些值。

至少阻止 .required 属性可用的原因

您可以创建自己的required验证器这一事实。 Angular 无法知道你的函数是做什么的。

100% 明白,我不是说它基于函数。 知道一个字段是必需的就像知道它的启用/禁用一样基本,它是可配置的、可访问的和反映在 UI 中的。
有什么可以做的,以便开发人员可以为需要的字段获得类似的功能,以便他们可以通过使用 FormControl 来有条件地显示所需的标记?

嗨,这是我们最新的 hack,用于自动标记所需的输入: https://stackblitz.com/edit/material-input-auto-required-asterisk。 相同的概念可以应用于 mat-select 或任何其他库。

const _clearValidators = AbstractControl.prototype.clearValidators;
AbstractControl.prototype.clearValidators = function(){
  (this as any).isRequired = false;
  _clearValidators.call(this);
}

const _setValidators = AbstractControl.prototype.setValidators;
AbstractControl.prototype.setValidators = function(newValidator: ValidatorFn | ValidatorFn[] | null): void {
  (this as any).isRequired = false;
  _setValidators.call(this, newValidator);
}

export function isRequired(control: AbstractControl): ValidationErrors | null{
  (control as any).isRequired = true;
  return Validators.required(control);
}

@Directive({ selector: '[matInput]:not([required])' })
export class MatInputRequiredDirective implements DoCheck {
  constructor(private readonly input: MatInput) { }

  ngDoCheck() {
    const isRequired = (this.input.ngControl && this.input.ngControl.control as any).isRequired || false;
    if(isRequired !== this.input.required){
      this.input.required = isRequired;
      this.input.ngOnChanges();
    }
  }
}

100% 明白这一点,我不是说它基于一个函数。 知道一个字段是必需的就像知道它的启用/禁用一样基本,它是可配置的、可访问的和反映在 UI 中的。

我不确定。 “需求”非常特定于应用程序的业务逻辑。 空字符串是否通过“必需”条件? 零呢? 多选的空数组? 复选框为假? 空,未定义?

有什么可以做的,以便开发人员可以为需要的字段获得类似的功能,以便他们可以通过使用 FormControl 来有条件地显示所需的标记?

您已经可以根据您喜欢的任何条件有条件地显示/隐藏错误。 错误的存在并不一定意味着您必须在任何地方显示它。 我看不到查询验证器而不是简单地查询错误(可用)如何对您有所帮助。

我不确定。 “需求”非常特定于应用程序的业务逻辑。 空字符串是否通过“必需”条件? 零呢? 多选的空数组? 复选框为假? 空,未定义?

我可以争辩说,启用/禁用的字段也非常特定于应用程序的业务逻辑,但 FormControl 为开发人员提供了通过构造函数和访问器进行管理的功能。 被要求是一条模糊的线,我知道你来自哪里。 这可能是个人偏好,但它是一个表示状态(如启用/禁用、有效/无效),因为它是值有效性的指示。

您已经可以根据您喜欢的任何条件有条件地显示/隐藏错误。 错误的存在并不一定意味着您必须在任何地方显示它。 我看不到查询验证器而不是简单地查询错误(可用)如何对您有所帮助。

是的,我可以有条件地隐藏/显示错误,但是一旦这些错误得到解决,任何脱离预期状态的展示部分都不再可用。 我无法根据验证器显示星号,因为一旦这些验证器满意,它们就不再可用。
我想现在我只需要解决它。

启用/禁用的字段也非常特定于应用程序的业务逻辑

我看不出怎么做。

我无法根据验证器显示星号

因此,请根据您自己的“必需”语义添加一个验证器。 不是所有的东西都可以是单线的。

我正在寻找任何合理的方式来支持这种情况,不一定是单线。 我也希望了解您的观点,以防我的观点错位。

如果我的语义只是普通的并且只需要利用 Validators.required 验证怎么办? 在下面的 StackBlitz 示例中,如果我将它添加到文本字段并输入一个值,了解它的唯一方法是通过field.errors对象并查找与验证器关联的属性。 但是,当验证器满足时,该属性不再存在。 也许我的方法不正确,有没有更好的方法来利用 FormControl 来实现我没有利用的这一点?

StackBlitz 示例

您不应该根据不同字段的有效性来切换验证器。 创建一个组验证器并检查包含名称和辅助名称的对象的值。 有关更多信息,请参阅此评论

至于明星......好吧,从某种意义上说,你的方法并不是“不正确的”,“这是一个糟糕的想法”,但这是一个不适用于验证器当前设想的方式的想法。 所以我宁愿说你的方法是_incompatibile_与现在设想如何使用表单。

如果您想快速实现您描述的效果,您需要创建自己的迷你表单创建库。 如果我遇到这个问题,下面是我如何解决这个问题的快速草图。 例如,您可以有一个函数createForm接受spec对象,如下所示:

spec = {
  name: {
    initialValue: '',
    isRequired: (formValue) => true, // always required
  },
  secondName: {
    initialValue: '',
    isRequired: (formValue) => isFilledIn(formValue.name), // required only if name is filled in
  }

isFilledIn是您确定是否填写值的函数。 可能类似于val => val != null && value != ''

createForm将返回与您创建它时相同的形式:

this.fb.group({
  name: [''],
  secondName: [''],
}, {
  validators: [
    (group) => {
      const value = group.value
      const errors = {}
      if (spec.name.isRequired(value) && !isFilled(value.name)) {
        errors['name-required'] = true
      }
      if (spec.secondName.isRequired(value) && !isFilled(value.secondName)) {
        errors['secondName-required'] = true
      }
      return errors // or null if no errors found
    }
  ]
})

错误现在将出现在组本身上。 secondName的错误确实属于那里 - 您无法自行验证secondName ,因为它的有效性取决于不同的字段(因此您可以验证这些字段的组)。 name的错误应该属于name ,但是这个实现草图会将它放在表单组中。 这可能对您有用,也可能不适用。 您可以在spec对象上引入一个字段,这样可以进一步调整配置。

现在在模板中您可以像往常一样使用您的表单,因为您手头有@angular/forms对象。 您需要打印表单组中的一些错误和控件中的一些错误(或者如果适合您,则可能全部来自组)。 切换星星的条件是spec[key].isRequired(form.value)

如果您要实现它的精确程度,可能性是无穷无尽的,而且这一切都取决于如此微小的决定,我认为任何表单 API 都不会满足所有人。 因此,根据您对自己项目中表单的期望,我认为最好在表单构建器周围引入一个“语义”包装器,它将根据您的要求执行其他操作。

感谢您的方法和花时间参考以前的评论,线程历史很长:)

我认为这就是我的部分问题所在; 正确管理分组控件和组外控件之间的字段依赖关系。 规范方法一直是我在大多数领域的后备方案,所以我很高兴它似乎也是你的方法。

我认为任何表单 API 都不会满足所有人

确实,不确定任何提供的 API 是否满足这种覆盖范围!

任何更新? 自创建此问题以来将近 4 年。
我们目前正在创建自定义实用程序函数来存储验证,这些函数从“香草”角度派生太多,只是为了获取 formControl 的验证器。

真的需要这个功能

这对于那些包装响应式表单的用户来说非常有帮助

非常有价值的线程。 但是,我对通过不授予表单控件上的验证器访问权限来解决什么目的感到有些困惑。 我遇到了这个要求,我们的自定义组件需要在需要时显示“*”,并且可能在其下方显示相关的错误消息(如果用户想要的话)。 另一种选择是通过让用户也从模板中设置一个属性来添加到控件本身的 API。 但这不应该是必需的,因为用户已经以反应形式提供了他所需的验证器(或任何其他验证器)。 我的自定义组件应该能够知道设置了这样的验证器,然后它可以基于此显示/隐藏元素。 似乎是一个非常普遍的要求。

具有自定义配置的自定义 FormControl 的解决方案,以获取有关验证器的所有信息并可能完全控制这些验证器:

export class CustomFormControl extends FormControl {

  public listValidator: CustomValidator[] = [];

  private constructor(formState?: any,
                      validatorOrOpts?: ValidatorFn | ValidatorFn[] | CustomCtrlOpts | null,
                      asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null) {

    super(formState, validatorOrOpts as ValidatorFn[], asyncValidator);

    if (validatorOrOpts instanceof CustomCtrlOpts) {
      this.listValidator = validatorOrOpts.customConf;
    }
  }

  // Builder ith default value
  public static init(formState?: any): CustomFormControl {
    return new CustomFormControl(isNil(formState) ? null : formState);
  }

  public required(): CustomFormControl {
    return this.and('required');
  }

  public email(): CustomFormControl {
    return this.and('email');
  }

  public custom(): CustomFormControl {
    return this.and('custom');
  }

  public min(min: number): CustomFormControl {
    return this.and('min', min);
  }

  public max(max: number): CustomFormControl {
    return this.and('max', max);
  }

  private and(key: TypeCustomValidator, value?: any): CustomFormControl {
    this.listValidator.push({key: key, value: value});
    this.setValidators(new CustomCtrlOpts(this.listValidator).validators as ValidatorFn[]);
    return this;
  }
}

// Add others if needed
export type TypeCustomValidator = 'min' | 'max' | 'required' | 'email' | 'custom' ;

export interface HandledValidator {
  key: TypeCustomValidator;
  function: Function;
  params?: boolean;
}

export const listHandledValidators: HandledValidator[] = [
  {key: 'required', function: Validators.required},
  {key: 'email', function: Validators.email},
  {key: 'min', function: Validators.min, params: true},
  {key: 'max', function: Validators.max, params: true},
  {key: 'custom', function: custom}
];

export interface CustomValidator {
  key: TypeCustomValidator;
  value?: any;
}

export class CustomCtrlOpts implements AbstractControlOptions {

  validators?: ValidatorFn | ValidatorFn[] | null;
  asyncValidators?: AsyncValidatorFn | AsyncValidatorFn[] | null;
  updateOn?: 'change' | 'blur' | 'submit';

  private _customConf?: CustomValidator[];

  constructor(value: CustomValidator[]) {
    this.customConf = value;
  }

  get customConf(): CustomValidator[] {
    return this._customConf;
  }

  set customConf(value: CustomValidator[]) {

    this._customConf = value;

    if (!value) {
      return;
    }

    this.validators = [];

    value.forEach(customValidator => {
      const validator = listHandledValidators.find(it => it.key === customValidator.key);
      if (validator.params) {
        (this.validators as ValidatorFn[]).push(validator.function(customValidator.value) as ValidatorFn);
      } else {
        (this.validators as ValidatorFn[]).push(validator.function as ValidatorFn);
      }
    });
  }
}

与 formGroup 初始化类似

this.form = this.fb.group({
    amount: CustomFormControl.init(10).required().min(0), 
    ...

这将是一个非常好的功能。 为此使用解决方法感觉像是在浪费时间。

真的需要这个功能

是的,这真的很需要。 简单来说,就是想如果有办法设置东西,那么一定也有类似的方法来获取它不是吗? 如果您可以轻松设置某些东西,但要让它们恢复,您必须花费很多精力,那么显然缺少一些_真正需要_的东西。

这个问题/功能有什么进展吗?

@BenKatanHighLander正在路上: https ://github.com/angular/angular/pull/37263#issuecomment -723333060

此页面是否有帮助?
0 / 5 - 0 等级