angular中的表单
使用angular挺长一段时间了,在关于表单的应用方面一直都在使用ng-zorro封装好的表单,至于他的基本概念以及相应的拓展都没有去详细了解,今天趁着空闲研究了一下form表单究竟是个什么东西,以及如何构建一个符合我们需求的自定义校验。
参考:《Angular权威教程》第五章 Angular中的表单,Angular官网
1.表单——既重要又复杂
在web应用中,表单是一个重要的部分。虽然我们常从点击链接或移动鼠标中得到事件的通知,但大多数“富数据“都是通过表单从用户那里获取的。
表面看起来表单只是简单的创建一个input标签,用户填入数据然后提交即可。但事实证明。表单最终可能是非常复杂的。原因如下:
- 表单输入意味着需要在页面和服务器端同时修改这份数据;
- 修改的内容通常要在页面的其他地方反映出来;
- 用户的输入可能存在很多问题,所以需要验证输入的内容
- 用户界面需要清晰的显示出可能出现的预期效果和错位信息;
- 字段之间的依赖可能存在复杂的业务逻辑
值得庆幸的是,Angular已经给出了上述所有问题的解决方案。
- 表单控件(Form Control)封装了表单中的输入,并提供了一些可供操纵的对象
- 验证器(validator)让我们能以自己喜欢的任何方式验证表单输入
- 观察者(observer)让我们能够监听表单的变化,并作出相应的回应
2.FormControl和FormGroup
FormControl和FormGroup是angular中两个最基本的表单对象
FormControl代表单一的输入字段,它是Angular表单中最小单员。FormControl封装了这些字段的值和状态,比如是否有效、是否脏(被修改过)或是否有错误等。
ngOnInit() {
// 创建一个新的FormControl对象并将其值设置成“nate”
const nameControl = new FormControl('nate');
const name = nameControl.value; // nate
console.log('nameControl', nameControl);
console.log('value', nameControl.value);
console.log('valid ', nameControl.valid); // true
console.log('dirty', nameControl.dirty); // false
console.log('errors', nameControl.errors);
}
我们通过打印FormControl对象会看到他还有很多属性,我们这里只讲这几个常用的属性,至于其他属性你们可以查看官网Angular-FormControl前去了解(这里可以注意一下errors的返回类型,后面自定义校验会用到)
FormGroup大多数表单都拥有不止一个字段,因此我们需要某种方式来管理多个FormControl。假设我们要检查表单的有效性。如果要遍历这个FormControl数组并检查每一个FormControl是否有效,必然相当繁琐;而FormGroup则可以为一组FormControl提供总包接口(wrapper interface),来解决这种问题。
ngOnInit() {
const personInfo = new FormGroup({
firstName: new FormControl("Nate"),
lastName: new FormControl("Murray"),
zip: new FormControl("90210")
})
console.log('personInfo', personInfo);
console.log('value', personInfo.value);
console.log('valid ', personInfo.valid); // true
console.log('dirty', personInfo.dirty); // false
console.log('errors', personInfo.errors);
}
FormGroup 和 FormControl 都继承自同一个祖先AbstractControl(这是FormControl
,FormGroup
和FormArray
的基类)。这意味检查 personInfo 的状态或值就像检查单个FormControl那么容易。
注意,当我们试图从FormGroup中获取value时,会收到一个“键值对”结构的对象。它能让我们从表单中一次性获取全部的值而无需逐一遍历FormControl,使用起来相当顺手。
3.创建表单
假设我们创建一个商品名称的表单,这个表单非常简单只有一个带(lable)的输入框和一个提交按钮。
<div>
<h2>基础表单:商品名称</h2>
<form #f="ngForm" (ngSubmit)="onSubmit(f.value)">
<div class="sku">
<label for="skuInput">商品名称:</label>
<input
type="text"
id="skuInput"
placeholder="商品名称"
name="sku"
ngModel
/>
</div>
<button>提交</button>
</form>
</div>
加载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 {}
FormsModule为我们提供了一些模板驱动的指令,例如:
ReactiveFormsModule则提供了下列指令:
我们导入了FormsModule,因此可以在视图中使用NgForm了。记住,当这些指令在视图中可用时,它就会被附加到任何能匹配其selector的节点上。
NgForm做了一件便利但隐晦的工作:它的选择器包含form 标签(而不用显式添加ngForm属性)。这意味着当我们导入FormsModule时候,NgForm就会被自动附加到视图中所有的标签上。
NgForm给我们提供了两个重要的功能:
(1) 一个名叫ngForm的FormGroup对象;
(2) 一个输出事件(ngSubmit)。
我们在视图的标签中同时用到了它们两个。
<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方法。
NgModel
NgModel指令指定的selector是ngModel。这意味着我们可以通过添加这个属性把它附加到
input标签上:ngModel=“whatever”。在这里我们指定了一个不带属性值的ngModel。
有两种不同的方法能在模板中指定ngModel,这里是第一种。当使用不带属性值的ngModel 时,我们是要指定:
(1) 单向数据绑定;
(2) 希望在表单中创建一个名为name的FormControl(这个name来自input标签上的name属性)。
NgModel会创建一个新的FormControl对象,把它自动添加到父FormGroup上(这里也就是form表单对象),并把这个FormControl对象绑定到一个DOM上。也就是说,它会在视图中的input标签和FormControl对象之间建立关联。这种关联是通过name属性建立的,在本例中是"name"。
注意:
- NgModel(Pascal命名法),指的是类和供代码中引用的对象。
- ngModel(首字母小写驼峰命名法),来自指令的选择器 selector,并且只会被用在DOM/模板中。
NgModel和FormControl并不是同一个,NgModel是用在视图中的指令, 而FormControl则用来表示表单中的数据和验证规则。
4.FormBuilder(响应表单)
通过上面的学习,我们知道使用ngForm和那个Control构建FormControl和FormGroup很方便,但是这却无法为我们提供更多的定制化选项。下面我们就一起来学习一下FormBuilder。
FormBuilder是一个名副其实的表单构建助手。(我们可以把他看作一个“工厂”对象)。
让我们在先前的例子中添加一个FormBuilder,看看:
- 如何在组件定义类中使用FormGroup;
- 如何在视图表单中使用自定义的FormGroup。
我们将使用formGroup和formControl指令来构建这个组件,这意味着我们需要导入相应的类。
import {
FormBuilder,
FormGroup
} from '@angular/forms';
使用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"。
我们需要将它绑定到表单元素上。
在视图中使用myForm
我们希望修改标签,让它使用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会自动应用于元素上。但其实有一个例外:NgForm不会应用到带formGroup属性的节点上。
这是因为NgForm的selector是:
form:not([ngNoForm]):not([formGroup]),ngForm,[ngForm]
这意味着你还可以使用 ngNoForm 属性产生一个不带NgForm的表单。
将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。
需要记住以下两点。
- 如果想隐式创建新的FormGroup和FormControl,使用:
- ngForm
- ngModel
- 如果要绑定一个现有的FormGroup和FormControl,使用:
- formGroup
- formControl
5.添加验证
用户输入的数据格式并不总是正确的。如果有人输入错误的数据格式,我们希望给他反馈。并阻止他提交表单。因此,我们要用到验证器。
验证器由validators模块提供。Validators.required是最简单的验证,表明指定的字段是必填项,否则就认为FormControl是无效的。
(1) 为FormControl对象指定一个验证器;
(2) 在视图中检查验证器的状态,并据此采取行动。
要 为 FormControl对 象 分 配 一 个 验 证 器 , 我 们 可 以 直 接 把 它 作 为 第 二 个 参 数 传 给FormControl的构造函数。
const control = new FormControl('name', Validators.required);
像这个例子中一样通过如下语法使用FormBuilder:
constructor(fb: FormBuilder) {
this.myForm = fb.group({
'name': ['', Validators.required]
});
this.name = this.myForm.controls['name'];
}
(1)我们可以显示地把name这个FormControl赋值给类的实例变量。虽然这有点啰嗦,但是便于我们在视图中访问这个FormControl。
(2)我们也可以在myForm中查找name这个FormCOntrol。这样能简化组件类中的工作,但在视图中稍微麻烦些。
为了说明两者之间的差异,我们来看两个例子。
把商品设置成实列变量并显示
在视图中,处理单个FormControls的最灵活的方式是将每个FormControl都定义在组件类上。把商品名称和料号定义在组件类上的代码如下所示。
export class NonInWarehouseComponent implements OnInit {
myForm: FormGroup;
name: AbstractControl;
constructor(fb: FormBuilder) {
this.myForm = fb.group({
name: ['牛奶', [Validators.required, Validators.pattern('^123')]],
code: ['', [Validators.required, Validators.pattern('^[A-Za-z0-9]*$')]],
});
this.name = this.myForm.controls.name;
}
ngOnInit() {
const nameControl = new FormControl('nate');
console.log('nameControl', nameControl);
}
onSubmit(a: any) {
console.log('a', a);
}
}
注意:
(1)我们在类的顶部设置name:AbstractControl;
(2)我们把用FormBuilder创建的myForm赋值给this.name变量。
这意味着我们可以在组件视图中到处引用name。不过这样做有一个缺点:我们不得不为表单中的每个字段定义一个实体变量。对大型表单而言,这会使得相当啰嗦。
现在我们的name可以得到验证了,我们要以四种不同的方式把它用在视图中:
(1)检查整个表单的有效性并显示一条错误信息;
(2)检查单个字段的有效性并显示一条错误信息;
(3)检查单个字段的有效性,当字段无效时将字段显示为红色;
(4)检查单个字段在特定规则下的有效性并显示一条错误信息。
-
表单信息
我们可以通过myForm.valid来检查整个表单的有效性。myForm是一个FormGroup;只有当里面所有的FormGroup都有效时,这个FormGroup才有效。
-
字段信息
当字段的FormControl无效时,我们也可以为该字段显示一条错误信息。 -
特定验证
可能有很多原因导致一个表单字段无效。对于失败的验证,我们通常希望根据不同的原因显示不同的信息。我们可以用hassError方法来检查特定的验证失败。
注意,FormControl和FormGroup都定义了hasError方法。这意味着我们可以给他传入第二个参数path来在FormGroup中查询特定的字段。比如:
完整代码
template:`<div>
<h2>商品表单:商品名称</h2>
<form [formGroup]="myForm" (ngSubmit)="onSubmit(myForm)">
<div>
<label for="nameInput">商品名称:</label>
<input
type="text"
id="nameInput"
placeholder="请输入名称"
[formControl]="myForm.controls['name']"
/>
<div style="color:red" *ngIf="!name.valid">
名称无效
</div>
<div style="color:red" *ngIf="name.hasError('textinvalid')">
名称不是以“123”开头
</div>
<div *ngIf="name.dirty">
数据已变动
</div>
</div>
<div>
<label for="codeInput">商品料号:</label>
<input
type="text"
id="codeInput"
placeholder="请输入料号"
[formControl]="myForm.controls['code']"
/>
<div
style="color:red"
*ngIf="myForm.controls.code.hasError('required')"
>
该项必填
</div>
<div
style="color:red"
*ngIf="myForm.controls.code.hasError('pattern')"
>
只可输入数字和英文
</div>
</div>
<div style="color:green" *ngIf="myForm.isvalid">
表单无效
</div>
<div style="color:green" *ngIf="myForm.valid">
表单有效
</div>
<button type="submit">提交</button>
</form>
</div>`
export class NonInWarehouseComponent implements OnInit {
myForm: FormGroup;
name: AbstractControl;
constructor(fb: FormBuilder) {
this.myForm = fb.group({
name: ['牛奶', Validators.compose([Validators.required, textValidator])],
code: ['', [Validators.required, Validators.pattern('^[A-Za-z0-9]*$')]],
});
this.name = this.myForm.controls.name;
}
ngOnInit() {
const nameControl = new FormControl('nate');
console.log('nameControl', nameControl);
}
onSubmit(a: any) {
console.log('a', a);
}
}
大多数情况下,我们并不希望为每一个AbstractControl控件都创建一个实例变量。在没有实例变量的情况下,我们可以通过myForm.controls属性
6.自定义验证器
我们经常需要根据需求写一些自定义的验证器,下面我们来看看如何实现。
要明白如何实现自己的验证器,不妨看看angular源代码中是如何实现Validators.required的:
export class Validators {
static required(control: AbstractControl): ValidationErrors | null;
}
export declare type ValidationErrors = {
[key: string]: any;
};
一个验证器:
- 接收一个AbstractControl对象作为输入;
- 当验证器失败时,会返回一个String Map<string,any>对象,他的键是”错误代码“,它的值是true。
编写验证器
假设我们的name有特殊的验证需求,比如name必须以123作为开始。我们写的验证器是这样的:
function textValidator(
controls: FormControl // 因为FormControl继承于AbstractControl所以也可以写成FormControl对象
): {
[s: string]: boolean;
} {
if (!controls.value.match(/^123/)) {
return { textinvalid: true };
}
}
当输入值(控件的值control.value)不是以123作为开始时,验证器会返回错误代码invalidSku。
给FormControl分配验证器
现在要为Form Control添加验证,但是一个小问题:name已经有一个验证器了,怎样才能在同一个字段上添加多个验证器。
我们可以用Validators.compose来实现。
Validators.compose([Validators.required, textValidator])
// 不用compose时
[Validators.required, textValidator]
注意:保留compose是为了向以前历史版本进行兼容,现在不用compose也可实现。
Validators.compose把两个验证器包装在一起,我们可以将其赋值给FormControl。只有当两个验证器都合法时,FormControl才是合法的。