Angular 2 自定义组件,具有银行账号验证功能






4.88/5 (6投票s)
以模型驱动和模板驱动的方式构建 Angular 2 属性指令和两个组件,以捕获和验证跨多个输入字段的银行账号。
引言
作为一名 Angular 1 / ES5 开发者,我一直关注着 Angular 2、ES6、TypeScript 的新闻,我最感兴趣的是如何构建一个可重用的表单组件,该组件可以封装应用程序中常见的某些行为和外观,并利用这些新技术使其能够很好地集成到表单中。例如,在新西兰,银行账号是 15 或 16 位数字,由银行/分行/主体/后缀号码组成,并且有其定义的验证规则。我该如何将这种自定义的验证行为封装到 Angular 2 的属性指令中,以及/或者如何编写一个捕获银行账号并进行验证的 Angular 2 组件?
使用代码
我们将通过三种方式来捕获和验证银行账号
- 编写一个属性指令并在一个普通的 input 元素中使用它
- 编写一个包含 4 个 input 元素的模型驱动表单组件
- 编写一个包含 4 个 input 元素 的模板驱动表单组件
我们的最终表单将如下所示,您可以在这个 plunker 上进行尝试。
作为先决条件,我们将在 bank-account.service.ts 中创建一个可注入的 AccountService
,它暴露一个 isValid
函数,其签名如下
public isValid(bank: string, branch: string, body: string, suffix: string): boolean { }
它还导出了 BankAccount
接口
export interface BankAccount { acc1: string, acc2: string, acc3: string, acc4: string }
我们假设这里唯一有效的账号是“0865231954512001”,所以如果给定的账号与上述账号完全相同,isValid
将返回 true,否则返回 false。将此服务分离出来是一种好习惯,因为它使我们的代码可测试且可重用。
我们的表单将捕获 4 个字段 - 姓名和 3 种不同方式输入的 3 个账号,我们的领域模型将如下所示
vmName: string = 'Bob Lee';
vmAccount1: string = '';
vmAccount2: BankAccount = { acc1: '08', acc2: '6523', acc3: '1954512', acc4: '001' };
vmAccount3: BankAccount = { acc1: '', acc2: '', acc3: '', acc4: '' };
第一种方式 - 属性指令
以下是我们 app.component.html 中表单模板的简化版本
<form #theForm="ngForm" name="theForm" novalidate (ngSubmit)="submit(theForm)">
<div>
<label>Name</label>
<input type="text" name="name" [(ngModel)]="vmName" />
</div>
<div>
<label>Account 1</label>
<input type="text" name="account1" [(ngModel)]="vmAccount1" />
</div>
<button type="submit">Submit</button>
</form>
这是一个模板驱动的表单,姓名和账号 1 字段通过“香蕉盒” ngModel 与我们的模型 vmName
和 vmAccount1
进行双向绑定,#theForm
在 form 标签的开头是模板引用变量,它引用了我们表单的 NgForm 实例,而 (ngSubmit) 是用于处理提交事件的事件绑定语法。
现在我们想为我们的账号 1 字段添加验证行为。根据 Angular 2 文档
引用属性指令会改变 DOM 元素的外观或行为。
在这种情况下,我们需要一些特定于银行账号的行为,例如只接受数字按键并验证输入的账号是否为银行账号。让我们在 bank-account-validator.directive.ts 文件中创建我们的属性指令 validateAccount
,并将其用于我们的账号 1 输入元素,如下所示
<input type="text" name="account1" ngModel validateAccount />
请注意,Angular 2 要求将 ngModel 指令与我们的自定义指令一起放置,以充当验证器。一个新的文件 bank-account-validator.directive.ts 如下所示
import { Directive } from '@angular/core';
@Directive({
selector: '[validateAccount][ngModel]',
})
export class AccountValidator {
}
为了限制按键只接受数字,我们需要处理 input 元素上的按键事件,Angular 2 为我们提供了 HostListener 装饰器,所以在一个 AccountValidator
类中,我们可以有一个 onKeypress 事件处理程序并这样装饰它
@HostListener('keypress', ['$event'])
onKeypress(event) {
ignoreSome(event);
}
账号 1 字段已准备好忽略非数字键,但仍允许左、右、退格键。
现在要添加验证行为,我们需要实现 Validator 接口。 Angular 2 源码 说
// An interface that can be implemented by classes that can act as validators.
export interface Validator { validate(c: AbstractControl): {[key: string]: any}; }
我们的 Validator 实现如下所示
export class AccountValidator {
validator: Function;
constructor(accountService: AccountService) {
this.validator = validateAccountFactory(accountService);
}
validate(c: FormControl) {
return this.validator(c);
}
}
function validateAccountFactory(accountService: AccountService) {
return (c: FormControl) => {
if (!c || !c.value) return null; // empty is valid
let invalid = { validateAccount: { valid: false } };
if (c.value.length === 15 || c.value.length === 16) {
let acc = c.value,
acc1 = acc.slice(0, 2),
acc2 = acc.slice(2, 6),
acc3 = acc.slice(6, 13),
acc4 = acc.slice(13);
if (accountService.isValid(acc1, acc2, acc3, acc4))
return null; // valid
}
return invalid;
};
}
如果您首先查看 validateAccountFactory
函数,它会返回一个函数,该函数接受 FormControl(来自我们的 Account1 输入字段)作为输入参数,然后使用 AccountService.isValid
函数验证 FormControl 的值。
AccountValidator
类在其构造函数中注入 AccountService
,并将来自 validateAccountFactory
的函数赋值给 validator 变量,然后 validate() 仅使用 validator 返回布尔结果。
要使此指令真正起作用的最后一步是通过在 providers 元数据中添加以下行来将我们的 Validator 实现挂钩到 NG_VALIDATORS 令牌。关于此的更详细解释可以在这篇文章中找到
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => AccountValidator), multi: true }
从现在开始,每次您在字段中输入任何数字时,表单都会立即知道输入的数字是否是有效的银行账号。因此,当用户提交表单时,代表表单的 NgForm 实例将知道表单无效,其中一个原因是银行账号无效。当提交处理程序记录 NgForm 对象时,如果您在开发者工具控制台中检查它,您会看到一些有趣的属性
- NgForm.submitted: true
- NgForm.invalid: true
- NgForm.value: { account1: “0865231954514001”, name: “Bob Lee” }
- NgForms.controls.account1.errors.validateAccount = { value: false }
- NgForm.valueChanges: EventEmitter
在模板中,我们可以使用模板引用变量 #theForm
来访问表单的 NgForm 实例,使用 #account1
来访问 Account1 输入字段的 NgModel 实例。因此,如果我们想在提交时显示任何验证错误,我们可以像这样使用它们
<div [hidden]="!theForm.submitted">
<div class="text-danger" [hidden]="account1.valid || !account1.errors.validateAccount">
Bank account number is not valid
</div>
</div>
请注意,bank-account-validator.directive.ts 还导出了 validateAccounGroupFactory
函数,它与 validateAccountFactory
类似,只是它接受 FormGroup 而不是 FormControl。此函数将在我们即将实现的组件中使用。
第二种方式 - 模型驱动组件
在上面,我们使用了一个 input 元素来捕获整个账号,并通过创建一个属性指令并将其添加到 input 元素来添加自定义验证行为。
如果我们想使用 4 个 input 元素来分别捕获银行/分行/主体/后缀号码怎么办?
那么创建一个自定义组件就很有意义了,该组件有自己的模板,并执行上面看到的相同验证。我们可以在表单中使用该组件,如下所示
<bank-account-model-driven name="account2" [(ngModel)]="vmAccount2">
</bank-account-model-driven>
该组件与我们的领域模型 vmAccount2
(BankAccount
类型)进行双向绑定。因此,当我们的页面加载时,vmAccount2
的值将通过属性绑定显示在视图中;当表单提交时,vmAccount2
应该通过事件绑定从视图中获取有效的账号。
{ acc1: '08', acc2: '6523', acc3: '1954512', acc4: '001' }
让我们在以下两个文件中创建 AccountModelDrivenComponent
- app/bank-account-model-driven.component.ts
- app/bank-account-model-driven.component.html
import { Component } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
@Component ({
selector: 'bank-account-model-driven',
templateUrl: 'app/bank-account-model-driven.component.html'
})
export class AccountModelDrivenComponent {
}
<div [formGroup]="accountNumber">
<label>Account 2</label>
<div class="form-inline">
<input type="text" formControlName="acc1" />
<input type="text" formControlName="acc2" />
<input type="text" formControlName="acc3" />
<input type="text" formControlName="acc4" />
</div>
</div>
在组件模板中,我们有一个 FormGroup accountNumber
,其内部有 4 个 FormControls - acc1
、acc2
、acc3
、acc4
。这相当于我们的领域模型的 BankAccount
类型。
然后在组件类中,我们使用 FormBuilder 显式构建表单
export class AccountModelDrivenComponent implements OnInit {
accountNumber: FormGroup;
constructor(private formBuilder: FormBuilder) { }
ngOnInit() {
this.accountNumber = this.formBuilder.group({
acc1: '',
acc2: '',
acc3: '',
acc4: ''
});
}
这是 Angular 2 引入的一种新的表单编写方式 - 模型驱动表单,它与 模板驱动表单略有不同,后者对 Angular 1 开发者来说应该很熟悉。基本上,在模型驱动表单中,模板往往更简洁,没有 ngModel;组件往往更冗长,因为您必须清楚地表达您想在那里做什么。
为了让此组件在父表单中充当单个表单控件,我们需要实现 ControlValueAccessor 接口。 Angular 2 源码 说
// A bridge between a control and a native element.
export interface ControlValueAccessor {
writeValue(obj: any): void;
registerOnChange(fn: any): void;
registerOnTouched(fn: any): void;
}
Angular 2 将调用 writeValue() 将模型值传播到视图,并调用 registerOnChange() 来注册一个处理程序函数,该函数会将视图上的任何更改传播到模型。关于此的更详细解释可以在这里找到。
我们的 ControlValueAccessor 实现如下所示
writeValue(value: BankAccount) {
if (value) {
this.accountNumber.setValue(value);
}
}
registerOnChange(fn: (value: any) => void) {
this.accountNumber.valueChanges.subscribe(fn);
}
Angular 2 在这里为我们提供了两个重要的 FormGroup 属性 - setValue
和 valueChanges
。FormGroup.setValue() 会巧妙地将给定的 BankAccount
模型值写入每个 FormControl 视图。FormGroup.valueChanges 是一个observable,它通过订阅其处理程序,使 Angular 2 能够更新模型值和任何视图更改上的有效性。
现在为了让组件验证银行账号,我们使用从 bank-account-validator.directive 导入的 validateAccounGroupFactory
函数来实现 Validator 接口
@Input() myRequired: boolean;
validator: Function;
constructor(accountService: AccountService, private formBuilder: FormBuilder) {
this.validator = validateAccountGroupFactory(accountService);
}
validate(c: FormGroup) {
return this.validator(c, this.myRequired);
}
为了将我们的 Validator 实现挂钩到 NG_VALIDATORS 令牌,请在 providers 元数据中添加以下行
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => AccountModelDrivenComponent), multi: true }
现在我们的组件已经准备好可以放入表单中,用于捕获和验证银行账号。
第三种方式 - 模板驱动组件
如果我们想用我们钟爱的模板驱动方式来做第二种方式中完成的相同事情怎么办?这也是我最后的挑战,感谢回答我 stackoverflow 问题的那位仁兄!
让我们在以下两个文件中创建 AccountTemplateDrivenComponent
- app/bank-account-template-driven.component.ts
- app/bank-account-template-driven.component.html
import { Component } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
@Component ({
selector: 'bank-account-template-driven',
templateUrl: 'app/bank-account-template-driven.component.html'
})
export class AccountTemplateDrivenComponent {
}
<form>
<div ngModelGroup="accountNumber">
<label>Account 3</label>
<div class="form-inline">
<input type="text" name="acc1" [ngModel]="accountNumber.acc1" (ngModelChange)="change('acc1', $event)" />
<input type="text" name="acc2" [ngModel]="accountNumber.acc2" (ngModelChange)="change('acc2', $event)" />
<input type="text" name="acc3" [ngModel]="accountNumber.acc3" (ngModelChange)="change('acc3', $event)" />
<input type="text" name="acc4" [ngModel]="accountNumber.acc4" (ngModelChange)="change('acc4', $event)" />
</div>
</div>
</form>
在组件模板中,我们有一个 ngModelGroup accountNumber
,其内部有 4 个 ngModels - acc1
、acc2
、acc3
、acc4
。另外,我不得不将“香蕉盒”拆分成属性绑定和事件绑定来处理组件上的视图更改。此组件的 ControlValueAccessor 实现如下所示
export class AccountTemplateDrivenComponent implements ControlValueAccessor {
accountNumber = {
acc1: '',
acc2: '',
acc3: '',
acc4: ''
}
constructor(accountService: AccountService) {
this.validator = validateAccountGroupFactory(accountService);
}
change(prop, value) {
this.accountNumber[prop] = value;
this.propagateChange(this.accountNumber);
}
writeValue(value: BankAccount) {
if (value) {
this.accountNumber = value;
}
}
propagateChange = (_: any) => {};
registerOnChange(fn: (value: any) => void) {
this.propagateChange = fn;
}
还记得在模型驱动方式中,我们订阅了 FormGroup.valueChanges observable 来传播视图更改并触发验证吗?在模板驱动方式中,我们没有这个功能,同样的事情应该像这里展示的这样手动完成。如果不通过 change
函数处理 ngModelChange 事件,组件将正确地将视图更改传播到模型,但验证不会被触发。
Validator 实现将与第二种方式完全相同。
摘要
我们已经了解了如何在 Angular 2 框架中使用模型驱动表单和模板驱动表单编写属性指令和自定义组件。要编写一个组件,我更倾向于模型驱动方式,因为它的模板更简洁,而且它似乎拥有更好的设施,如 setValue 和 valueChanges 来表达组件应该如何工作。
.NET 开发者应该会习惯 TypeScript 在类型、接口、类方面的语法,并且很快就会觉得它在编译时检查、可读性和显式抽象方面很出色。
感谢阅读。
历史
- 30Aug16 初始版本
- 31Aug16 修复了代码块中的单引号/双引号和拼写错误
- 03Sep16 添加了使用模型驱动模板的第三个示例
- 24Sep16 添加了模板驱动表单组件的优秀示例