Using FormBuilder to Validate Reactive Forms in AngularJs2.

使用 FormBuilder 来更灵活的制作响应式表单验证

Posted by Zhenzhen Lin on 2017-03-16

前言

想弱弱的问一句, 从事技术领域, 你有没有一两项技术点是不想涉及的? 我有, 超不喜欢做表单验证 ~ 刚开始打代码的时候, 做个表单验证, 我要写好多重复的 CSS 类, 好多重复的 JS 判断, 各种 IF AND ELSE. 不过随着技术慢慢成长, 而且现在有这么多的流行框架和组件, 也是可以欣慰一下的. 但是还是不想做表单验证, 这次做表单验证又研究了三天左右才有了起色, 因为现在想的不仅仅是完成了该功能, 更多在意的是逻辑强, 易维护, 语义化等.

回归正题, Angular2 的表单验证非常强大, 在这里我简单说一下, Angular2 的表单验证大致分为两类型:

  • 模板驱动表单方式
  • 响应式表单方式

这篇文章主要记录第二种方式 响应式表单方式, 不过还是做个简单的对比, 先让大家了解一下.

简单认识 - 模板驱动表单方式

特点: 属性验证, 指令验证, 信息提示在页面表单内, 直接看段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<label for="name">Name</label>
<input type="text" id="name" class="form-control"
required minlength="4" maxlength="24"
name="name" [(ngModel)]="person.name"
#name="ngModel" >
<div *ngIf="name.errors && (name.dirty || name.touched)" class="alert alert-danger">
<div [hidden]="!name.errors.required">
昵称必填
</div>
<div [hidden]="!name.errors.minlength">
昵称不能少于 4 位字符
</div>
<div [hidden]="!name.errors.maxlength">
昵称不能大于 24 位字符
</div>
</div>

简单认识 - 响应式表单方式

特点: 元素验证, 自定义指令验证, 信息配置在组件控制器中, 直接看段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
buildForm(): void {
// 响应式验证绑定
this.myForm = this.fb.group({
'name': [this.person.name, [
Validators.required,
Validators.minLength(4),
Validators.maxLength(24)
]
],
'age': [this.person.age],
'mobile': [this.person.mobile, Validators.required]
});
// 监控 myForm 表单值的改变
this.myForm.valueChanges
.subscribe(data => this.onValueChanged(data));
// 调用验证
this.onValueChanged(); // (re)set validation messages now
}

以上就是两者的区别, 经实践并调研, 认为第二种方式的验证更为灵活一些, 可以增加自定义的指令, 正则表达式, 消息提示控制均可封装在一起, 好处多多, 你可以:

  • 随时添加、修改和删除验证函数
  • 在组件内动态操纵控制器模型
  • 使用孤立单元测试来测试验证和控制器逻辑

深入了解 - 响应式表单方式

在制作响应式表单验证之前, 先介绍一个 @angular/forms 的依赖 - FormBuilder.

FormBuilder 是一个名副其实的助手类, 帮助我们构建表单组 - ControlGroup, 可以认为它是一个“工厂”对象.

1.先引入以下三个依赖, 我们之后都会用到

1
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

2.声明 FormGroup, 实例化 FormBuilder, 绑定我们表单元素并放至 fb.group()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Person } from './person.class';
@Component({
selector: 'my-form',
templateUrl: 'my-form.component.html',
styleUrls: ['my-form.css']
})
export class myFormComponent {
// 1. 声明我的表单为 FormGroup
private myForm: FormGroup;
// 2. 声明 person 对象
private person = new Person();
// 3. 实例化 FormBuilder
constructor(private fb: FormBuilder) {}
// 4. 页面初始化时, 调用表单绑定
ngOnInit(): void {
this.buildForm();
}
// 5. 表单绑定
private buildForm(): void {
this.myForm = fb.group({
'name': [this.person.name, [Validators.required]],
'mobile': [this.person.mobile, [
Validators.required,
Validators.minLength(9)
]]
});
}
}

上述我们就在组件内, 简单绑定了 html 表单中的两个标签元素, name, mobile, 并为它们绑定了 Validators 提供的内置监测 required()minLength(), 即不能为空的和最少位数. 我们的响应式表单是如下这样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<form [formGroup]="myForm" (submit)="onSubmit(myForm.value)">
<div class="filed">
<label for="name">Name: </label>
<input type="text" required [formControl]="myForm.controls['name']" placeholder="昵称">
</div>
<div class="filed">
<label for="mobile">Mobile: </label>
<input type="text" required [formControl]="myForm.controls['mobile']" placeholder="联系方式">
</div>
<button type="submit" class="button">Submit</button>
</form>

