我们会在本文中探索 Angular 2 内建的自定义验证。
# 介绍
Angular 2 原生就支持一些有用的验证器:
- required: 验证字段必须存在 
- minlength: 验证字段值的最小长度有效 
- maxlength: 验证字段值的最大长度有效 
- pattern: 验证输入的值是否匹配给定的模板,比如 email 
我们会基于下面的接口创建一个表单来获取用户信息。
// user.interface.ts
export interface User {
    username: string; // required, must be 5-8 characters
    email: string; // required, must be valid email format
    password: string; // required, value must be equal to confirm password.
    confirmPassword: string; // required, value must be equal to password.
}需求
仅在字段数据不正确或提交表单的时候,为每个字段显示错误消息。
UI 展示:

# App 配置
这是我们的文件结构:
|- app/ |- app.component.html |- app.component.ts |- app.module.ts |- equal-validator.directive.ts |- main.ts |- user.interface.ts |- index.html |- styles.css |- tsconfig.json
为了使用新的表单模块,我们需要用 npm install @ angular/forms 指令调用 npm 包,并在应用程序模块中导入最新的表单模块。
$ npm install @angular/forms --save
下面是我们应用程序的 app.module.ts 模块:
// app.module.ts
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent }   from './app.component';
@NgModule({
  imports:      [ BrowserModule, FormsModule ], // import forms module here
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ],
})
export class AppModule { }# App 组件
让我们继续创建 App 组件。
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { User } from './user.interface';
@Component({
  moduleId: module.id,
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.css']
})
export class AppComponent implements OnInit {
    public user: User;
    ngOnInit() {
        // initialize model here
        this.user = {
            username: '',
            email: '',
            password: '',
            confirmPassword: ''
        }
    }
    save(model: User, isValid: boolean) {
        // 调用API保存customer
        console.log(model, isValid);
    }
}# HTML 视图
这是我们的 HTML 视图的样子。
<!-- app.component.html -->
<div>
    <h1>Add user</h1>
    <form #f="ngForm" novalidate (ngSubmit)="save(f.value, f.valid)">
        <!-- 我们将把验证的字段放在这里 -->
        <button type="submit" [disabled]="!myForm.valid">Submit</button>
    </form>
</div># 实现
现在来一个个添加控件。
用户名
需求:必填,长度在 5-8 个字符之间
<!-- app.component.html -->
...
<div>
    <label>Username</label>
    <input type="text" name="username" [ngModel]="user.username" 
        required minlength="5" maxlength="8" #username="ngModel">
    <small [hidden]="username.valid || (username.pristine && !f.submitted)">
        Username is required (minimum 5 characters).
    </small>
</div>
<pre *ngIf="username.errors">{{ username.errors | json }}</pre>
...required、minlength、maxlength 都是内置的验证器,所以很容易使用。
我们只会在用户名无效、获得焦点和提交表单的情况下显示错误消息。最后一条的 pre 标签在开发过程中对调试很有用。它会显示字段的所有验证错误。
电子邮件地址
需求:必填,必须是有效的电子邮件地址格式
<!-- app.component.html -->
...
<div>
    <label>Email</label>
    <input type="email" name="email" [ngModel]="user.email" 
        required pattern="^[a-zA-Z0–9_.+-]+@[a-zA-Z0–9-]+.[a-zA-Z0–9-.]+$" #email="ngModel" >
    <small [hidden]="email.valid || (email.pristine && !f.submitted)">
        Email is required and format should be <i>john@doe.com</i>.
    </small>
</div>
...我们把 email 设置为必填,然后使用内建的模板验证器,通过正则表达式来检查 email 的值:^[a-zA-Z0–9_.+-]+@[a-zA-Z0–9-]+.[a-zA-Z0–9-.]+$.
密码和确认密码
需求:
- 密码:必填,值必须与确认密码的值相同。 
- 确认密码:必填,值必须与密码的值相同。 
<!-- app.component.html -->
...
<div>
    <label>Password</label>
    <input type="password" name="password" [ngModel]="user.password" 
        required #password="ngModel">
    <small [hidden]="password.valid || (password.pristine && !f.submitted)">
        Password is required
    </small>
</div>
<div>
    <label>Retype password</label>
    <input type="password" name="confirmPassword" [ngModel]="user.confirmPassword" 
        required validateEqual="password" #confirmPassword="ngModel">
    <small [hidden]="confirmPassword.valid ||  (confirmPassword.pristine && !f.submitted)">
        Password mismatch
    </small>
