内容简介:我们知道,@angular/forms 包主要用来解决表单问题的,而表单问题非常重要的一个功能就是表单校验功能。这样输入的如果不是 email 格式,实际上,上面 demo 中不仅仅绑定了
我们知道,@angular/forms 包主要用来解决表单问题的,而表单问题非常重要的一个功能就是表单校验功能。 数据校验非常重要,不仅仅前端在发请求给后端前需要校验数据,后端对前端发来的数据也需要校验其有效性和逻辑性,尤其在存入数据库前还得校验数据的有效性。 @angular/forms 定义了一个 Validator 接口 ,并内置了 RequiredValidator 、 CheckboxRequiredValidator 、 EmailValidator 、 MinLengthValidator 、 MaxLengthValidator 、 PatternValidator 六个常用的校验指令,每一个 validator 都实现了 Validator 接口 。这些校验指令的使用很简单,比如使用 EmailValidator 和 RequiredValidator 指令来校验输入的数据得是 email 且不能为空:
<input type="email" name="email" ngModel email required> 复制代码
这样输入的如果不是 email 格式, EmailValidator 指令就会校验错误,会给 host(这里也就是 input 元素)添加 'ng-invalid' class ,这样开发者可以给这个 class 添加一些 css 效果,提高用户体验。那么,其内部运行过程是怎样的呢?
实际上,上面 demo 中不仅仅绑定了 NgModel 指令,还绑定了 EmailValidator 和 RequiredValidator 两个 validators 指令。指令在实例化时是按照声明顺序依次进行的,有依赖的指令则置后, FormsModule 先是声明了 RequiredValidator 指令,然后是 EmailValidator 指令,最后才是 NgModel ,所以实例化顺序是 RequiredValidator -> EmailValidator -> NgModel,同时由于 NgModel 依赖于 NG_VALIDATORS ,所以就算 NgModel 声明在前也会被置后实例化。 RequiredValidator 和 EmailValidator 在实例化过程中都会提供 REQUIRED_VALIDATOR 和 EMAIL_VALIDATOR 两个服务,并且 StaticProvider 的 multi 属性设置为 true,这样可以容许有多个依赖服务(这里是 RequiredValidator 和 EmailValidator 对象)公用一个令牌(这里是 NG_VALIDATORS), multi 属性作用可以查看源码中说明 。当 NgModel 实例化时,其构造依赖于 @Self() NG_VALIDATORS , @Self() 表示从 NgModel 指令挂载的宿主元素中去查找这个令牌拥有的服务, NgModel 没有提供 NG_VALIDATORS ,但是挂载在 input 宿主元素上的 REQUIRED_VALIDATOR 和 EMAIL_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 源码解析之双向绑定 。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- ReactNative源码解析-初识源码
- Spring源码系列:BeanDefinition源码解析
- Spring源码分析:AOP源码解析(下篇)
- Spring源码分析:AOP源码解析(上篇)
- 注册中心 Eureka 源码解析 —— EndPoint 与 解析器
- 新一代Json解析库Moshi源码解析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。