@angular/forms 源码解析之 Validators

栏目: 编程语言 · AngularJS · 发布时间: 7年前

内容简介:我们知道,@angular/forms 包主要用来解决表单问题的,而表单问题非常重要的一个功能就是表单校验功能。这样输入的如果不是 email 格式,实际上,上面 demo 中不仅仅绑定了

我们知道,@angular/forms 包主要用来解决表单问题的,而表单问题非常重要的一个功能就是表单校验功能。 数据校验非常重要,不仅仅前端在发请求给后端前需要校验数据,后端对前端发来的数据也需要校验其有效性和逻辑性,尤其在存入数据库前还得校验数据的有效性。 @angular/forms 定义了一个 Validator 接口 ,并内置了 RequiredValidator CheckboxRequiredValidator EmailValidator MinLengthValidator MaxLengthValidator PatternValidator 六个常用的校验指令,每一个 validator 都实现了 Validator 接口 。这些校验指令的使用很简单,比如使用 EmailValidatorRequiredValidator 指令来校验输入的数据得是 email 且不能为空:

<input type="email" name="email" ngModel email required>
复制代码

这样输入的如果不是 email 格式, EmailValidator 指令就会校验错误,会给 host(这里也就是 input 元素)添加 'ng-invalid' class ,这样开发者可以给这个 class 添加一些 css 效果,提高用户体验。那么,其内部运行过程是怎样的呢?

实际上,上面 demo 中不仅仅绑定了 NgModel 指令,还绑定了 EmailValidatorRequiredValidator 两个 validators 指令。指令在实例化时是按照声明顺序依次进行的,有依赖的指令则置后, FormsModule 先是声明了 RequiredValidator 指令,然后是 EmailValidator 指令,最后才是 NgModel ,所以实例化顺序是 RequiredValidator -> EmailValidator -> NgModel,同时由于 NgModel 依赖于 NG_VALIDATORS ,所以就算 NgModel 声明在前也会被置后实例化。 RequiredValidatorEmailValidator 在实例化过程中都会提供 REQUIRED_VALIDATOR EMAIL_VALIDATOR 两个服务,并且 StaticProvider 的 multi 属性设置为 true,这样可以容许有多个依赖服务(这里是 RequiredValidator 和 EmailValidator 对象)公用一个令牌(这里是 NG_VALIDATORS), multi 属性作用可以查看源码中说明 。当 NgModel 实例化时,其构造依赖于 @Self() NG_VALIDATORS @Self() 表示从 NgModel 指令挂载的宿主元素中去查找这个令牌拥有的服务, NgModel 没有提供 NG_VALIDATORS ,但是挂载在 input 宿主元素上的 REQUIRED_VALIDATOREMAIL_VALIDATOR 却提供了这个服务,所以 NgModel 的依赖 validators 就是这两个指令组成的对象数组。

NgModel在实例化时,由于没有父控件容器,所以会调用 _setUpStandalone() ,从而调用 setUpControl() 方法设置 FormControl 对象的 同步 validator 依赖 (如果有异步 validator 依赖,也同理),这个依赖是调用 Validators.compose() 返回的一个 ValidatorFn 函数。而 Validators.compose() 参数调用的是 NgModel.validator ,也就是调用 composeValidators 获得 ValidatorFn,内部会调用 normalizeValidator() 函数转换为为 (AbstractControl) => Validator.validate() 。所以,和 input 控件绑定的 FormControl 对象就有了同步 validator 数据校验器。那在 input 输入框内输入数据时,校验器是在何时被运行的呢?

