摘要:校验器运行完成后,会设置属性,从而计算的属性,假设校验错误,则属性值为。这样就理解了校验器的整个运行过程,也包括为何校验错误时会自动添加描述控件状态的。
我们知道,@angular/forms 包主要用来解决表单问题的,而表单问题非常重要的一个功能就是表单校验功能。数据校验非常重要,不仅仅前端在发请求给后端前需要校验数据,后端对前端发来的数据也需要校验其有效性和逻辑性,尤其在存入数据库前还得校验数据的有效性。 @angular/forms 定义了一个 Validator 接口,并内置了 RequiredValidator、CheckboxRequiredValidator、EmailValidator、MinLengthValidator、MaxLengthValidator、PatternValidator 六个常用的校验指令,每一个 validator 都实现了 Validator 接口。这些校验指令的使用很简单,比如使用 EmailValidator 和 RequiredValidator 指令来校验输入的数据得是 email 且不能为空:
这样输入的如果不是 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: `Template-Driven Form
Reactive-Driven Form
Update Forbidden Text
` }) 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 源码解析之双向绑定。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/96539.html
摘要:由于的属性提供了令牌,并且该令牌指向的对象就是,所以构造函数中注入的令牌包含的对象数组只有一个。这样的构造函数里就会包含一个对象,然后把这个传给对象,最后注册回调,这样以后值更新时就会运行。整个包的设计也是按照这种数据流形式,并不复杂。 我们知道,Angular 的 @angular/forms 包提供了 NgModel 指令,来实现双向绑定,即把一个 JS 变量(假设为 name)与...
摘要:在模块里面引入要使用响应式表单,就要从包中导入,并把它添加到你的的数组中。导入验证器函数响应式表单包含了一组开箱即用的常用验证器函数。这些验证器属性可以和响应式表单提供的内置验证器组合使用。 1:在AppModule模块里面引入 ReactiveFormsModule 要使用响应式表单,就要从@angular/forms包中导入ReactiveFormsModule,并把它添加到你的N...
摘要:在模块里面引入要使用响应式表单,就要从包中导入,并把它添加到你的的数组中。导入验证器函数响应式表单包含了一组开箱即用的常用验证器函数。这些验证器属性可以和响应式表单提供的内置验证器组合使用。 1:在AppModule模块里面引入 ReactiveFormsModule 要使用响应式表单,就要从@angular/forms包中导入ReactiveFormsModule,并把它添加到你的N...
摘要:在模块里面引入要使用响应式表单,就要从包中导入,并把它添加到你的的数组中。导入验证器函数响应式表单包含了一组开箱即用的常用验证器函数。这些验证器属性可以和响应式表单提供的内置验证器组合使用。 1:在AppModule模块里面引入 ReactiveFormsModule 要使用响应式表单,就要从@angular/forms包中导入ReactiveFormsModule,并把它添加到你的N...
阅读 2422·2021-10-09 09:59
阅读 2177·2021-09-23 11:30
阅读 2590·2019-08-30 15:56
阅读 1145·2019-08-30 14:00
阅读 2938·2019-08-29 12:37
阅读 1251·2019-08-28 18:16
阅读 1654·2019-08-27 10:56
阅读 1020·2019-08-26 17:23