我们会在本文中探索 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 验证器源码来了解它们是如何构建内置验证器的。
就这些。编码快乐!