前言
不管团队是否慢慢壮大, 组件开发是必不可少的, 尤其是项目多, 伙伴多, 需求量大且快速开发频繁的时候, 组件
式的开发就显得格外的重要了. 对于前端来说, 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
| <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(); } onValueChanged(): void {} private formErrors = { 'accountName': '', 'cardNo': '', 'bankCode': '' }; private validationMessages = { 'accountName': { 'required': VALIDATE.required.msg, 'pattern': VALIDATE.anyAtLeast20.msg }, 'cardNo': { 'required': VALIDATE.required.msg, 'number': VALIDATE.number.msg, 'pattern': VALIDATE.between12and20.msg } 'bankCode': { 'notZero': VALIDATE.required.msg } }; private banks: any = this.selectBankService.getBanks; onChangeBank(): void { let id = event.target.value; } }
|
上述代码分别是 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()
为 x
与 xChange
模式, 也用到了 组件通讯-使用输入绑定把数据从父组件传给子组件
当我们子组件 (change)
改变时候, 向父组件 sendSelectedId()
发送改变的值, 父组件在 [(model)]
的情况下就接收到了改变后的值, 而且会触发改变事件, 这时候 FB
就能从订阅中知道, 这个值的改变就会触发表单的验证. 其中父组件的订阅信息是这行:
1
| _that.myForm.valueChanges.subscribe(data => _that.onValueChanged(data));
|
完美的解决问题了~
总结
抽离子组件, 体现在了封装性, 下一步开发简单快速集成即可, 大大改善了我们的开发效率.
最后父亲节快乐, 给老爸发了6.18
的红包. 完美的被老妈抢了一半儿, 哈哈 ~