NgModel实例化时,还安装了一个 视图数据更新回调 ,这样当 input 视图内的数据更新时,就会运行这个回调,该回调会更新 FormControl 的 value 值,即 FormControl.setValue() 函数 ,内部会调用 updateValueAndValidity ,从而开始 运行数据校验器 ,上文说到 FormControl 的 validator 依赖实际上是 Validators.compose() 返回的函数,所以此时会运行 这个回调函数 ,而这个 presentValidators 是 (AbstractControl) => RequiredValidator.validate() 和 (AbstractControl) => EmailValidator.validate() 组成的数组,然后依次 运行 这两个 Validator 的 validate() 函数。如果校验错误,就返回 ValidationErrors ,比如 email 校验器返回的是 {'email': true} 。这里还需注意的是,Validator 指令里的 validate() 函数实际上调用的还是 Validator 类 的对应的静态函数,这样验证器指令可以直接在模板里使用,而 Validator 类的静态函数可以在 响应式表单 中使用。校验器运行完成后,会设置 FormControl.errors 属性,从而计算 FormControl 的 status 属性 ,假设校验错误,则 status 属性值为 INVALID 。那如果校验错误,input 的 class 为何会添加 'ng-invalid' 呢?因为实际上还有一个 NgControlStatus 指令 也在绑定这个 input 元素,该指令的依赖会从当前挂载的宿主元素查找 NgControl ,本 demo 中就是 NgModel 指令, NgControlStatus 指令 的 host 属性中的 '[class.ng-invalid]': 'ngClassInvalid' ,会运行 ngClassInvalid() 函数判断是否会有 'ng-invalid' class ,而校验错误时,该函数运行结果是 true,因为它读取的是 FormControl.invalid 属性,则 'ng-invalid' class 就会被添加到 input 元素上。同理,其他 class 如 pending、dirty 等也同样道理。这样就理解了校验器的整个运行过程,也包括为何校验错误时会自动添加描述控件状态的 'ng-invalid' class

我们已经理解了 Validators 的内部运行流程,这样写一个自定义的 Validator 就很简单了(当然,写一个自定义的 Validator 不需要去了解 Validator 内部运行原理)。比如,写一个自定义校验器 ForbiddenValidator,input 输入内容不能还有某些字符串,那可以模仿 @angular/forms 中的内置校验器 MinLengthValidator 写法:

import {Validators as FormValidators} from '@angular/forms';

export class Validators extends FormValidators {
  static forbidden(forbidden: string): ValidatorFn {
    return (control: AbstractControl): ValidationErrors | null => {
      return (new RegExp(forbidden)).test(control.value) ? {forbidden: true} : null;
    }
  }
}

export const FORBIDDEN_VALIDATOR: StaticProvider = {
  provide: NG_VALIDATORS,
  useExisting: forwardRef(() => ForbiddenValidator),
  multi: true
};

@Directive({
  selector:
    ':not([type=checkbox])[forbidden][formControlName],:not([type=checkbox])[forbidden][formControl],:not([type=checkbox])[forbidden][ngModel]',
  providers: [FORBIDDEN_VALIDATOR],
})
export class ForbiddenValidator implements Validator{
  private _onChange: () => void;
  private _validator: ValidatorFn;
  
  @Input() forbidden: string;
  
  ngOnChanges(changes: SimpleChanges) {
    if ('forbidden' in changes) {
      this._createValidator();
      if (this._onChange) this._onChange();
    }
  }
  
  registerOnValidatorChange(fn: () => void): void {
    this._onChange = fn;
  }
  
  validate(c: AbstractControl): ValidationErrors | null {
    return this.forbidden ? this._validator(c) : null;
  }
  
  private _createValidator(): void {
    this._validator = Validators.forbidden(this.forbidden);
  }
}
复制代码

这样就可以在组件模板中使用了:

@Component(
{
    template: `
        <h2>Template-Driven Form</h2>
        <input type="email" name="email" [ngModel]="email" email required [forbidden]="forbiddenText">
        <h2>Reactive-Driven Form</h2>
        <input type="email" name="email" [formControl]="emailFormControl" email required [forbidden]="forbiddenText">
        <h2>Update Forbidden Text</h2>
        <input [(ngModel)]="forbiddenText">
    `
})
export class AppComponent {
    // custom validator
      forbiddenText = 'test';
      email = 'test@test.com';
      emailFormControl = new FormControl('test@test.com', [Validators.forbidden(this.forbiddenText)]);
}
复制代码

完整代码可参见 stackblitz demo

所以,在理解了 Validator 内部运行原理后,不仅仅可以写自定义的 Validator,该 Validator 可以用于模板驱动表单也可以用于响应式表单, 还能明白为啥需要那么写,这个很重要!

也可阅读 @angular/forms 相关文章了解 NgModel 双向绑定内部原理: @angular/forms 源码解析之双向绑定


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Design Accessible Web Sites

Design Accessible Web Sites

Jeremy Sydik / Pragmatic Bookshelf / 2007-11-05 / USD 34.95

It's not a one-browser web anymore. You need to reach audiences that use cell phones, PDAs, game consoles, or other "alternative" browsers, as well as users with disabilities. Legal requirements for a......一起来看看 《Design Accessible Web Sites》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具