Angular 4 基于AbstractControl自定义表单
Angular 为我们提供了多种方式和 API,进行表单验证。接下来我们将介绍如何利用 AbstractControl
实现 FormGroup
的验证。文章中会涉及 FormGroup
、FormControl
和 FormBuilder
的相关知识,因此建议不了解上述知识的读者,阅读本文前先阅读 Angular 4 Reactive Forms 这篇文章。
Contents
- What is a FormGroup
- FormBuilder/FormGroup source code
- AbstractControl
- Custom validation properties
- Custom validation Object hook
What is a FormGroup
我们先来看一下 Angular 4 Reactive Forms 中,使用 FormBuilder
的示例:
signup-form.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { User } from './signup.interface';
@Component({...})
export class SignupFormComponent implements OnInit {
user: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.user = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
account: this.fb.group({
email: ['', Validators.required],
confirm: ['', Validators.required]
})
});
}
onSubmit({ value, valid }: { value: User, valid: boolean }) {
console.log(value, valid);
}
}
上面示例中,我们通过 FormBuilder
对象提供的 group()
方法,方便的创建 FormGroup
和 FormControl
对象。接下来我们来详细分析一下 FormBuilder
类。
FormBuilder/FormGroup source code
FormBuilder source code
// angular2/packages/forms/src/form_builder.ts 片段
@Injectable()
class FormBuilder {
// 基于controlsConfig、extra信息,创建FormGroup对象
group(controlsConfig: {[key: string]: any}, extra:
{[key: string]: any} = null): FormGroup {}
// 基于formState、validator、asyncValidator创建FormControl对象
control(
formState: Object, validator: ValidatorFn|ValidatorFn[] = null,
asyncValidator: AsyncValidatorFn|AsyncValidatorFn[] = null): FormControl {}
//基于controlsConfig、validator、asyncValidator创建FormArray对象
array(
controlsConfig: any[], validator: ValidatorFn = null,
asyncValidator: AsyncValidatorFn = null): FormArray {}
}
首先,我们先来看一下 group()
方法:
group(controlsConfig: {[key: string]: any}, extra: {[key: string]: any} = null):
FormGroup {}
从 group()
方法签名中,可以清楚的知道该方法的输入参数和返回类型。具体的使用示例如下:
this.user = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
account: this.fb.group({
email: ['', Validators.required],
confirm: ['', Validators.required]
})
});
接下来我们来看一下 group()
方法的内部实现:
group(controlsConfig: {[key: string]: any}, extra: {[key: string]: any} = null):
FormGroup {
// 创建controls对象集合
const controls = this._reduceControls(controlsConfig);
// 获取同步验证器
const validator: ValidatorFn = extra != null ? extra['validator'] : null;
// 获取异步验证器
const asyncValidator: AsyncValidatorFn = extra != null ?
extra['asyncValidator'] : null;
return new FormGroup(controls, validator, asyncValidator);
}
我们在来看一下 _reduceControls()
方法的内部实现:
_reduceControls(controlsConfig: {[k: string]: any}): {[key: string]: AbstractControl} {
const controls: {[key: string]: AbstractControl} = {};
// controlsConfig - {name: [...], account: this.fb.group(...)}
Object.keys(controlsConfig).forEach(controlName => {
// 获取控件的名称,然后基于控件对应的配置信息,创建FormControl控件,并保存到controls对象上
controls[controlName] = this._createControl(controlsConfig[controlName]);
});
return controls;
}
继续看一下 _createControl()
方法的内部实现:
_createControl(controlConfig: any): AbstractControl {
if (controlConfig instanceof FormControl || controlConfig instanceof FormGroup ||
controlConfig instanceof FormArray) {
return controlConfig;
} else if (Array.isArray(controlConfig)) {
// controlConfig - ['', [Validators.required, Validators.minLength(2)]]
const value = controlConfig[0]; // 获取初始值
// 获取同步验证器
const validator: ValidatorFn = controlConfig.length > 1 ? controlConfig[1] : null;
// 获取异步验证器
const asyncValidator: AsyncValidatorFn = controlConfig.length > 2 ?
controlConfig[2] : null;
// 创建FormControl控件
return this.control(value, validator, asyncValidator);
} else {
return this.control(controlConfig);
}
}
最后我们看一下 control()
方法的内部实现:
control(
formState: Object,
validator: ValidatorFn|ValidatorFn[] = null,
asyncValidator: AsyncValidatorFn|AsyncValidatorFn[] = null): FormControl {
return new FormControl(formState, validator, asyncValidator);
}
现在先来总结一下,通过分析 FormBuilder
类的源码,我们发现:
this.fb.group({...}, { validator: someCustomValidator })
等价于
new FormGroup({...}, someCustomValidator)
在我们实现自定义验证规则前,我们在来介绍一下 FormGroup
类。
FormGroup source code
// angular2/packages/forms/src/model.ts 片段
export class FormGroup extends AbstractControl {
constructor(
public controls: {[key: string]: AbstractControl},
validator: ValidatorFn = null,
asyncValidator: AsyncValidatorFn = null) {
super(validator, asyncValidator);
this._initObservables();
this._setUpControls();
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
}
}
通过源码我们发现,FormGroup
类继承于 AbstractControl
类。在创建 FormGroup
对象时,会把 validator
和 asyncValidator
作为参数,然后通过 super
关键字调用基类 AbstractControl
的构造函数。
AbstractControl
接下来我们来看一下 AbstractControl
类:
// angular2/packages/forms/src/model.ts 片段
export abstract class AbstractControl {
_value: any;
...
private _valueChanges: EventEmitter<any>;
private _statusChanges: EventEmitter<any>;
private _status: string;
private _errors: ValidationErrors|null;
private _pristine: boolean = true;
private _touched: boolean = false;
constructor(public validator: ValidatorFn, public asyncValidator: AsyncValidatorFn) {}
// 获取控件的valid状态,用于表示控件是否通过验证
get valid(): boolean { return this._status === VALID; }
// 获取控件的invalid状态,用于表示控件是否通过验证
get invalid(): boolean { return this._status === INVALID; }
// 获取控件的pristine状态,用于表示控件值未改变
get pristine(): boolean { return this._pristine; }
// 获取控件的dirty状态,用于表示控件值已改变
get dirty(): boolean { return !this.pristine; }
// 获取控件的touched状态,用于表示控件已被访问过
get touched(): boolean { return this._touched; }
...
}
使用 AbstractControl 不是实现我们自定义 FormGroup 验证的关键,因为我们也可以注入 FormGroup
来实现与表单控件进行交互。现在我们再来观察一下最初的代码:
@Component({...})
export class SignupFormComponent implements OnInit {
user: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.user = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
account: this.fb.group({
email: ['', Validators.required],
confirm: ['', Validators.required]
})
});
}
}
接下来我们要实现的自定义验证规则是,确保 email
字段的值与 confirm
字段的值能够完全一致。我们可以通过 AbstractControl
来实现该功能,首先我们先来定义验证函数:
email-matcher.ts
export const emailMatcher = () => {};
下一步,我们需要注入 AbstractControl
:
export const emailMatcher = (control: AbstractControl): {[key: string]: boolean} => {
};
在 Angular 4.x Reactive Forms 文章中,我们介绍了通过 FormGroup
对象 (FormGroup 类继承于AbstractControl),提供的 get()
方法,可以获取指定的表单控件。get() 方法的签名如下:
get(path: Array<string|number>|string): AbstractControl { return _find(this, path, '.'); }
// 使用示例 - 获取sub-group的表单控件
this.form.get('person.name');
-OR-
this.form.get(['person', 'name']);
具体示例如下:
<div class="error" *ngIf="user.get('foo').touched &&
user.get('foo').hasError('required')">
This field is required
</div>
了解完 AbstractControl,接下来我们来更新一下 emailMatcher
函数:
export const emailMatcher = (control: AbstractControl): {[key: string]: boolean} => {
const email = control.get('email');
const confirm = control.get('confirm');
};
上面的示例中,control 表示的是 FormGroup
对象,email
和 confirm
都是表示 FormControl
对象。我们可以在控制台中输出它们的值:
► FormGroup {asyncValidator: null, _pristine: true, _touched: false, _onDisabledChange: Array[0], controls: Object…}
► FormControl {asyncValidator: null, _pristine: true, _touched: false, _onDisabledChange: Array[1], _onChange: Array[1]…}
► FormControl {asyncValidator: null, _pristine: true, _touched: false, _onDisabledChange: Array[1], _onChange: Array[1]…}
Custom validation properties
实际上 emailMatcher
自定义验证规则,就是比较 email
与 confirm
控件的值是否一致。如果它们的值是一致的,那么返回 null,表示验证通过,没有出现错误。具体代码如下:
export const emailMatcher = (control: AbstractControl): {[key: string]: boolean} => {
const email = control.get('email');
const confirm = control.get('confirm');
if (!email || !confirm) return null;
if (email.value === confirm.value) {
return null;
}
};
上述代码意味着如果一切正常,我们都不会返回任何错误。现在我们需要添加自定义验证。
Custom validation Object hook
我们先来看一下,在 HTML 模板中,我们自定义验证规则的预期使用方式:
...
<div formGroupName="account">
<label>
<span>Email address</span>
<input type="email" placeholder="Your email address" formControlName="email">
</label>
<label>
<span>Confirm address</span>
<input type="email" placeholder="Confirm your email address"
formControlName="confirm">
</label>
<div class="error" *ngIf="user.get('account').touched &&
user.get('account').hasError('nomatch')">
Email addresses must match
</div>
</div>
...
忽略掉其它无关的部分,我们只关心以下的代码片段:
user.get('account').hasError('nomatch')
这意味着,我们需要先获取 account 对象 (FormGroup实例),然后通过 hasError()
方法,判断是否存在 nomatch
的错误。接下来我们按照该需求更新 emailMatcher
函数,具体如下:
export const emailMatcher = (control: AbstractControl): {[key: string]: boolean} => {
const email = control.get('email');
const confirm = control.get('confirm');
if (!email || !confirm) return null;
return email.value === confirm.value ? null : { nomatch: true };
};
最后,我们需要导入我们的自定义验证规则,然后在调用 fb.group()
创建 account FormGroup对象时,设置第二个参数,具体示例如下:
...
import { emailMatcher } from './email-matcher';
...
ngOnInit() {
this.user = this.fb.group({
name: ['', Validators.required],
account: this.fb.group({
email: ['', Validators.required],
confirm: ['', Validators.required]
}, { validator: emailMatcher })
});
}
...
完整的示例代码如下:
signup.interface.ts
export interface User {
name: string;
account: {
email: string;
confirm: string;
}
}
email-matcher.ts
export const emailMatcher = (control: AbstractControl): {[key: string]: boolean} => {
const email = control.get('email');
const confirm = control.get('confirm');
if (!email || !confirm) {
return null;
}
return email.value === confirm.value ? null : { nomatch: true };
};
signup-form.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { emailMatcher } from './email-matcher';
@Component({
selector: 'signup-form',
template: `
<form class="form" novalidate (ngSubmit)="onSubmit(user)" [formGroup]="user">
<label>
<span>Full name</span>
<input type="text" placeholder="Your full name" formControlName="name">
</label>
<div class="error" *ngIf="user.get('name').touched &&
user.get('name').hasError('required')">
Name is required
</div>
<div formGroupName="account">
<label>
<span>Email address</span>
<input type="email" placeholder="Your email address" formControlName="email">
</label>
<label>
<span>Confirm address</span>
<input type="email" placeholder="Confirm your email address"
formControlName="confirm">
</label>
<div class="error" *ngIf="user.get('account').touched &&
user.get('account').hasError('nomatch')">
Email addresses must match
</div>
</div>
<button type="submit" [disabled]="user.invalid">Sign up</button>
</form>
`
})
export class SignupFormComponent implements OnInit {
user: FormBuilder;
constructor(public fb: FormBuilder) {}
ngOnInit() {
this.user = this.fb.group({
name: ['', Validators.required],
account: this.fb.group({
email: ['', Validators.required],
confirm: ['', Validators.required]
}, { validator: emailMatcher })
});
}
onSubmit({ value, valid }) {
console.log(value, valid);
}
}
具体详情,可以查看线上示例。