Using FormBuilder to validate a child component.

使用 FormBuilder 来验证子组件的值, 如 Select

Posted by Zhenzhen Lin on 2017-06-18

前言

不管团队是否慢慢壮大, 组件开发是必不可少的, 尤其是项目多, 伙伴多, 需求量大且快速开发频繁的时候, 组件式的开发就显得格外的重要了. 对于前端来说, HTML、CSS、JS 都可以封装成组件, 为的是提高开发效率和降低成本付出等.

在基于 AngularJS-2 的基础上, 我所在的团队也开始了更方面的组件化, 虽然团队在 AngularJS-1 的基础也作了不少组件化的封装, 但过于松散, 这次觉得应该对组件进行一次升级改造了.

本节是在记录父级在使用 FormBuilder 验证表单的情况下, 描述怎样去验证一个时时变换子组件的值, 就是父级的表单验证如何与子组件进行通讯验证.

背景

本记录是基于了解 Angular FormBuilder 的情况所记录的, 如不太了解, 请先看 Using FormBuilder to Validate Reactive Forms in AngularJs2.

本篇的问题是基于一个页面写好的表单验证情况下, 把 银行下拉框 提取成为一个公共的子组件, 这只是一个为演示问题并解决问题, 用了一个简单的例子, 但更多情况是实现其他复杂的父组件验证子组件时的解决方案, 如下一步会提取 城市三级联下拉框等.

先来看段页面的表单验证:

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
<!-- HTML FORM 部分 -->
<form [formGroup]="myForm">
<div class="form-item">
<i>姓名: </i>
<input type="text" formControlName="accountName" required iFocus [(ngModel)]="pay.accountName"/>
<div *ngIf="formErrors.accountName" class="alert alert-danger">
{{ formErrors.accountName }}
</div>
</div>
<div class="form-item">
<i>银行卡号: </i>
<input type="text" formControlName="cardNo" required iFocus [(ngModel)]="pay.cardNo"/>
<div *ngIf="formErrors.cardNo" class="alert alert-danger">
{{ formErrors.cardNo }}
</div>
</div>
<div class="form-item">
<i>请选择银行: </i>
<select formControlName="bankCode" [(ngModel)]="pay.bankCode" (change)="onChangeBank()">
<option *ngFor="let bank of banks" value="{{ bank.id }}">{{ bank.value }}</option>
</select>
<div *ngIf="formErrors.bankCode" class="alert alert-danger">
{{ formErrors.bankCode }}
</div>
</div>
<div [class.disabled-btn]="!myForm.valid" class="btn btn-primary">
<a (click)="submit()">提交</a>
</div>
</form>
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/* 表单验证部分 */
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
import { SelectBankService } from "public/core/services/select-bank.service";
import { VALIDATE } from 'public/core/services/validate.service';
import { PatternValidator } from "public/core/directives/v-pattern.directive";
import { NotZeroValidator } from "public/core/directives/v-not-zero.directive";
import { Pay } from "pay.class";
@Component({
selector: 'payment-form',
templateUrl: 'payment-form.html',
styleUrls: [
'payment-form.css'
]
})
export class PaymentFormComponent implements OnInit {
constructor(private selectBankService:SelectBankService) {}
ngOnInit():void {
this.buildForm();
}
private myForm:FormGroup;
private pay:Pay;
private buildForm():void {
let _that = this;
let formObj:any = {
'accountName': [
_that.pay.accountName, [
Validators.required,
PatternValidator(VALIDATE.anyAtLeast20.reg)
]],
'cardNo': [
_that.pay.cardNo, [
Validators.required,
PatternValidator(VALIDATE.number.reg, 'number'),
PatternValidator(VALIDATE.between12and20.reg)
]],
'bankCode': [
_that.pay.bankCode, [
NotZeroValidator()
]]
};
_that.myForm = _that.fb.group(formObj);
_that.myForm.valueChanges.subscribe(data => _that.onValueChanged(data));
_that.onValueChanged(); // (re)set validation messages now
}
// 这个方法就不贴代码了, 在上述提到了另外一篇文章中有写, 复制即可.
onValueChanged(): void {}
private formErrors = {
'accountName': '',
'cardNo': '',
'bankCode': ''
};
private validationMessages = {
'accountName': {
'required': VALIDATE.required.msg, // '不能为空',
'pattern': VALIDATE.anyAtLeast20.msg // '至少20位字符'
},
'cardNo': {
'required': VALIDATE.required.msg, // '不能为空',
'number': VALIDATE.number.msg, // '只能是数字',
'pattern': VALIDATE.between12and20.msg // '至少在12-20位字符'
}
'bankCode': {
'notZero': VALIDATE.required.msg // '请选择银行'
}
};
// 获取银行信息
private banks: any = this.selectBankService.getBanks;
onChangeBank(): void {
let id = event.target.value;
// do sth when bank`s select tag is been changing.
}
}

