FormControl和FormGroup是Angular中两个最基础的表单对象。
1. FormControl
FormControl代表单一的输入字段,它是Angular表单中的最小单元。
FormControl封装了这些字段的
值和状态
,比如是否有效、是否脏(被修改过)或是否有错
误等。
比如,下列代码演示了如何在TypeScript中使用FormControl:
// create a new FormControl with the value "Nate"
let nameControl = new FormControl("Nate");
let name = nameControl.value; // -> Nate
// now we can query this control for certain values:
nameControl.errors // -> StringMap<string, any> of errors
nameControl.dirty // -> false
nameControl.valid // -> true
// etc.
为了构建表单,我们会创建几组FormControl对象,然后为它们附加元数据和逻辑。
在Angular中,我们经常将一个类(本例中为FormControl)以属性形式(本例中为formControl)附加在DOM上。比如下面这个表单:
<!-- part of some bigger form -->
<input type="text" [formControl]="name" />
这会在此form的上下文中创建一个新的FormControl对象。
2. FormGroup
大多数表单都拥有不止一个字段,因此我们需要某种方式来管理多个FormControl。假设我们要检查表单的有效性。如果要遍历这个FormControl数组并检查每一个FormControl是否有效,必然相当繁琐;而FormGroup则可以为一组FormControl提供总包接口(wrapper interface),来解决这种问题。
下面是FormGroup的创建方式:
let personInfo = new FormGroup({
firstName: new FormControl("Nate"),
lastName: new FormControl("Murray"),
zip: new FormControl("90210")
})
FormGroup 和 FormControl 都继承自同一个祖先AbstractControl。这意味检查 personInfo 的状态或值就像检查单个FormControl那么容易:
// firstName: "Nate",
// lastName: "Murray",
// zip: "90210"
// }
// now we can query this control group for certain values, which have sensible
// values depending on the children FormControl's values:
personInfo.errors // -> StringMap<string, any> of errors
personInfo.dirty // -> false
personInfo.valid // -> true
// etc.
注意
,当我们试图从FormGroup中获取value时,会收到一个“键值对”结构的对象。它能让我们从表单中一次性获取全部的值而无需逐一遍历FormControl,使用起来相当顺手。
3. 创建表单
我们要构建一个表单,效果如图:
3.1 加载 FormsModule
为了使用这个新的表单库,先要确保我们的NgModule中导入了这个表单库。
Angular中有两种使用表单的方式,我们在本章中都会展开讨论:使用FormsModule以及使用ReactiveFormsModule。既然都要用到,那么这个模块就同时导入它们。因此需要在引用启动程序app.ts中这样写:
import {
FormsModule,
ReactiveFormsModule
} from '@angular/forms';
// farther down...
@NgModule({
declarations: [
FormsDemoApp,
DemoFormSku,
// ... our declarations here
],
imports: [
BrowserModule,
FormsModule, // <-- add this
ReactiveFormsModule // <-- and this
],
bootstrap: [ FormsDemoApp ]
})
class FormsDemoAppModule {}
这确保了我们能在视图中使用Angular表单指令。
FormsModule为我们提供了一些模板驱动的指令,例如:
- ngModel
-
NgForm
ReactiveFormsModule则提供了下列指令: - formControl
- ngFormGroup
3.2 简易SKU 表单:template
template: `
<div class="ui raised segment">
<h2 class="ui header">Demo Form: Sku</h2>
<form #f="ngForm"
(ngSubmit)="onSubmit(f.value)"
class="ui form">
<div class="field">
<label for="skuInput">SKU</label>
<input type="text"
id="skuInput"
placeholder="SKU"
name="sku" ngModel>
</div>
<button type="submit" class="ui button">Submit</button>
</form>
</div>
`
3.2.1. form和NgForm
我们导入了FormsModule,因此可以在视图中使用NgForm了。记住,当这些指令在视图中可用时,它就会被附加到任何能匹配其selector的节点上。
NgForm做了一件便利但隐晦的工作:它的选择器包含form 标签(而不用显式添加ngForm属性)。这意味着当我们导入FormsModule时候,NgForm就会被自动附加到视图中所有的<form>标签上。这确实非常有用,但由于它发生在幕后,也许会让很多人感到困惑。
NgForm给我们提供了两个重要的功能:
(1) 一个名叫ngForm的FormGroup对象;
(2) 一个输出事件(ngSubmit)。
我们在视图的<form>标签中同时用到了它们两个。
<form #f="ngForm"
(ngSubmit)="onSubmit(f.value)"
首先,我们使用了#f=”ngForm”。
#v=thing
语法的意思是,我们希望在当前视图中创建一个局部变量。
这里我们为视图中的ngForm创建了一个别名,并绑定到变量#f。这个ngForm来自哪里呢?它是由NgForm指令导出的。
ngForm是什么类型的对象呢?它是FormGroup类型的。这意味着我们可以在视图中把变量 f 当作FormGroup使用,而这也正是我们在输出事件(ngSubmit)中的使用方法。
我们在表单中绑定ngSubmit事件的语法是:(ngSubmit)=”onSubmit(f.value)”。
- (ngSubmit):来自NgForm指令。
- onSubmit():将会在我们的组件类中进行定义(稍后)。
- f.value:f就是我们前面提到的FormGroup,而.value会以键值对的形式返回FormGroup中所有控件的值。
总结起来,这行代码的意思是:“当我提交表单时,将会以该表单的值作为参数,调用组件实例上的onSubmit方法。”
3.2.2. input和NgModel
<input type="text"
id="skuInput"
placeholder="SKU"
name="sku" ngModel>
NgModel指令指定的selector是ngModel。这意味着我们可以通过添加这个属性把它附加到
input标签上:
ngModel=”whatever”
。在这里我们指定了一个不带属性值的ngModel。
有两种不同的方法能在模板中指定ngModel,这里是第一种。当使用不带属性值的ngModel 时,我们是要指定:
(1) 单向数据绑定;
(2) 希望在表单中创建一个名为sku的FormControl(这个sku来自input标签上的name属性)。
NgModel会创建一个新的FormControl对象,把它自动添加到父FormGroup上(这里也就是form表单对象),并把这个FormControl对象绑定到一个DOM上。也就是说,它会在视图中的input标签和FormControl对象之间建立关联。这种关联是通过name属性建立的,在本例中是”sku”。
注意:
- NgModel,指的是类和供代码中引用的对象。
- ngModel,来自指令的选择器 selector,并且只会被用在DOM/模板中。
NgModel和FormControl并不是同一个,NgModel是用在视图中的指令, 而FormControl则用来表示表单中的数据和验证规则。
3.3. 简易 SKU 表单:组件定义类
import { Component } from '@angular/core';
@Component({
selector: 'demo-form-sku',
template: `
<div class="ui raised segment">
<h2 class="ui header">Demo Form: Sku</h2>
<form #f="ngForm"
(ngSubmit)="onSubmit(f.value)"
class="ui form">
<div class="field">
<label for="skuInput">SKU</label>
<input type="text"
id="skuInput"
placeholder="SKU"
name="sku" ngModel>
</div>
<button type="submit" class="ui button">Submit</button>
</form>
</div>
`
})
export class DemoFormSku {
onSubmit(form: any): void {
console.log('you submitted value:', form);
}
}
4. 使用 FormBuilder
FormBuilder是一个名副其实的表单构建助手。你应该还记得,表单是由FormControl和FormGroup构成的,而FormBuilder则可以帮助我们创建它们(你可以把它看作一个“工厂”对象)。
让我们在先前的例子中添加一个FormBuilder,看看:
- 如何在组件定义类中使用FormGroup;
- 如何在视图表单中使用自定义的FormGroup。
我们将使用formGroup和formControl指令来构建这个组件,这意味着我们需要导入相应的类。
import {
FormBuilder,
FormGroup
} from '@angular/forms';
4.1. 使用 FormBuilder
通过在组件类上声明带参数的constructor,我们注入了一个FormBuilder。
export class DemoFormSkuBuilder {
myForm: FormGroup;
constructor(fb: FormBuilder) {
this.myForm = fb.group({
'sku': ['ABC123']
});
}
onSubmit(value: string): void {
console.log('you submitted value: ', value);
}
}
Angular将会注入一个从FormBuilder类创建的对象实例,并把它赋值给fb变量(来自构造函数)。
我们将会使用FormBuilder中的两个主要函数:
- control,用于创建一个新的FormControl;
- group,用于创建一个新的FormGroup。
myForm是FormGroup类型。我们通过调用fb.group()来创建FormGroup。.group方法的参数是代表组内各个FormControl的键值对。
在这里,我们设置了一个名为sku的控件,其值为[“ABC123”]——意思是控件的默认值为”ABC123″。
我们需要将它绑定到表单元素上。
4.2 在视图中使用myForm
我们希望修改<form>标签,让它使用myForm变量。回忆一下,在上一节中我们提到过,当导入FormsModule时,ngForm就会自动起作用。还提到过ngForm会自动创建它自己的FormGroup。但在这里我们不希望使用外部的FormGroup,而是使用FormBuilder创建的这个myForm实例变量。那该怎么做呢?
Angular提供了另一个指令,能让我们使用现有的FormGroup。它叫作formGroup,可以这样使用。
<h2 class="ui header">Demo Form: Sku with Builder</h2>
<form [formGroup]="myForm"
这里我们告诉Angular,想用myForm作为这个表单的FormGroup。
注意:
当使用FormsModule时,NgForm会自动应用于<form>元素上。但其实有一个例外:NgForm不会应用到带formGroup属性的<form>节点上。
你也许不明白原因,这是因为NgForm的selector是:
form:not([ngNoForm]):not([formGroup]),ngForm,[ngForm]
这意味着你还可以使用
ngNoForm
属性产生一个不带NgForm的<form>表单。
将FormControl绑定到input标签上。记住,ngModel会创建一个新的FormControl对象,并附加到父FormGroup中。但在这个例子中,我们已经用FormBuilder创建了自己的FormControl。
要将现有的FormControl绑定到input上,可以用
formControl
。
<label for="skuInput">SKU</label>
<input type="text"
id="skuInput"
placeholder="SKU"
[formControl]="myForm.controls['sku']">
我 们 将 input标 签 上 的 formControl指 令 指 向 了 myForm.controls上 现 有 的FormControl控件sku。
完整代码
import { Component } from '@angular/core';
import {
FormBuilder,
FormGroup
} from '@angular/forms';
@Component({
selector: 'demo-form-sku-builder',
template: `
<div class="ui raised segment">
<h2 class="ui header">Demo Form: Sku with Builder</h2>
<form [formGroup]="myForm"
(ngSubmit)="onSubmit(myForm.value)"
class="ui form">
<div class="field">
<label for="skuInput">SKU</label>
<input type="text"
id="skuInput"
placeholder="SKU"
[formControl]="myForm.controls['sku']">
</div>
<button type="submit" class="ui button">Submit</button>
</form>
</div>
`
})
export class DemoFormSkuBuilder {
myForm: FormGroup;
constructor(fb: FormBuilder) {
this.myForm = fb.group({
'sku': ['ABC123']
});
}
onSubmit(value: string): void {
console.log('you submitted value: ', value);
}
}
需要记住以下两点。
- 如果想隐式创建新的FormGroup和FormControl,使用:
- ngForm
- ngModel
- 如果要绑定一个现有的FormGroup和FormControl,使用:
- formGroup
- formControl
5. 添加验证
想使用验证器,我们得做两件事:
(1) 为FormControl对象指定一个验证器;
(2) 在视图中检查验证器的状态,并据此采取行动。
要 为 FormControl对 象 分 配 一 个 验 证 器 , 我 们 可 以 直 接 把 它 作 为 第 二 个 参 数 传 给FormControl的构造函数。
let control = new FormControl('sku', Validators.required);
像这个例子中一样通过如下语法使用FormBuilder:
constructor(fb: FormBuilder) {
this.myForm = fb.group({
'sku': ['', Validators.required]
});
this.sku = this.myForm.controls['sku'];
}
6. 监听变化
到目前为止,我们只在提交表单时才调用onSubmit方法来获取表单的值。但我们也要经常监听控件的变化。
FormGroup和FormControl都带有EventEmitter(事件发射器),我们可以通过它来观察变化。
想监听控件的变化,我们要:
(1) 通过调用control.valueChanges访问到这个EventEmitter;
(2) 然后使用.subscribe方法添加一个监听器。
下面是一个例子。
constructor(fb: FormBuilder) {
this.myForm = fb.group({
'sku': ['', Validators.required]
});
this.sku = this.myForm.controls['sku'];
this.sku.valueChanges.subscribe(
(value: string) => {
console.log('sku changed to:', value);
}
);
this.myForm.valueChanges.subscribe(
(form: any) => {
console.log('form changed to:', form);
}
);
}
在这里我们监听了两个事件:sku字段的变化和整个表单的变化。
我们传递了一个带有next键的对象(也可以传递其他键,但目前还不用关心它们)。next就是我们希望当值发生变化时被调用的函数。
如果在输入框中输入kj,就会在控制台中看到:
sku changed to: k
form changed to: Object {sku: "k"}
sku changed to: kj
form changed to: Object {sku: "kj"}
如你所见,每一次按键都会触发控件的变化,我们的可观察对象也会被触发。监听单个FormControl时,我们会得到一个值(例如kj);而监听整个表单时,我们会得到一个包含键值对的对象(例如{sku: “kj”})。
7. ngModel
ngModel是一个特殊的指令,它将模型绑定到表单中。ngModel的特殊之处在于它实现了
双向绑定
。
相对于单向绑定来说,双向绑定更加复杂和难以推断。Angular通常的数据流向是单向的:自顶向下。但对于表单来说,双向绑定有时会更容易。
下面对表单稍作修改:我们希望能输入产品名称productName。这次要用ngModel来保持组件实例和视图的同步。
我们的组件定义类如下所示。
export class DemoFormNgModel {
myForm: FormGroup;
productName: string;
constructor(fb: FormBuilder) {
this.myForm = fb.group({
'productName': ['', Validators.required]
});
}
onSubmit(value: string): void {
console.log('you submitted value: ', value);
}
}
注意
,我们只是简单地将productName: string存成了实例变量。
紧接着,我们在input标签上使用ngModel。
<label for="productNameInput">Product Name</label>
<input type="text"
id="productNameInput"
placeholder="Product Name"
[formControl]="myForm.get('productName')"
[(ngModel)]="productName">
注意
,这里ngModel的语法很有意思:我们在ngModel属性上同时使用了()和[]。我们既使用了表示输入属性(@Input)的方括号[],又使用了表示输出属性(@Output)的圆括号(),这就是
双向绑定的标志
。
另 外 还 需 要 注 意 的 是 : 我 们 仍 然 用 formControl指 定 此 input应 该 绑 定 到 表 单 上 的FormControl。这是因为ngModel只负责将input绑定到对象实例上,但FormControl的功能是与此独立的。由于我们还需要对这个值加以验证并把它作为表单的一部分提交上去,仍要保留formControl指令。