@angular/forms 源码解析之 Validators

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

内容简介:我们知道,@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 源码解析之双向绑定


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

查看所有标签

猜你喜欢:

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

We Are the Nerds

We Are the Nerds

Christine Lagorio-Chafkin / Hachette Books / 2018-10-2 / USD 18.30

Reddit hails itself as "the front page of the Internet." It's the third most-visited website in the United States--and yet, millions of Americans have no idea what it is. We Are the Nerds is an eng......一起来看看 《We Are the Nerds》 这本书的介绍吧!

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

在线压缩/解压 HTML 代码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器