表单
Angular 提供了两种不同的方法来通过表单处理用户输入:响应式表单和模板驱动表单。 两者都从视图中捕获用户输入事件、验证用户输入、创建表单模型、修改数据模型,并提供跟踪这些更改的途径。
选择一种方法
响应式表单和模板驱动表单以不同的方式处理和管理表单数据。每种方法都有各自的优点。
表单 | 详情 |
---|---|
响应式表单 | 提供对底层表单对象模型直接、显式的访问。它们与模板驱动表单相比,更加健壮:它们的可扩展性、可复用性和可测试性都更高。如果表单是你的应用程序的关键部分,或者你已经在使用响应式表单来构建应用,那就使用响应式表单。 |
模板驱动表单 | 依赖模板中的指令来创建和操作底层的对象模型。它们对于向应用添加一个简单的表单非常有用,比如电子邮件列表注册表单。它们很容易添加到应用中,但在扩展性方面不如响应式表单。如果你有可以只在模板中管理的非常基本的表单需求和逻辑,那么模板驱动表单就很合适。 |
建立表单模型
响应式表单和模板驱动型表单都会跟踪用户与之交互的表单输入元素和组件模型中的表单数据之间的值变更。这两种方法共享同一套底层构建块,只在如何创建和管理常用表单控件实例方面有所不同。
常用表单基础类
响应式表单和模板驱动表单都建立在下列基础类之上。
基础类 | 详情 |
---|---|
FormControl | 追踪单个表单控件的值和验证状态。 |
FormGroup | 追踪一个表单控件组的值和状态。 |
FormArray | 追踪表单控件数组的值和状态。 |
ControlValueAccessor | 在 Angular 的 FormControl 实例和内置 DOM 元素之间创建一个桥梁 |
建立响应式表单
对于响应式表单,你可以直接在组件类中定义表单模型。[formControl] 指令会通过内部值访问器来把显式创建的 FormControl 实例与视图中的特定表单元素联系起来。
下面的组件使用响应式表单为单个控件实现了一个输入字段。在这个例子中,表单模型是 FormControl 实例。
1 | import { Component } from '@angular/core'; |
建立模板驱动表单
在模板驱动表单中,表单模型是隐式的,而不是显式的。指令 NgModel 为指定的表单元素创建并管理一个 FormControl 实例。
下面的组件使用模板驱动表单为单个控件实现了同样的输入字段。
1 | import { Component } from '@angular/core'; |
响应式表单
响应式表单使用显式的、不可变的方式,管理表单在特定的时间点上的状态。对表单状态的每一次变更都会返回一个新的状态,这样可以在变化时维护模型的整体性。响应式表单是围绕 Observable 流构建的,表单的输入和值都是通过这些输入值组成的流来提供的,它可以同步访问。
添加基础表单控件
下面的例子展示了如何添加一个表单控件。在这个例子中,用户在输入字段中输入自己的名字,捕获其输入值,并显示表单控件的当前值。
动作 | 详情 |
---|---|
注册响应式表单模块 | 要使用响应式表单控件,就要从 @angular/forms 包中导入 ReactiveFormsModule,并把它添加到你的 NgModule 的 imports 数组中。 |
1 | import { ReactiveFormsModule } from '@angular/forms'; |
生成新的 FormControl | 可以用 FormControl 的构造函数设置初始值,这个例子中它是空字符串。通过在你的组件类中创建这些控件,你可以直接对表单控件的状态进行监听、修改和校验。 |
1 | import { Component } from '@angular/core'; |
在模板中注册该控件 | 在组件类中创建了控件之后,你还要把它和模板中的一个表单控件关联起来。修改模板,为表单控件添加 formControl 绑定,formControl 是由 ReactiveFormsModule 中的 FormControlDirective 提供的。 |
1 | <label for="name">Name: </label> |
使用这种模板绑定语法,把该表单控件注册给了模板中名为 name 的输入元素。这样,表单控件和 DOM 元素就可以互相通讯了:视图会反映模型的变化,模型也会反映视图中的变化。
显示该组件 | 把该组件添加到模板时,将显示指派给 name 的表单控件。 |
1 | <app-name-editor></app-name-editor> |
显示表单控件的值
你可以用下列方式显示它的值。
- 通过可观察对象 valueChanges,你可以在模板中使用 AsyncPipe 或在组件类中使用 subscribe() 方法来监听表单值的变化。
- 使用 value 属性。它能让你获得当前值的一份快照。
一旦你修改了表单控件所关联的元素,这里显示的值也跟着变化了。
替换表单控件的值
FormControl
实例提供了一个setValue()
方法,它会修改这个表单控件的值,并且验证与控件结构相对应的值的结构。比如,当从后端 API 或服务接收到了表单数据时,可以通过setValue()
方法来把原来的值替换为新的值。
下列的例子往组件类中添加了一个方法,它使用setValue()
方法来将控件的值修改为 Nancy。
1 | updateName() { |
在这个例子中,你只使用单个控件,但是当调用 FormGroup 或 FormArray 实例的 setValue() 方法时,传入的值就必须匹配控件组或控件数组的结构才行。
把表单控件分组
就像 FormControl 的实例能让你控制单个输入框所对应的控件一样,FormGroup 的实例也能跟踪一组 FormControl 实例(比如一个表单)的表单状态。当创建 FormGroup 时,其中的每个控件都会根据其名字进行跟踪。下面的例子展示了如何管理单个控件组中的多个 FormControl 实例。
生成一个 ProfileEditor 组件并从 @angular/forms 包中导入 FormGroup 和 FormControl 类。
import { FormGroup, FormControl } from '@angular/forms';
创建一个 FormGroup 实例。
在组件类中创建一个名叫 profileForm 的属性,并设置为 FormGroup 的一个新实例。要初始化这个 FormGroup,请为构造函数提供一个由控件组成的对象,对象中的每个名字都要和表单控件的名字一一对应。对此个人档案表单,要添加两个 FormControl 实例,名字分别为 firstName 和 lastName。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import { Component } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
({
selector: 'app-profile-editor',
templateUrl: './profile-editor.component.html',
styleUrls: ['./profile-editor.component.css']
})
export class ProfileEditorComponent {
profileForm = new FormGroup({
firstName: new FormControl(''),
lastName: new FormControl(''),
address: new FormGroup({
street: new FormControl(''),
city: new FormControl(''),
state: new FormControl(''),
zip: new FormControl('')
})
});
}这些独立的表单控件被收集到了一个控件组中。这个 FormGroup 用对象的形式提供了它的模型值,这个值来自组中每个控件的值。FormGroup 实例拥有和 FormControl 实例相同的属性(比如 value、untouched)和方法(比如 setValue())。
把这个 FormGroup 模型关联到视图。
这个表单组还能跟踪其中每个控件的状态及其变化,所以如果其中的某个控件的状态或值变化了,父控件也会发出一次新的状态变更或值变更事件。该控件组的模型来自它的所有成员。在定义了这个模型之后,你必须更新模板,来把该模型反映到视图中。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<form [formGroup]="profileForm">
<label for="first-name">First Name: </label>
<input id="first-name" type="text" formControlName="firstName">
<label for="last-name">Last Name: </label>
<input id="last-name" type="text" formControlName="lastName">
<div formGroupName="address">
<h2>Address</h2>
<label for="street">Street: </label>
<input id="street" type="text" formControlName="street">
<label for="city">City: </label>
<input id="city" type="text" formControlName="city">
<label for="state">State: </label>
<input id="state" type="text" formControlName="state">
<label for="zip">Zip Code: </label>
<input id="zip" type="text" formControlName="zip">
</div>
</form>就像 FormGroup 所包含的那控件一样,profileForm 这个 FormGroup 也通过 FormGroup 指令绑定到了 form 元素,在该模型和表单中的输入框之间创建了一个通讯层。
由 FormControlName 指令提供的 formControlName 属性把每个输入框和 FormGroup 中定义的表单控件绑定起来。这些表单控件会和相应的元素通讯,它们还把更改传给 FormGroup,这个 FormGroup 是模型值的事实之源。
保存表单数据
ProfileEditor 组件从用户那里获得输入,但在真实的场景中,你可能想要先捕获表单的值,等将来在组件外部进行处理。FormGroup 指令会监听 form 元素发出的 submit 事件,并发出一个 ngSubmit 事件,让你可以绑定一个回调函数。把 onSubmit() 回调方法添加为 form 标签上的 ngSubmit 事件监听器。1
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
ProfileEditor 组件上的 onSubmit() 方法会捕获 profileForm 的当前值。要保持该表单的封装性,就要使用 EventEmitter 向组件外部提供该表单的值。下面的例子会使用 console.warn 把这个值记录到浏览器的控制台中。
1
2
3
4onSubmit() {
// TODO: Use EventEmitter with form value
console.warn(this.profileForm.value);
}显示此组件
要显示包含此表单的 ProfileEditor 组件,请把它添加到组件模板中。
1
<app-profile-editor></app-profile-editor>
ProfileEditor 让你能管理 FormGroup 中的 firstName 和 lastName 等 FormControl 实例。
更新部分数据模型
当修改包含多个 FormGroup 实例的值时,你可能只希望更新模型中的一部分,而不是完全替换掉。
有两种更新模型值的方式:
方法 | 详情 |
---|---|
setValue() | 使用 setValue() 方法来为单个控件设置新值。setValue() 方法会严格遵循表单组的结构,并整体性替换控件的值。 |
patchValue() | 用此对象中定义的任意属性对表单模型进行替换。 |
setValue() 方法的严格检查可以帮助你捕获复杂表单嵌套中的错误,而 patchValue() 在遇到那些错误时可能会默默的失败。
1 | updateProfile() { |
profileForm 模型中只有 firstName 和 street 被修改了。注意,street 是在 address 属性的对象中被修改的。这种结构是必须的,因为 patchValue() 方法要针对模型的结构进行更新。patchValue() 只会更新表单模型中所定义的那些属性。
使用 FormBuilder 服务生成控件
手动创建多个表单控件实例会非常繁琐。FormBuilder 服务提供了一些便捷方法来生成表单控件。FormBuilder 在幕后也使用同样的方式来创建和返回这些实例,只是用起来更简单。
通过下列步骤可以利用这项服务。
导入 FormBuilder 类。
从 @angular/forms 包中导入 FormBuilder 类。
1
import { FormBuilder } from '@angular/forms';
注入这个 FormBuilder 服务。
FormBuilder 是一个可注入的服务提供者,它是由 ReactiveFormModule 提供的。只要把它添加到组件的构造函数中就可以注入这个依赖。
constructor(private fb: FormBuilder) { }
生成表单内容。
FormBuilder 服务有三个方法:control()、group() 和 array()。这些方法都是工厂方法,用于在组件类中分别生成 FormControl、FormGroup 和 FormArray。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
({
selector: 'app-profile-editor',
templateUrl: './profile-editor.component.html',
styleUrls: ['./profile-editor.component.css']
})
export class ProfileEditorComponent {
profileForm = this.fb.group({
firstName: [''],
lastName: [''],
address: this.fb.group({
street: [''],
city: [''],
state: [''],
zip: ['']
}),
});
constructor(private fb: FormBuilder) { }
}在上面的例子中,你可以使用 group() 方法,用和前面一样的名字来定义这些属性。这里,每个控件名对应的值都是一个数组,这个数组中的第一项是其初始值。
响应式表单 API 汇总
下表给出了用于创建和管理响应式表单控件的基础类和服务。要了解完整的语法,请参阅 API 文档中的 Forms 包。
类
类 | 详情 |
---|---|
AbstractControl | 所有三种表单控件类(FormControl、FormGroup 和 FormArray)的抽象基类。它提供了一些公共的行为和属性。 |
FormControl | 管理单体表单控件的值和有效性状态。它对应于 HTML 的表单控件,比如 或 |
FormGroup | 管理一组 AbstractControl 实例的值和有效性状态。该组的属性中包括了它的子控件。组件中的顶层表单就是 FormGroup。 |
FormArray | 管理一些 AbstractControl 实例数组的值和有效性状态。 |
FormBuilder | 一个可注入的服务,提供一些用于提供创建控件实例的工厂方法。 |
FormRecord | 跟踪 FormControl 实例集合的值和有效性状态,每个实例都具有相同的值类型。 |
指令
指令 | 详情 |
---|---|
FormControlDirective | 把一个独立的 FormControl 实例绑定到表单控件元素。 |
FormControlName | 把一个现有 FormGroup 中的 FormControl 实例根据名字绑定到表单控件元素。 |
FormGroupDirective | 把一个现有的 FormGroup 实例绑定到 DOM 元素。 |
FormGroupName | 把一个内嵌的 FormGroup 实例绑定到一个 DOM 元素。 |
FormArrayName | 把一个内嵌的 FormArray 实例绑定到一个 DOM 元素。 |
类型化表单
自动无类型表单迁移
升级到 Angular 14 时,包含的迁移将自动使用相应的无类型版本替换代码中的所有表单类。例如,上面的代码段将变为:
1 | const login = new UntypedFormGroup({ |
每个 Untyped 符号都与以前的 Angular 版本具有完全相同的语义,因此你的应用程序应该像以前一样继续编译。通过删除 Untyped 前缀,你可以增量启用这些类型。
FormControl :入门
1 | const email = new FormControl('angularrox@gmail.com'); |
此控件将被自动推断为 FormControl<string|null> 类型。TypeScript 会在整个FormControl API中自动强制执行此类型,例如 email.value 、 email.valueChanges 、 email.setValue(…) 等。
可空性
你可能想知道:为什么此控件的类型包含 null ?这是因为控件可以随时通过调用 reset 变为 null
1 | const email = new FormControl('angularrox@gmail.com'); |
TypeScript 将强制你始终处理控件已变为 null 的可能性。如果要使此控件不可为空,可以用 nonNullable 选项。这将导致控件重置为其初始值,而不是 null :
1 | const email = new FormControl('angularrox@gmail.com', {nonNullable: true}); |
指定显式类型
可以指定类型,而不是依赖推理。考虑一个初始化为 null 的控件。因为初始值为 null,所以 TypeScript 将推断 FormControl
1 | const email = new FormControl(null); |
为防止这种情况,我们将类型显式指定为 string|null
1 | const email = new FormControl<string|null>(null); |
FormArray :动态的、同质的集合
FormArray 包含一个开放式控件列表。type 参数对应于每个内部控件的类型:
1 | const names = new FormArray([new FormControl('Alex')]); |
此 FormArray 将具有内部控件类型 FormControl<string|null>。
如果你想在数组中有多个不同的元素类型,则必须使用 UntypedFormArray,因为 TypeScript 无法推断哪种元素类型将出现在哪个位置。
FormGroup 和 FormRecord
Angular 为具有枚举键集的表单提供了 FormGroup 类型,并为开放式或动态组提供了一种名为 FormRecord 的类型。
1 | const login = new FormGroup({ |
在任何 FormGroup 上,都可以禁用控件。任何禁用的控件都不会出现在组的值中。
因此,login.value 的类型是 Partial<{email: string, password: string}>。这种类型的 Partial 意味着每个成员可能是未定义的。
更具体地说,login.value.email 的类型是 string|undefined,TypeScript 将强制你处理可能 undefined 的值(如果你启用了 strictNullChecks)。
如果你想访问包括禁用控件的值,从而绕过可能的 undefined 字段,可以用 login.getRawValue()。
FormBuilder 和 NonNullableFormBuilder
FormBuilder 类已升级为支持新类型,方式与上面的示例相同。
此外,还有一个额外的构建器:NonNullableFormBuilder。它是在所有控件都上指定 {nonNullable: true} 的简写,用来在大型非空表单中消除主要的样板代码。你可以用 FormBuilder 上的 nonNullable 属性访问它:
1 | const fb = new FormBuilder(); |
在上面的示例中,两个内部控件都将不可为空
表单常用 元素
NgForm
类型:DIRECTIVE
选择器:
form:not([ngNoForm]):not([formGroup])
ng-form
[ngForm]
属性:
submitted: boolean 返回是否已触发表单提交。
form: FormGroup 为此表单创建的 FormGroup
@Output()ngSubmit: EventEmitter “ngSubmit” 的事件发射器
@Input(‘ngFormOptions’) options NgForm 实例的选项。接受下列属性:updateOn:为所有子级的 NgModel 设置 updateOn 的默认值(除非子 NgModel 通过 ngModelOptions 显式指定了这个值)。可能的值有:’change’ | ‘blur’ | ‘submit’.
formDirective: Form 指令实例。
control: FormGroup 内部 FormGroup 实例。
path: string[] 返回表示该组路径的数组。由于此指令始终位于调用表单的顶层,因此它始终是一个空数组。
controls: {[key: string]: AbstractControl;} 返回此组中控件的映射表。
#myTemplateVar=”ngForm”
说明
创建一个顶级的 FormGroup 实例,并把它绑定到一个表单,以跟踪表单的聚合值及其验证状态。
只要你导入了 FormsModule,该指令就会默认在所有
你可以以 ngForm 作为 key 把该指令导出到一个局部模板变量(如 #myForm=”ngForm”)。这是可选的,但很有用。来自本指令背后的 FormGroup 实例的很多属性,都被复制到了指令自身,所以拿到一个对该指令的引用就可以让你访问此表单的聚合值和验证状态, 还有那些用户交互类的属性,比如 dirty 和 touched。
要使用该表单注册的子控件,请使用带有 name 属性的 NgModel。你可以使用 NgModelGroup 在表单中创建子组。
如果需要,还可以监听该指令的 ngSubmit 事件,以便当用户触发了一次表单提交时得到通知。发出 ngSubmit 事件时,会携带原始的 DOM 表单提交事件。
在模板驱动表单中,所有
NgModelGroup
类型:DIRECTIVE
选择器:[ngModelGroup]
属性: @Input(‘ngModelGroup’) name: string 跟踪绑定到指令 NgModelGroup 的名称。该名称对应于父 NgForm 中的键名。
模板变量参考手册: #myTemplateVar=”ngModelGroup”
说明
创建 FormGroup 的实例并将其绑定到 DOM 元素。
此指令只能用作 NgForm 的子级(在
使用此指令可以独立于表单的其余部分来验证表单的子组,或者当把领域模型中的某些值和嵌套对象一起使用更有意义时。
在表单组中使用控件
1 | @Component({ |
NgModel
类型:DIRECTIVE
选择器:
[ngModel]:not([formControlName]):not([formControl])
属性:
@Input(‘ngModelOptions’) options: {
name?: string;
standalone?: boolean;
updateOn?: FormHooks;
}
跟踪该 ngModel 实例的配置项。name:用来设置表单控件元素的 name 属性的另一种方式。参见把 ngModel 用作独立控件的那个例子。
standalone:如果为 true,则此 ngModel 不会把自己注册进它的父表单中,其行为就像没在表单中一样。默认为 false。
updateOn: 用来定义该何时更新表单控件的值和有效性。默认为 ‘change’。可能的取值为:’change’ | ‘blur’ | ‘submit’。
模板变量参考手册: #myTemplateVar=”ngModel”
说明
根据领域对象创建一个 FormControl 实例,并把它绑定到一个表单控件元素上。
当在
如果你只是要为表单设置初始值,对 ngModel 使用单向绑定就够了。在提交时,你可以使用从表单导出的值,而不必使用领域模型的值。
FormControl
类型:CLASS
说明
追踪单个表单控件的值和验证状态。
这是 Angular 表单的四个基本构建块之一,与 FormGroup、FormArray 和 FormRecord。它扩展了 AbstractControl 类,该类实现了用于访问值、验证状态、用户交互和事件的大多数基础特性。
FormControl 接受一个通用参数,该参数描述其值的类型。此参数始终隐式包含 null,因为控件可以重置。要更改此行为,请设置 nonNullable 或查看下面的使用说明。
FormGroup
类型:CLASS
说明
跟踪一组 FormControl 实例的值和有效状态。
FormGroup 是用于在 Angular 中定义表单的四个基本构建块之一,与 FormControl、FormArray 和 FormRecord。
当实例化 FormGroup 时,请传入子控件的集合作为第一个参数。每个子项的键都会注册控件的名称。
FormGroup 适用于提前知道密钥的用例。如果你需要动态添加和删除控件,请改用 FormRecord。
FormGroup 接受一个可选的类型参数 TControl,它是一种以内部控件类型作为值的对象类型。
FormRecord
类型:CLASS
说明
跟踪 FormControl 实例集合的值和有效性状态,每个实例都具有相同的值类型。
FormRecord 与 FormGroup 非常相似,除了它可以与动态键一起使用,并根据需要添加和删除控件。
FormRecord 接受一个通用参数,该参数描述了它包含的控件的类型。
使用说明
1 | let numbers = new FormRecord({bill: new FormControl('415-123-456')}); |
一个非常简单的例子如下:如何在事先不知道key的情况下,动态地将控件添加到现有的表单中?
对 FormGroup 类进行严格的类型化,这种工作可能会很复杂。
Angular 添加了一个新的 API 来解决这个问题,FormRecord
1 |
|
FormRecord 类允许您动态添加其值必须具有相同类型的控件。
FormArray
类型:CLASS
说明
跟踪 FormControl、FormGroup 或 FormArray 实例的数组的值和有效状态。
FormArray 是 FormGroup 之外的另一个选择,用于管理任意数量的匿名控件。像 FormGroup 实例一样,你也可以往 FormArray 中动态插入和移除控件,并且 FormArray 实例的值和验证状态也是根据它的子控件计算得来的。不过,你不需要为每个控件定义一个名字作为 key,因此,如果你事先不知道子控件的数量,这就是一个很好的选择。
使用说明
- 定义 FormArray 控件:
为 profileForm 添加一个 aliases 属性,把它定义为 FormArray 类型。
使用 FormBuilder.array() 方法来定义该数组,并用 FormBuilder.control() 方法来往该数组中添加一个初始控件。
1 | profileForm = this.fb.group({ |
- 访问 FormArray 控件
使用 getter 语法创建类属性 aliases,以从父表单组中接收表示绰号的表单数组控件。
1 | get aliases() { |
因为返回的控件的类型是 AbstractControl,所以你要为该方法提供一个显式的类型声明来访问 FormArray 特有的语法。
- 在模板中显示表单数组
要想为表单模型添加 aliases,你必须把它加入到模板中供用户输入。和 FormGroupNameDirective 提供的 formGroupName 一样,FormArrayNameDirective 也使用 formArrayName 在这个 FormArray 实例和模板之间建立绑定。
在 formGroupName元素的结束标签下方,添加一段模板 HTML。1
2
3
4
5
6
7
8
9
10<div formArrayName="aliases">
<h2>Aliases</h2>
<button type="button" (click)="addAlias()">+ Add another alias</button>
<div *ngFor="let alias of aliases.controls; let i=index">
<!-- The repeated alias template -->
<label for="alias-{{ i }}">Alias:</label>
<input id="alias-{{ i }}" type="text" [formControlName]="i">
</div>
</div>
*ngFor 指令对 aliases FormArray 提供的每个 FormControl 进行迭代。因为 FormArray 中的元素是匿名的,所以你要把索引号赋值给 i 变量,并且把它传给每个控件的 formControlName 输入属性。
每当新的 alias 加进来时,FormArray 的实例就会基于这个索引号提供它的控件。这将允许你在每次计算根控件的状态和值时跟踪每个控件。