上述代码分别是 HTML 表单部分和父组件基于 FormBuilder 的验证, 在验证银行预留姓名, 银行卡号及所在开户银行. 在这个基础上把银行下拉框提取出来成为一个子组件, 这样, 父组件只需像下面代码引入在表单中就好了, 无需获取数据并遍历数据, 以及做数据 (change) 的监控.

我们要把下拉框变成这样的:

1
<select-bank></select-bank>

变成一个子组件非常容易, 再写一个组件声明就好了, 请直接看代码:

1
2
3
<select class="form-bank">
<option *ngFor="let bank of banks" value="{{ bank.id }}">{{ bank.value }}</option>
</select>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Component } from '@angular/core';
import { SelectBankService } from "public/core/services/select-bank.service";
@Component({
selector: 'select-bank',
templateUrl: 'select-bank.component.html',
styleUrls: [
'select-bank.component.css'
]
})
export class SelectBankComponent {
constructor(private selectBankService:SelectBankService) {}
private banks:any = this.selectBankService.getBanks;
}

这样先完成了提取一个子组件, 但本章是讲, 如何把子组件在通讯断开的基础上, 让父组件还是像原来一样去验证这个子组件.

双向绑定父与子

首先父组件是这样调用子组件的:

1
2
3
4
5
6
7
<div class="form-item">
<i>请选择银行: </i>
<select-bank formControlName="bankCode" [(model)]="pay.bankCode" ngDefaultControl></select-bank>
<div *ngIf="formErrors.bankCode" class="alert alert-danger">
{{ formErrors.bankCode }}
</div>
</div>

先解释一下上述的代码的改变
1. formControlName="bankName" 还是原来绑定的名称
2. ngDefaultControl 这里必须与 formControlName 搭配使用, 是为让它把子组件当作是原生标签一样在验证, 如不然他会报如下类似的错误信息:

1
No value accessor for form control with unspecified name

我在 Stackoverflow 找到了这个issue

3. [(model)]="pay.bankCode" 这里的 model 为双向绑定, 为了与 [(ngModel)] 相对应, 我们采用命名为 model 的形式, 如果你正在写程序, 可以打开控制台, 看看表单变换的时候, 有个 ng-reflect-model 的名称, 命名为 model 只为了与这个名称相符合, 让遵循 ngModel 的形式, 并且对绑定的值也有好处.

但是以上3描述不同的是, [(ngModel)] 是 NG 事件, [(model)] 自身没有事件, 我们需要在子组件内为它写入一个双向绑定事件. 这个解决的办法, 我是在官网API-模版语法中受到启发的, 其中有段描述 双向绑定- NgModel 的内容:

我们不能把[(ngModel)]用到非表单类的原生元素或第三方自定义组件上,除非写一个合适的值访问器,这种技巧超出了本章的范围。
我们自己写的Angular组件不需要值访问器,因为我们可以让值和事件的属性名适应Angular基本的双向绑定语法,而不使用NgModel。

幸运的是, 官网还有个例子, 描述了 x 值与 xChange 事件的模式, 请翻阅 前面看过的sizer就是使用这种技巧的例子。

基于官网 x 与 xChange 模式, 完善一下子组件的代码

1
2
3
<select (change)="sendSelectedId(bank.value)" class="select-bank" #bank>
<option *ngFor="let bank of banks" value="{{ bank.id }}">{{ bank.value }}</option>
</select>
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
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { SelectBankService } from "public/core/services/select-bank.service";
@Component({
selector: 'select-bank',
templateUrl: 'select-bank.component.html',
styleUrls: [
'select-bank.component.css'
]
})
export class SelectBankComponent {
constructor(private selectBankService:SelectBankService) {}
private banks:any = this.selectBankService.getBanks;
@Input() model:number;
@Output() modelChange = new EventEmitter<number>();
sendSelectedId(id:number = 0) {
this.model = id;
this.modelChange.emit(this.model);
}
}

上述 @Input()@Output()xxChange 模式, 也用到了 组件通讯-使用输入绑定把数据从父组件传给子组件

当我们子组件 (change) 改变时候, 向父组件 sendSelectedId() 发送改变的值, 父组件在 [(model)] 的情况下就接收到了改变后的值, 而且会触发改变事件, 这时候 FB 就能从订阅中知道, 这个值的改变就会触发表单的验证. 其中父组件的订阅信息是这行:

1
_that.myForm.valueChanges.subscribe(data => _that.onValueChanged(data)); // buildForm()

完美的解决问题了~

总结

抽离子组件, 体现在了封装性, 下一步开发简单快速集成即可, 大大改善了我们的开发效率.

最后父亲节快乐, 给老爸发了6.18的红包. 完美的被老妈抢了一半儿, 哈哈 ~