</div>
...validateEqual 是我们自定义的验证器。它会将当前输入的值与输入的密码值进行对比验证。
# 自定义确认密码验证器
我们将制定一个 validate equal 指令。
// equal-validator.directive.ts
import { Directive, forwardRef, Attribute } from '@angular/core';
import { Validator, AbstractControl, NG_VALIDATORS } from '@angular/forms';
@Directive({
    selector: '[validateEqual][formControlName],[validateEqual][formControl],[validateEqual][ngModel]',
    providers: [
        { provide: NG_VALIDATORS, useExisting: forwardRef(() => EqualValidator), multi: true }
    ]
})
export class EqualValidator implements Validator {
    constructor( @Attribute('validateEqual') public validateEqual: string) {}
    validate(c: AbstractControl): { [key: string]: any } {
        // self value (e.g. retype password)
        let v = c.value;
        // control value (e.g. password)
        let e = c.root.get(this.validateEqual);
        // value not equal
        if (e && v !== e.value) return {
            validateEqual: false
        }
        return null;
    }
}代码很长,让我们把它拆开一部分一部分地看。
申明指令
// equal-validator.directive.ts
@Directive({
    selector: '[validateEqual][formControlName],[validateEqual] 
    [formControl],[validateEqual][ngModel]',
    providers: [
        { provide: NG_VALIDATORS, useExisting: forwardRef(() => EqualValidator), multi: true }
    ]
})首先,我们使用 @Directive 注解定义指令。然后我们指定 selector。selector 是必须的。我们会扩展内建验证器集合 NG_VALIDATORS 来将我们的等值验证器用于 providers.
定义类
// equal-validator.directive.ts
export class EqualValidator implements Validator {
    constructor( @Attribute('validateEqual') public validateEqual: string) {}
    validate(c: AbstractControl): { [key: string]: any } {}
}我们的指令类必须实现 Validator 接口。Validator 接口需要 avalidate 函数。在构建函数中,我们通过 @Attribute('validateEqual') 注解注入属性值,并将其赋值给 validateEqual 变量。在我们的示例中, validateEqual 的值是 "password"。
实现验证
// equal-validator.directive.ts
validate(c: AbstractControl): { [key: string]: any } {
    // 自己的值 (如 retype password)
    let v = c.value;
    // 控件的值 (如 password)
    let e = c.root.get(this.validateEqual);
    // 值不等旱
    if (e && v !== e.value) return {
        validateEqual: false
    }
    return null;
}首先我们从输入控件读入值,赋给 v。然后我们在表单中找到 password 控件的值赋值给 e。之后检查值是否相等,如果不等就返回错。
# 在应用模块中导入自定义验证器
要使用自定义验证器,需要先将其导入到应用程序模块中。
// app.module.ts
...
import { EqualValidator } from './equal-validator.directive';  // 导入验证器
import { AppComponent }   from './app.component';
@NgModule({
  imports:      [ BrowserModule, FormsModule ],
  declarations: [ AppComponent, EqualValidator ], // 导入到应用模块
  bootstrap:    [ AppComponent ],
})
...好了!假如你在 password 字段中输入 "123",在 retype password 字段中输入"xyz",就会显示一个密码不匹配的错误。
# 看起来挺好,但是……
现在一切都挺好,直到你在 retype passowrd 中输入文本之后又修改了 password 字段的值。
比如,你在 password 字段中输入 "123",在 retype password 字段中也输入 "123",然后将 password 字段的值改为 "1234"。验证仍然通过。为什么?
因为我们只把等值验证器应用到 retype password 字段。只有当 retype password 的值发生变化时才会触发验证。
解决办法
有几种方法可以解决这个问题。我们这里只讨论其中一种,你自己可以去找到其它办法。我们会再次使用 validateEqual 验证器并添加一个 reverse 属性。
<!-- app.component.html -->
...
<input type="password" class="form-control" name="password" 
    [ngModel]="user.password" 
    required validateEqual="confirmPassword" reverse="true">
<input type="password" class="form-control" name="confirmPassword"  
    [ngModel]="user.confirmPassword" 
    required validateEqual="password">
...- reverse 是 false 或者没有设置的情况下,我们会像前一节提到的那样执行等值验证器。 
- reverse 是 true 的时候,我们仍然会执行等值验证器,但它不会为当前控件添加错误消息,而是为指定会把的目标控件添加错误消息。 
在我们的例子中,我们设置 password 验证的 reverse 为 true。只要 password 与 retype password 的 值不等,我们会为确证密码字段添加一个错误消息,而不是重置 password 字段。
完整的自定义验证器代码如下:
// equal-validator.directive.ts
import { Directive, forwardRef, Attribute } from '@angular/core';
import { Validator, AbstractControl, NG_VALIDATORS } from '@angular/forms';
@Directive({
    selector: '[validateEqual][formControlName],[validateEqual][formControl],[validateEqual][ngModel]',
    providers: [
        { provide: NG_VALIDATORS, useExisting: forwardRef(() => EqualValidator), multi: true }
    ]
})
export class EqualValidator implements Validator {
    constructor(@Attribute('validateEqual') public validateEqual: string,
    @Attribute('reverse') public reverse: string) {
    }
    private get isReverse() {
        if (!this.reverse) return false;
        return this.reverse === 'true' ? true: false;
    }
    validate(c: AbstractControl): { [key: string]: any } {
        // self value
        let v = c.value;
        // control vlaue
        let e = c.root.get(this.validateEqual);
        // value not equal
        if (e && v !== e.value && !this.isReverse) {
            return {
                validateEqual: false
            }
        }
        // value equal and reverse
        if (e && v === e.value && this.isReverse) {
            delete e.errors['validateEqual'];
            if (!Object.keys(e.errors).length) e.setErrors(null);
        }
        // value not equal and reverse
        if (e && v !== e.value && this.isReverse) {
            e.setErrors({ validateEqual: false });
        }
        return null;
    }
}# 总结
当然,还有其他方法也能解决密码和确认密码验证问题。有些人建议在组(stack overflow)中添加密码和确认密码的机制,然后验证它。
方法没有绝对的好与坏,适合自己的才是最好的。
更多细节请参考:
- 有关 @Attribute 的更多信息,请参阅 angular docs。 
- 有关 Angular 2 指令的更多信息,请参阅此文档。 
- 我们可以参考 Angular 2 验证器源码来了解它们是如何构建内置验证器的。 
就这些。编码快乐!








