Angular 深入浅出三十天:表单与测试 Day26 - 进阶表单开发技巧 - 自订验证器

Day26

之前在开发表单的时候,我们都是使用 Angular 所提供的验证器来验证表单栏位里的值是否符合我们的需求。

虽然 Angular 已经这麽贴心地提供了这麽多验证了,但每个国家、地区的人文风土民情都不同,还有太多太多需要我们自己自订规则才能符合需求的情况。

因此,今天我们就一起来看看要怎麽自订验证器吧!

自订验证器的型别

既然要自订验证器,就不能不知道验证器的型别与其定义。

其实之前在第三天的文章:Reactive Forms 实作 - 以登入为例 里就有提到验证器的型别。

ValidatorFn

验证器的型别是 ValidatorFn ,其原始码定义如下:

interface ValidatorFn {
  (control: AbstractControl): ValidationErrors | null
}

从定义中我们可以知道,验证器其实就只是一个函式,该函式会传入一个型别为 AbstractControl 的参数来让我们在函式中判定该栏位的值是否符合我们的需求。

如果验证结果符合需求,那就回传 null ,代表没有任何的错误;如果验证结果不符合需求那就回传一个型别为 ValidationErrors 的错误。

ValidationErrors 又是什麽呢?

ValidationErrors

ValidationErrors 之前最早是在第二天的文章: Template Driven Forms 实作 - 以登入为例 里登场。

其原始码定义如下:

type ValidationErrors = {
    [key: string]: any;
};

没错,你没看错,就是这麽简单!

从定义上看起来,基本上只要是个物件,就符合该型别的要求,而这也是因为满足客制的条件,让使用 Angular 的开发者有程度的规范但拥有尽可能大的弹性。

不过虽然大家可以随意自订,但我非常建议大家在自订的时候可以参考官方的验证器。

举例来说,官方的 Validators.required 验证器在验证有误时,会回传的 ValidationErrors 是:

{ required: true }

Validators.pattern 验证器在验证有误时,会回传的 ValidationErrors 是:

{ 
  actualValue: 'xxx',
  requiredPattern: 'xxx'
}

Validators.minlengthValidators.minlength 验证器在验证有误时,则会回传:

{
  actualLength: 1,
  requiredLength: 2
}

我们从中不难发现官方会在 ValidationErrors 中,回传该栏位当前的状态以及需求的状态;而物件的属性名称也会按照 actual 加上 XXXX 以及 required 加上 XXXX 的方式来命名。

虽然具体上还是要看实际需求,但我个人觉得我们自己在自订验证器的 ValidationErrors 时,也可以照着这个规则来处理。

一方面,整个系统会比较一致;另一方面,也不需要多写太多额外的程序来处理我们自订的错误。

举个例子,如果我们想自订一个栏位的值只能是 Leo 的验证器,其程序码可能会像是这样:

export leoValidator: ValidatorFn = (control: AbstractControl) => {
  const isLeo = control.value === 'Leo';
  if (isLeo) {
    return null
  }
  return { 
    actualValue: control.value, 
    requiredValue: 'Leo'
  };
};

如此我们就在需要用到它时,直接像这样使用即可:

new FormControl('', leoValidator);

又或者是弹性一点,让使用它的人来决定该栏位的值只能是什麽,其程序码应该会像是这样:

export function nameValidator(name: string): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    if (control.value === name) {
      return null
    }
    return { 
      actualValue: control.value, 
      requiredValue: name
    };
  };
}

然後就可以像这样使用:

new FormControl('', nameValidator('Leo'));

不过, ValidatorFn 是用同步的方式来执行验证,万一遇到需要非同步验证的情况要怎麽办?

非同步验证器

大家应该都满喜欢玩游戏的吧?!

谜之音:不要自己爱玩就认为别人都爱玩

绝大多数的游戏,尤其是线上游戏,在取名时不能够取跟别人相同的名字,而当取到跟别人一样的名字时,系统会提示「此名称已被使用」之类的错误讯息。

面对这种应用场景,相信大部分的朋友可能会是使用 valueChanges 来订阅栏位的变化事件,当使用者输入名称时,会呼叫 API 让後端来帮忙验证该名字是否已被使用,然後再根据回传结果来决定是否显示错误讯息。

毕竟前端不可能事先取得几千、几万甚至是几十万、几百万的名字再一一比对吧?

不过如果有这样的应用场景,我个人觉得还满适合使用非同步验证器来处理的。

顺带一提,这只是我个人举例,不代表真实应用情况。

AsyncValidator

interface AsyncValidator extends Validator {
  validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null>

  // inherited from forms/Validator
  validate(control: AbstractControl): ValidationErrors | null
  registerOnValidatorChange(fn: () => void)?: void
}

从上述定义可以看出,跟同步的验证器所不一样的是,我们在自订非同步的验证器时,不是直接制作一个符合 AsyncValidatorFn 定义的函式

而是要用一个可被注入的 Class 来实作 AsyncValidator 这个介面。

就像下面这个官网的范例一样:

@Injectable({ providedIn: 'root' })
export class UniqueAlterEgoValidator implements AsyncValidator {
  constructor(private heroesService: HeroesService) {}

  validate(
    ctrl: AbstractControl
  ): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
    return this.heroesService.isAlterEgoTaken(ctrl.value).pipe(
      map(isTaken => (isTaken ? { uniqueAlterEgo: true } : null)),
      catchError(() => of(null))
    );
  }
}

使用方式

为我们栏位设定非同步验证器的方式也非常地简单。

FormControl 来说, 我们可以用这样子的方式来设定:

new FormControl('', [/* 一般验证器 */], [/* 非同步验证器 */]);

简单来说,不论是用 Reactive Forms 的哪种方式建立栏位,非同步验证器都是放在一般验证器後面就对了!

本日小结

希望透过今天的分享,能让大家可以初步掌握自订验证器的技巧。虽然在实务上,大家不一定遇的到需要使用非同步验证器的场景,但如果真的有需要用到又忘记怎麽做时,至少有这篇文章在,随时都可以回来查询。

此外,虽然没有分享自订 Template Driven Forms 验证器的方式,但大家可以自行参考官方的 Form Validation - Adding custom validators to template-driven forms 文件。

而在非同步验证器的部份,官网也有提到一些优化非同步验证器的技巧,大家可以参考官方的 Form Validation - Optimizing performance of async validators 文件。

以上,就是今天的文章,如果有任何的问题或是回馈,还请麻烦留言给我让我知道,感激不尽!


<<:  Day-24 DOM Node

>>:  Day26 Cookie 的使用-1

[Day 09] 从 tensorflow.keras 开始的 VGG Net 生活 (第二季)

2. VGG 实作(tensorflow) 2.1 南无观世"import"啥?...

[Day 8] 2D世界中的数学 (一)

今日目标 基本的数学函式库(向量与阵列) 要多少才够 从另一个角度看,我认为游戏中的从小小的让角色移...

DAY7 Ngrok运行原理&安装Ngrok

Ngrok运行原理 其实ngrok有客户端ngrok和服务端ngrokd,在用户客户端发起请求时,就...

【C++】Data Type Size Of

这次我们要来学习资料型态在程序中的大小,亦即调查其所占的空间。 我列出一些常用的data type~...

建JS环境 Node Nodemon

我们飞快的结束惹html ,css,欢迎进入到下一个阶段JavaScrip。JS的助教很严,学习之前...