[Angular] Day27. Validating form input

在前几篇中介绍了 Template-driven forms 和 reactive forms 的用法与概念,虽然建立的方式不同但在根本上都是可以建立一个表单的,而两种表单中都会需要验证使用者输入的资料的准确性与完整性来保持整体数据的质量,本章中将会介绍如何验证使用者输入的内容。

https://ithelp.ithome.com.tw/upload/images/20210826/20124767oGJRbnjfIU.jpg


Validating input in template-driven forms

首先先介绍 template-driven forms 中的 validator,要在 template-driven forms 中添加验证可以添加与原生 HTML 表单验证相同的验证 attribute,比如说可以在 <input> 中添加 required attribute 这样这个输入框就不能为空,又或是可以填入 max 代表填入的数字不能大於指定的数字等等,Angular 会使用 directive 将这些 attribute 和框架中的 validator functions 做匹配。

每次表单控制元件的值发生变化时,Angualr 都会运行验证并生成导致 INVALID 状态的错误列表或是生成 VALID 状态的 null

<div class="content">
  <input
    type="text"
    id="name"
    name="name"
    class="form-control"
    required
    minlength="4"
    appForbiddenName="bob"
    [(ngModel)]="hero.name"
    #name="ngModel"
  />

  <div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert">
    <div *ngIf="name.errors?.required" class="alert alert-danger">Name is required.</div>
    <div *ngIf="name.errors?.minlength" class="alert alert-danger">
      Name must be at least 4 characters long.
    </div>
    <div *ngIf="name.errors?.forbiddenName" class="alert alert-danger">Name cannot be Bob.</div>
  </div>
</div>

在一个 <input> 中加入 requiredminlengthappForbiddenName,代表这个输入框不能为空不能小於 4 个字且不能是 bob,可以利用 name ( template variable ) 的 errors 来判断是哪一个验证出错进而显示不同的提示,而使用 *ngIf 判断当状态为 INVALID表单已经被触碰过更改过时,才会显示提示画面。

  • 如果输入框为空

    https://ithelp.ithome.com.tw/upload/images/20210826/20124767admY7u506b.png

  • 如果输入的内容字数小於4个

    https://ithelp.ithome.com.tw/upload/images/20210826/20124767oNm1D5Kayu.png

    至於名字不能是 bob 由於这个是客制化验证的部分,留到後面再补上。


Validating input in reactive forms

在 Reactive forms 中的验证是通过直接在 component 中的 FormControl 添加验证,每当 FormControl 发生变化 Angular 就会调用这些函数,而 reactive forms 的验证器可以是同步也可以是非同步的

  • Sync validators:此用 FormControl 的实例并立即返回一组验证错误或是验证成功(null) 的同步函数,可以在实例化 FormControl 的时候将验证函数作为第二个参数传入。
  • Async validators:非同步验证函数接收一个 FormControl 并返回一个 promiseObservable,随後会发出一组验证错误或是验证成功(null),可以在实例化 FormControl 的时候将验证函数作为第三个参数传入。

设置同步与非同步验证器时需要注意,Angular 因为出於性能的问题,如果所有 Sync validators 都通过,Angular 就只会运行 Async validators。

Built-in validator functions

要设置 reactive forms 的验证器可以和 template-driven forms 在 template 中添加的验证器一样,比如使用 required 制定这个表单不能为空等等,以下列一个 validators 的表单