[formGroup]="myForm" 为绑定我们的表单;
[formControl]="myForm.controls['name']" 绑定我们的需要验证的元素, 这里还有另外一种方式, 也可写成:

1
2
3
4
5
<!-- 方式一 -->
<input type="text" required [formControl]="myForm.controls['name']" placeholder="昵称">
<!-- 方式二 我选择这个, 之后讲解的代码中也是使用这种绑定方式 -->
<input type="text" required formControlName="name" placeholder="昵称">

3.下一步我们需要验证消息, 并提示在页面内, 先把我们页面显示信息的div写好:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<form [formGroup]="myForm" (submit)="onSubmit(myForm.value)">
<div class="filed">
<label for="name">Name: </label>
<input type="text" required formControlName="name" placeholder="昵称">
<div *ngIf="formErrors.name" class="alert alert-danger">
{{ formErrors.name }}
</div>
</div>
<div class="filed">
<label for="mobile">mobile: </label>
<input type="text" required formControlName="mobile" placeholder="联系方式">
<div *ngIf="formErrors.mobile" class="alert alert-danger">
{{ formErrors.mobile }}
</div>
</div>
<button type="submit" class="button">Submit</button>
</form>

上述代码我就在每个input元素之后, 添加了 formErrors.name 的绑定, formErrors 之后会在我们的组件内声明该变量, 用于储存我们的首要提示错误信息.

4.在组件中, 监测我们的表单变动, 并时时提示验证信息, 继续完善 step2 中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 5. 表单绑定
private buildForm(): void {
this.myForm = fb.group({
'name': [this.person.name, [Validators.required]],
'mobile': [this.person.mobile, [
Validators.required,
Validators.minLength(9)
]]
});
this.myForm.valueChanges.subscribe(data => _that.onValueChanged(data));
this.onValueChanged(); // (re)set validation messages now
}
// 6. 声明值改变的方法, 过滤要提示的错误信息
private onValueChanged(data?: any) {
let _that = this;
if (!_that.myForm) { return; }
const form = _that.myForm;
for (const field in _that.formErrors) {
// clear previous error message (if any)
_that.formErrors[field] = '';
const control = form.get(field);
if (control && control.dirty && !control.valid) {
const messages = _that.validationMessages[field];
for (const key in control.errors) {
_that.formErrors[field] = messages[key];
}
}
}
}
// 7. 最新错误信息记录
private formErrors = {
'name': '',
'mobile': ''
};
// 8. 元素的多个提示信息组
private validationMessages = {
'name': {
'required': '昵称是必填项'
},
'mobile': {
'required': '年龄是必填项',
'minlength': '最少为 9 为数字'
}
};

我们基本的响应式表单样式就这样结束了, html 表单的元素会根据, 订阅的 Form.valueChanges() 事件去填充 formErrors, formErrors 会在页面显示我们最新的错误信息, 页面样式就随大家喜欢了.

就这样就结束了吗?

Validators 提供了 required, minLengh, maxLenght 好像满足不了我们高大上的表单安全验证, 所以我们要扩展我们的自定义指令, 来与 Validators 一起一个元素多个验证.

在这里我创建一个非常实用的多功能正则表达式自定义指令, 如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { ValidatorFn, FormControl } from '@angular/forms';
export function PatternValidator(pattern: RegExp): ValidatorFn {
return (control: FormControl): { [s: string]: boolean } => {
// 1. pattern 为 RegExp 类型的参数, 在这里先做个简单的兼容
const reg = pattern instanceof RegExp ? pattern : new RegExp(pattern, 'i');
// 2. control.value 就是我们 input 当中的值
const res = !reg.test(control.value);
// 3. 不符合我们的正则要求, 就返回 true, 否则返回 null
return res ? {'pattern': true} : null;
}
}

上述需要注意的一点, 在 return 部分 {'pattern': true} 中的 pattern 这个key需要我们在组件内使用, 用来绑定我们的错误信息提示, 请往下看~

