Angular 提供了两种不同的方法来通过表单处理用户输入:响应式表单
和模板驱动表单
。 两者都从视图中捕获用户输入事件、验证用户输入、创建表单模型、修改数据模型,并提供跟踪这些更改的途径。
响应式表单
提供对底层表单对象模型直接
、显式
的访问。它们与模板驱动表单相比,更加健壮:它们的可扩展性、可复用性和可测试性
都更高。如果表单是你的应用程序的关键部分,或者你已经在使用响应式表单来构建应用,那就使用响应式表单。模板驱动表单
依赖模板中的指令
来创建和操作底层的对象模型。它们对于向应用添加一个简单的表单非常有用,比如电子邮件列表注册表单。它们很容易添加到应用中,但在扩展性方面不如响应式表单。如果你有可以只在模板中管理的非常基本的表单需求和逻辑,那么模板驱动表单就很合适。响应式 | 模板驱动 | |
---|---|---|
建立表单模型 | 显式的,在组件类中创建 | 隐式的,由指令创建 |
数据模型 | 结构化和不可变的 | 非结构化和可变的 |
可预测性 | 同步 | 异步 |
表单验证 | 函数 | 指令 |
响应式表单和模板驱动型表单都会跟踪用户与之交互的表单输入元素和组件模型中的表单数据之间的值变更。这两种方法共享同一套底层构建块
,只在如何创建
和管理
常用表单控件实例
方面有所不同。
响应式表单和模板驱动表单都建立在下列基础类之上。
响应式表单使用显式的、不可变的方式,管理表单在特定的时间点上的状态。对表单状态的每一次变更都会返回一个新的状态,这样可以在变化时维护模型的整体性。响应式表单是围绕 Observable
流构建的,表单的输入和值都是通过这些输入值组成的流来提供的,它可以同步
访问。
使用表单控件有三个步骤。
在你的应用中注册响应式表单模块。该模块声明了一些你要用在响应式表单中的指令。
生成一个新的 FormControl 实例,并把它保存在组件中。
在模板中注册这个 FormControl。
要使用响应式表单控件,就要从 @angular/forms 包中导入 ReactiveFormsModule,并把它添加到你的 NgModule 的 imports 数组中。
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
imports: [
// other imports ...
ReactiveFormsModule
],
})
export class AppModule { }
要注册一个表单控件,就要导入 FormControl 类并创建一个 FormControl 的新实例,将其保存为类的属性。
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
@Component({
selector: 'app-name-editor',
templateUrl: './name-editor.component.html',
styleUrls: ['./name-editor.component.css']
})
export class NameEditorComponent {
name = new FormControl('');
}
可以用 FormControl 的构造函数设置初始值
,这个例子中它是空字符串
。通过在你的组件类中创建这些控件,你可以直接对表单控件的状态进行监听
、修改
和校验
。
在组件类中创建了控件之后,你还要把它和模板中的一个表单控件关联起来。修改模板,为表单控件添加 formControl 绑定,formControl 是由 ReactiveFormsModule 中的 FormControlDirective 提供的。
<label>
Name:
<input type="text" [formControl]="name">
label>
你可以用下列方式显示它的值:
valueChanges
,你可以在模板中使用 AsyncPipe
或在组件类中使用 subscribe()
方法来监听表单值的变化。<label>
Name:
<input type="text" [formControl]="name">
label>
<p>Value: {{ name.value }}p>
public name = new FormControl('test');
public testValueChange() {
this.name.valueChanges.subscribe({
next: value => {
console.log("name value is: " + value);
}
})
}
响应式表单还有一些方法可以用编程的方式``修改
控件的值,它让你可以灵活的修改控件的值而不需要借助用户交互。FormControl 提供了一个 setValue()
方法,它会修改这个表单控件的值,并且验证与控件结构相对应的值的结构。比如,当从后端 API 或服务接收到了表单数据时,可以通过 setValue() 方法来把原来的值替换为新的值。
updateName() {
this.name.setValue('Nancy' + new Date().getTime());
}
<p>
<button (click)="updateName()">Update Namebutton>
p>
表单中通常会包含几个相互关联的控件
。响应式表单提供了两种把多个相关控件分组到同一个输入表单中的方法。
表单组
定义了一个带有一组控件的表单,你可以把它们放在一起管理。表单组的基础知识将在本节中讨论。你也可以通过嵌套表单组
来创建更复杂的表单。表单数组
定义了一个动态表单,你可以在运行时添加和删除控件。你也可以通过嵌套表单数组
来创建更复杂的表单要将表单组添加到此组件中,请执行以下步骤。
FormGroup
实例。在组件类中创建一个名叫 profileForm 的属性,并设置为 FormGroup 的一个新实例。要初始化这个 FormGroup,请为构造函数提供一个由控件组成的对象,对象中的每个名字都要和表单控件的名字一一对应。
import { FormControl, FormGroup } from '@angular/forms';
profileForm = new FormGroup({
firstName: new FormControl(''),
lastName: new FormControl(''),
});
// 可以整个获取值
public onSubmit() {
// TODO: Use EventEmitter with form value
console.warn(this.profileForm.value);// {firstName: "", lastName: ""}
}
// 可以借助 valueChanges 整个可观察对象整个获取值
this.profileForm.valueChanges.subscribe( {
next: value => {
console.log("name value is: " + JSON.stringify(value)); // dashboard.component.ts:53 name value is: {"firstName":"dddd","lastName":"bb"}
}
})
// 可以通过后期单个控件单独获取值
this.profileForm.get('firstName').valueChanges.subscribe({
next: value => {
console.log("First Name is: " + value); // First Name is: aa
}
ps: 这个 FormGroup 用对象的形式提供了它的模型值,这个值来自组中每个控件的值
。 FormGroup 实例拥有和 FormControl 实例相同的属性
(比如 value、untouched)和方法(比如 setValue())。
这个表单组还能跟踪其中每个控件的状态及其变化,所以如果其中的某个控件的状态或值变化了,父控件也会发出一次新的状态变更或值变更事件。该控件组的模型来自它的所有成员。在定义了这个模型之后,你必须更新模板,来把该模型反映到视图中。
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
<label>
First Name:
<input type="text" formControlName="firstName">
label>
<label>
Last Name:
<input type="text" formControlName="lastName">
label>
<button type="submit" [disabled]="!profileForm.valid">Submitbutton>
form>
表单组可以同时接受单个表单控件实例和其它表单组实例作为其子控件。这可以让复杂的表单模型更容易维护,并在逻辑上把它们分组到一起。
要制作更复杂的表单,请遵循如下步骤。
要在 profileForm 中创建一个嵌套组,就要把一个嵌套的 address 元素添加到此表单组的实例中。
public profileForm = new FormGroup({
firstName: new FormControl(''),
lastName: new FormControl(''),
address: new FormGroup({
street: new FormControl(''),
city: new FormControl(''),
state: new FormControl(''),
zip: new FormControl('')
})
});
// 可以借助 valueChanges 整个可观察对象整个获取值
this.profileForm.valueChanges.subscribe( {
next: value => {
console.log("name value is: " + JSON.stringify(value));// name value is: {"firstName":"","lastName":"","address":{"street":"b","city":"","state":"","zip":""}}
}
});
// 可以通过后期单个控件单独获取值
this.profileForm.get('firstName').valueChanges.subscribe({
next: value => {
console.log("First Name is: " + value);
}
});
// 可以获取form组件某个form组的整个值
this.profileForm.get('address').valueChanges.subscribe(({
next: value => {
console.log('address value is: ' + JSON.stringify(value));// address value is: {"street":"b","city":"","state":"","zip":""}
}
}));
// 可以获取form组件某个form组的某个formcontrol实例的值
this.profileForm.get('address').get('street').valueChanges.subscribe(({
next: value => {
console.log('street value is: ' + value);// street value is: b
}
}));
在修改了组件类中的模型之后,还要修改模板,来把这个 FormGroup 实例对接到它的输入元素。
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
<label>
First Name:
<input type="text" formControlName="firstName">
label>
<label>
Last Name:
<input type="text" formControlName="lastName">
label>
<div formGroupName="address">
<h3>Addressh3>
<label>
Street:
<input type="text" formControlName="street">
label>
<label>
City:
<input type="text" formControlName="city">
label>
<label>
State:
<input type="text" formControlName="state">
label>
<label>
Zip Code:
<input type="text" formControlName="zip">
label>
div>
<button type="submit" [disabled]="!profileForm.valid">Submitbutton>
form>
当修改包含多个 FormGroup 实例的值时,你可能只希望更新模型中的一部分,而不是完全替换掉。
有两种更新模型值的方式:
setValue()
方法来为单个控件
设置新值。 setValue() 方法会严格遵循表单组的结构
,并整体性替换控件的值
。patchValue()
方法可以用对象中所定义的任何属性
为表单模型进行替换。setValue() 方法的严格检查可以帮助你捕获复杂表单嵌套中的错误,而 patchValue() 在遇到那些错误时可能会默默的失败。
public updateProfile() {
// profileForm 模型中只有 firstName 和 street 被修改了。注意,street 是在 address 属性的对象中被修改的。这种结构是必须的,因为 patchValue() 方法要针对模型的结构进行更新。patchValue() 只会更新表单模型中所定义的那些属性。
this.profileForm.patchValue({
firstName: 'Nancy' + new Date().getTime(),
address: {
street: '123 Drew Street' + new Date().getTime()
}
});
// ERROR Error: Must supply a value for form control with name: 'lastName'.
// setValue() 方法会严格遵循表单组的结构
this.profileForm.setValue({
firstName: 'Nancy' + new Date().getTime(),
address: {
street: '123 Drew Street' + new Date().getTime()
}
});
}
FormArray 是 FormGroup 之外的另一个选择,用于管理任意数量的匿名控件。像 FormGroup 实例一样,你也可以往 FormArray 中动态插入和移除控件,并且 FormArray 实例的值和验证状态也是根据它的子控件计算得来的。 不过,你不需要为每个控件定义一个名字作为 key,因此,如果你事先不知道子控件的数量,这就是一个很好的选择。
要定义一个动态表单,请执行以下步骤。
通过把一组(从零项到多项)控件定义在一个数组中来初始化一个 FormArray。为 profileForm 添加一个 aliases 属性,把它定义为 FormArray 类型。
import { FormControl, FormGroup, FormArray } from '@angular/forms';
public profileForm = new FormGroup({
firstName: new FormControl(''),
lastName: new FormControl(''),
address: new FormGroup({
street: new FormControl(''),
city: new FormControl(''),
state: new FormControl(''),
zip: new FormControl('')
}),
aliases: new FormArray([
new FormControl('1')
])
});
public aliases = (<FormArray>this.profileForm.get('aliases'));
public addAlias() {
(<FormArray>this.profileForm.get('aliases')).push(new FormControl('1'));
}
// 获取整个 formArray 的数据
this.profileForm.get('aliases').valueChanges.subscribe({
next: value => {
console.log('aliases values is: ' + JSON.stringify(value)); // aliases values is: ["1","3"]
}
});
// 获取 formArray 中单个 formControl 的数据
(<FormArray>this.profileForm.get('aliases')).controls[0].valueChanges.subscribe({
next: value => {
console.log('aliases[0] values is: ' + value); // aliases[0] values is: 0
}
})
要想为表单模型添加 aliases,你必须把它加入到模板中供用户输入。和 FormGroupNameDirective 提供的 formGroupName 一样,FormArrayNameDirective 也使用 formArrayName 在这个 FormArray 实例和模板之间建立绑定
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
<label>
First Name:
<input type="text" formControlName="firstName">
label>
<label>
Last Name:
<input type="text" formControlName="lastName">
label>
<div formGroupName="address">
<h3>Addressh3>
<label>
Street:
<input type="text" formControlName="street">
label>
<label>
City:
<input type="text" formControlName="city">
label>
<label>
State:
<input type="text" formControlName="state">
label>
<label>
Zip Code:
<input type="text" formControlName="zip">
label>
div>
<div formArrayName="aliases">
<h3>Aliasesh3> <button (click)="addAlias()">Add Aliasbutton>
<div *ngFor="let alias of aliases.controls; let i=index">
<label>
Alias:
<input type="text" [formControlName]="i">
label>
div>
div>
form>
类 | 说明 |
---|---|
AbstractControl | 所有三种表单控件类(FormControl、FormGroup 和 FormArray)的抽象基类。它提供了一些公共的行为和属性。 |
FormControl | 管理单体表单控件的值和有效性状态。它对应于 HTML 的表单控件,比如 或 。 |
FormGroup | 管理一组 AbstractControl 实例的值和有效性状态。该组的属性中包括了它的子控件。组件中的顶层表单就是 FormGroup。 |
FormArray | 管理一些 AbstractControl 实例数组的值和有效性状态。 |
FormBuilder | 一个可注入的服务,提供一些用于提供创建控件实例的工厂方法。 |
在模板驱动表单中,表单模型是隐式的,而不是显式的。指令 NgModel 为指定的表单元素创建并管理一个 FormControl 实例。
下面的组件使用模板驱动表单为单个控件实现了同样的输入字段。
import { Component } from '@angular/core';
@Component({
selector: 'app-template-favorite-color',
template: `
Favorite Color:
`
})
export class FavoriteColorComponent {
favoriteColor = '';
}
在组件类中直接
把验证器函数添加到表单控件模型
上(FormControl)。然后,一旦控件发生了变化,Angular 就会调用这些函数。
验证器函数可以是同步函数,也可以是异步函数。
一组验证错误或 null
。你可以在实例化一个 FormControl 时把它作为构造函数的第二个参数
传进去。一个 Promise 或 Observable
,它稍后
会发出一组验证错误或 null
。在实例化 FormControl 时,可以把它们作为第三个参数
传入。出于性能方面的考虑,只有在所有同步验证器都通过之后,Angular 才会运行异步验证器。当每一个异步验证器都执行完之后,才会设置这些验证错误。
在模板驱动表单中用作属性的那些内置验证器,比如 required 和 minlength,也都可以作为 Validators 类中的函数使用
public profileForm = new FormGroup({
firstName: new FormControl('', [
Validators.required
]),
});
this.profileForm.get('firstName').valueChanges.subscribe({
next: value => {
console.log("First Name is: " + value);
console.log(this.profileForm.get('firstName').errors);// { required: true } | null
}
});
<form [formGroup]="profileForm">
<label>
First Name:
<input type="text" formControlName="firstName">
<div *ngIf="firstName.errors?.required">
Name is required.
div>
label>
form>
内置的验证器并不是总能精确匹配应用中的用例,因此有时你需要创建一个自定义验证器。
public profileForm = new FormGroup({
firstName: new FormControl('', [
Validators.required,
this.forbiddenNameValidator(/bob/i)
])
});
public forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} | null => {
const forbidden = nameRe.test(control.value);
return forbidden ? {forbiddenName: {value: control.value}} : null;
};
}
get firstName() { return this.profileForm.get('firstName'); }
this.profileForm.get('firstName').valueChanges.subscribe({
next: value => {
console.log("First Name is: " + value); // First Name is: bob
console.log(JSON.stringify(this.profileForm.get('firstName').errors));// {"forbiddenName":{"value":"bob"}} | null
}
});
跨字段交叉验证器是一种自定义验证器
,可以对表单中不同字段的值进行比较,并针对它们的组合进行接受或拒绝。
下列交叉验证的例子说明了如何进行如下操作:
要想在单个自定义验证器中计算这两个控件,你就必须在它们共同的祖先控件中执行验证: FormGroup。你可以在 FormGroup 中查询它的子控件,从而让你能比较它们的值。要想给 FormGroup 添加验证器,就要在创建时把一个新的验证器传给它的第二个参数。
this.profileForm.valueChanges.subscribe( {
next: value => {
console.log(JSON.stringify(this.profileForm.errors));// {"identityRevealed":true} | null
}
});
public profileForm = new FormGroup({
firstName: new FormControl('', [
Validators.required,
]),
lastName: new FormControl(''),
}, { validators: this.identityRevealedValidator});
public identityRevealedValidator(control: FormGroup): ValidationErrors | null{
const firstName = control.get('firstName');
const lastName = control.get('lastName');
return firstName && lastName && firstName.value === lastName.value ? { identityRevealed: true } : null;
};
异步验证器实现了 AsyncValidatorFn
和 AsyncValidator
接口。它们与其同步版本非常相似,但有以下不同之处。
Promise 或可观察对象
,有尽
的,这意味着它必须在某个时刻完成(complete)
。要把无尽的可观察对象转换成有尽的,可以在管道中加入过滤操作符,比如 first、last、take 或 takeUntil。异步验证在同步验证完成后才会发生
,并且只有在同步验证成功时才会执行。如果更基本的验证方法已经发现了无效输入,那么这种检查顺序就可以让表单避免使用昂贵的异步验证流程(例如 HTTP 请求)。
let formControl = this.profileForm.get('firstName');
formControl.updateValueAndValidity();
angular表单: https://angular.cn/guide/form-validation