Name Description
min 输入的内容要大於等於 validator 设定的值
max 输入的内容要小於等於 validator 设定的值
required 输入的值不能为空
requiredTrue 输入的值必须为 true,常用在复选框中的必选栏位
email 输入的值需要通过电子邮件验证测试,常用正则表达式验证
minLength 要求输入的长度需要大於等於 validator 设置的值,他仅用於举有数字长度 property 的类型(字串或阵列)
maxLength 要求输入的长度需要小於等於 validator 设置的值,规则与 minLength 一样
pattern 输入的值需要匹配正则表达式的设定
nullValidator 不执行任何操作的 validator
compose 将多个 validator 合成一个函数,该函数返回提供 FormControl 的各个错误映射的联合
composeAsync 将多个 Async validator 合成一个函数,该函数返回提供 FormControl 的各个错误映射的联合
  1. 在 reactive.component.ts 中建立 FormControl

    import { Component, OnInit } from '@angular/core';
    import { FormControl, FormGroup, Validators } from '@angular/forms';
    
    @Component({
      selector: 'app-reactive',
      templateUrl: './reactive.component.html',
      styleUrls: ['./reactive.component.css'],
    })
    export class ReactiveComponent implements OnInit {
      hero = new FormGroup({                                                 // (1)
        name: new FormControl('', [Validators.required, Validators.minLength(4)]),
      });
      constructor() {}
    
      ngOnInit(): void {}
    
      errorDetect() {
        return this.name?.invalid && (this.name.touched || this.name.dirty); // (2)
      }
    
      getErrorType (): string {                                              // (3)
        if (this.name?.errors !== null) {
          if (this.name?.errors.required) {
            return 'required';
          } else if (this.name?.errors.minlength) {
            return 'minlength';
          }
        }
        return '';
      }
    
      get name() {                                                           // (4)
        return this.hero.get('name');
      }
    }
    
    • (1): 使用 FormGroup 和 FormControl 建立表单控制元件
    • (2): 新增一个 method 用於获得 name 的状态(是否是无效的内容、使否被处碰与是否被更改)
    • (3): 新增一个 method 用於获得 name 的错误类型
    • (4): 新增一个 get property 用於获得 name 的 FormControl 实例
  2. 在 reactive.component.html 中绑定 FormControl

    <form [formGroup]="hero">
      <div>
        <label for="name">Hero Name</label>
        <input type="text" id="name" class="form-control" formControlName="name" />
        <div *ngIf="errorDetect()" class="alert alert-danger">
          <ng-container [ngSwitch]="getErrorType()">
            <div *ngSwitchCase="'required'">Name is required.</div>
            <div *ngSwitchCase="'minlength'">
              Name must be at least 4 characters long.
            </div>
            <div *ngSwitchDefault></div>
          </ng-container>
        </div>
      </div>
    </form>
    

    在 tempalte 中绑定 component 中的 FormControl,这边我的写法会跟 Angular 官方文档的写法不一样,因为个人喜欢把显示的逻辑放在 component 中,所以无论是判断 name 是否 INVALID 或判断 name error 的型态都是绑定 component 中的 method。

https://ithelp.ithome.com.tw/upload/images/20210826/20124767ltGgbqbE4G.png

https://ithelp.ithome.com.tw/upload/images/20210826/20124767NDiGEUEh4U.png


Defining custom validators

除了上面介绍的预设 Validator function 之外还可以客制化自己的 Validator 用於验证各种表单内容,template-driven forms 和 reactive forms 都可以客制化自己的 Validator 但他们的方式不一样,首先先介绍 template-driven forms 的方式。

template-driven forms custom validators

在上面的例子中有使用一个 appForbiddenName 名称不能为 bob 的 validator,但上面目前不会对 bob 这个名字进行验证,是因为还没建立属於他的 validator,那麽就来创建这个客制化的验证器吧

  1. 使用 Angular CLI 建立一个 directive

    ng generate directive forbidden-name
    
  2. 在 forbidden-name.directive.ts 中加入 validator function

    import { Directive, Input } from '@angular/core';                     // (1)
    import {
      AbstractControl,
      NG_VALIDATORS,
      ValidationErrors,
      Validator,
      ValidatorFn,
    } from '@angular/forms';                                              // (2)
    
    @Directive({                                                          // (3)
      selector: '[appForbiddenName]',
      providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenNameDirective, nulti: true}]
    })
    export class ForbiddenNameDirective implements Validator {            // (4)
      @Input('appForbiddenName') forbiddenName = '';                      // (5)
      constructor() {}
    
      forbiddenNameValidator(nameRe: RegExp): ValidatorFn {               // (6)
        return (control: AbstractControl): ValidationErrors | null => {
          const forbidden = nameRe.test(control.value);
          return forbidden ? { forbiddenName: { value: control.value } } : null;
        };
      }
    
      validate(control: AbstractControl): ValidationErrors | null {       // (7)
        return this.forbiddenName
          ? this.forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(
              control
            )
          : null;
      }
    }
    
    • (1): 从 @angular/core 中引入 Input
    • (2): 从 @angular/forms 中引入
      • AbstractControl 用於为传入 validate function 的 input 定义型态
      • NG_VALIDATORS 用於注册与 AbstractControl 一起使用的 sync validators InjectionToken
      • ValidationErrors 用於定义失败的验证检查返回错误的映射
      • Validator 是执行 sync validator 的 class 的 Interface
      • ValidatorFn 用於接收 FormControl 并 sync 的返回验证错误的映射或返回 null
    • (3): NG_VALIDATORS 使用 useExistingForbiddenNameDirective 为模板创建一个实例
    • (4): 使用 implements 来实践抽象的 interface ( Validator )
    • (5): 使用 @Input() 装饰 forbiddenName property 为输入,并取别名为 appForbiddenName
    • (6): 新增一个 method 用於接收字串後判断字串是否符合验证规定,返回验证错误的映照或 null
    • (7): 新增一个一个 method 用於验证输入的字串是否符合验证规定