我们开使用该 PatternValidator 指令在我们的验证组中, 继续完善一小段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { PatternValidator } from "../directives/form/v-pattern.directive";
export class myFormComponent {
// 5. 表单绑定
private buildForm(): void {
this.myForm = fb.group({
'name': [this.person.name, [
Validators.required,
PatternValidator(/^[\S\s]{0,20}$/) // 我希望昵称是 1~20 位任意字符
]],
'mobile': [this.person.mobile, [
Validators.required,
PatternValidator(/^1[3|4|5|7|8]\d{9}$/) // 我希望联系方式是符合正规号码 9位, 并已 13, 14, 15, 17, 18开头
]]
});
this.myForm.valueChanges.subscribe(data => _that.onValueChanged(data));
this.onValueChanged(); // (re)set validation messages now
}
// 6. 省略
// 7. 省略
// 8. 元素的多个提示信息组
private validationMessages = {
'name': {
'required': '昵称是必填项',
'pattern': '1~20 位字符' // pattern 是我们指令抛出的key值
},
'mobile': {
'required': '年龄是必填项',
'pattern': '号码格式错误'
}
};
}

上述注意要引入我们的 PatternValidator 指令才能使用哦 ~

送你一记双向绑定, 肯定你需要

表单验证成功, 该保存或修改我们的数据了, 这么能快速拿到我们表单的全部数据呢, 这里使用双向绑定 ngModel 啦, 优化代码如下:

1
2
3
<!-- 此处省去了其他表单内容-->
<input type="text" required formControlName="name" [(ngModel)]="person.name" placeholder="昵称">
<input type="text" required formControlName="mobile" [(ngModel)]="person.mobile" placeholder="联系方式">

person 为我们在组件内声明的对象类:

1
2
// 2. 声明 person 对象
private person = new Person();

这个类看起来是这样子的:

1
2
3
4
5
6
export class Person {
public id: number;
public name: string;
public mobile: string;
public active: number = 0; // 初始一个默认值有好处哦
}

有了上述的 person 对象, 我们就可以随时拿到表单的数据了, 代码就行这样, 这就是我们建议一个模型类的好处:

1
2
// 使用 Person 的类模型的值
let data = this.person;

除了这个可以获取之外, 还有一种方式可以获取, 可还记得, 我们也绑定了表单:

1
2
// 1. 声明我的表单为 FormGroup
private myForm: FormGroup;
1
2
// 使用 myForm 的值
let data = this.myForm.value;

但是需要注意的是, myForm.value 是拿的你声明在 formControlName 或者 fb.group 的元素组, 如果有注意命名方式的习惯, 请注意一下. (在这里我的Person对象都是以下划线链接, 因为要用作请求的数据, 页面的绑定声明则使用驼峰法, 所以我直接获取的是person的对象模型值).

另外 myForm 的值也可以做为参数传入 onSubmit() 方法中, 代码如下:

1
<form [formGroup]="myForm" (submit)="onSubmit(myForm.value)"> </form>

Form 提交

我们想让表单元素都得到正确验证后, 才能让 submit 的 button 点亮, 所以我们再次优化一下我们的表单提交方式, 如下代码:

1
2
3
4
<form [formGroup]="myForm">
<!-- 此处省略其他表单元素-->
<button type="submit" [class.disabled]="!myForm.valid" (submit)="onSubmit()" class="button">Submit</button>
</form>

!myForm.valid 为监测我们的 Form 是否还有错误.

Wrapping Up

在这里我阐述了最轻量, 最灵活, 最整洁的一种验证方式的结合, 另外还有一些值, 也是可以使用的, 比如:

1.不声明对象模型, 只声明一个简单的变量并监测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private sth: any;
private buildForm(): void {
this.myForm = fb.group({
'sth': ['', [Validators.required]]
});
this.sth = this.myForm.controls['sth'];
this.sth.valueChanges.subscribe( (value: string) => {
console.log('sth changed to:', value);
}
);
}

2.不使用 FormErrors 绑定错误信息, 可以在页面随意绑定并显示

1
2
3
4
5
6
<form [formGroup]="myForm">
<input type="text" [formControl]="myForm.controls['sth']">
<div *ngIf="!myForm.controls['sku'].valid" class="error message">Sth is invalid</div>
<div *ngIf="myForm.controls['sku'].hasError('required')" class="error message">Sth is required</div>
</form>

更多资料, 可参考:

Angular2官网 - 表单验证
Github - Angular2中的表单 这个是对 Angular2 还未印刷的英文书籍做的翻译, 仅表单部分

可能出现的小坑

1.有没有在 @NgModule 中引入 FormsModule 呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
// 重要的一部分
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@NgModule({
imports: [
BrowserModule,
FormsModule, // 在这里 imports
ReactiveFormsModule
],
declarations: [
AppComponent
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}