https://ithelp.ithome.com.tw/upload/images/20210826/20124767lyWNYBJbAf.png

reactive forms custom validators

介绍完 template-driven forms 的客制化验证器後,接着介绍 reactive forms 的客制化验证,他不像 template-driven forms 一样需要建立一个 directive,只需要在 component 中的 FormControl 加入客制化验证函数即可。

  1. 先在 reactive.component.ts 中新增一个客制化验证函数

    forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
      return (control: AbstractControl): ValidationErrors | null => {
        const forbidden = nameRe.test(control.value);
        return forbidden ? { forbiddenName: { value: control.value } } : null;
      };
    }
    
  2. 在 reactive.component.ts 中 FormGroup 的 name (FormControl) 的 Validator 中添加客制化验证函数

hero = new FormGroup({
  name: new FormControl('', [
    Validators.required,
    Validators.minLength(4),
    this.forbiddenNameValidator(/hank/i),
  ]),
  alterEgo: new FormControl(''),
  power: new FormControl('', Validators.required),
});

https://ithelp.ithome.com.tw/upload/images/20210826/20124767wlh2Pe5yek.png


结论

本章介绍了如何在 template-driven forms 和 reactive forms 中添加预设的 validators function,也介绍了如何建立客制化的 validators function 然後加入到两种不同的 forms 中,表单验证在开发表单时非常重要,可以确保资料的确定性。

明天将会介绍 Form 的另一种使用方法,虽然再开发大型表单时可以使用 FormGroup 将类似的 FormControl 分组管理,但真的很大型的表单时还是会显得非常杂乱难以维护,这时就需要使用 Control Value Accessor 这个功能,可以把它想像成 component 中的父子层,在上层的 Component 中定义大概的 Form Model,将细节放在子层的 component 中,这样将整个表单内容以 Component 分割,可以更好的维护与测试,详细该怎麽使用就明天再详细讲解吧,那麽明天见


Reference


<<:  DAY 12 SASS 间的相似之处

>>:  【Day27】React Redux 原理及应用方法简介 ╰(°ㅂ°)╯

Day 24 : Jenkins 在Build完通知与好用套件

介绍Jenkins的章节即将进入尾声了。事实上你可能会想Jenkins默认介面这麽老气,怎麽就成为全...

大人也舍不得离开的公园 — 共融游乐场 Inclusive Playground

生活中有大大小小的设计,也许就在你我的日常中却未曾发现,其中一项替城市街景增添设计风采的设施就是公园...

Day 10 Dart语言-混合及泛型

混合mixins 介绍:mixin是一种可以把自己的方法提供给别的类别使用,却不需要成为其他类别的父...

Day17_控制项(A9存取控制)

=___="再次证明,每天都要发文,原来也不容易。来到了第17天了~要撑住 ▉A.9存取控...

30天打造品牌特色电商网站 Day.10 CSS框架-Bootstrap5

除了昨天提到的media query可以做出响应式网页之外,今天介绍的Bootstrap 5.1版本...