Angular 表单(一)

Angular 为表单处理提供了丰富的支持,超越了常规的数据绑定,将表单处理作为顶级特性进行了专门的功能设计和开发。

HTML 表单

HTML 提供了表单基本功能:例如显示表单项、校验用户输入、提交表单数据等。但 HTML 表单无法很好地支持真实的业务,我们希望有更好的工具,帮助用户输入的数据、自定义校验规则、显示用户友好信息以及选择数据提交到服务器的方式等。

只使用浏览器标准功能的 HTML 表单:

1
2
3
4
5
6
7
<form action="/regist" method="post">
<div>用户名:<input type="text"></div>
<div>手机号:<input type="text"></div>
<div>密码:<input type="password"></div>
<div>确认密码:<input type="password"></div>
<button type="submit">注册</button>
</form>

注:不要启动 Angular2 服务器,直接打开本地文件。

由于没有加入任何校验,当我们点击“注册”按钮时,表单数据会被 HTTP POST 请求提交到服务器。

而我们希望:

  • 每一个输入字段独立校验规则
  • 如果用户不符合校验规则,显示提示信息
  • 依赖的字段一起校验
  • 应用提供提交到服务器的值,当用户点击注册按钮时,应该调用事件处理方法,并将表单的值传给这个事件处理方法。方法可以在表单发送数据之前,校验合法性或改变数据格式。
  • 应用应该可以控制数据如何被发送到数据库,可以是一个 HTTP 请求,也可以是 Ajax 请求,也可以是 websocket 消息。

HTML可以部分满足前两个需求,例如 requiredmaxlengthpattern 等:

1
<div>用户名:<input type="text" required patter="[a-zA-Z0-9]+"></div>

但会存在一些问题:

  • 消息过于模糊
  • 每隔一小段时间或输入框失去焦点时,消息消失
  • 消息的样式无法自定义

定义正确的输入类型可以提供更好的用户体验,例如:

1
<div>邮政编码:<input type="number" min="100000" max="999999"></div>

但这样的写法仍然存在问题,并不一定每个 100000-999999 区间的数字都是邮政编码。然而真实场景中,我们可能会有更复杂的需求,例如河北省的用户只能输入河北的邮政编码等。

Angular 表单

Angular 提供了两种表单处理的方式,一种是模版的处理方式,另一种是响应式编程的方式,这两种方式表现为不同的 API。

1、模版式表单

表单的数据模型是通过组件模版中的相关指令来定义的,因为使用这种方式定义表单的数据模型时会受限于 HTML 的语法,所以,模版驱动方式只适合于一些简单的场景。

2、响应式表单

响应式表单是通过编写 TypeScript 代码而不是 HTML 的代码来创建一个底层的数据模型,在这个模型定义好后使用一些特定的指令,将模版上的 HTML 元素与底层的数据模型连接在一起。

不管是哪种表单,都有一个对应的数据模型来存储表单的数据。在模版式表单中,数据模型是由 Angular 给予你组件模版中的指令隐式创建的,而在
响应式表单中,你通过编码明确地创建数据模型然后将模版上的 HTML 元素与底层的数据模型连接在一起。

数据模型并不是一个任意的对象,它是由 angular/forms 模块中的一些特定的类,如 FormContol, FormGroup, FormArray 等组成的。在模版式表单中,你是不能直接访问到这些类的。

响应式表单并不会替你生成 HTML,模板仍然需要你自己来编写。

无论哪种表单,都需要引入相应的 Module:

模版式表单:FormsModule
响应式表单:ReactiveFormsModule

模版式表单

表单的数据模型只能通过组件模版中的相关「指令」来定义,这些指令包括 NgFormNgModelNgModelGroup,这些指令均来自于 FormsModule

NgForm 指令代表整个表单,会被自动添加到每一个 <form> 标签上,并隐式地创建一个 FormGroup 类的实例,这个类代表表单的数据模型,并且存储数据。

如何自动添加到 <form> 标签上呢?

1
ng g component templateForm

template-form.component.html

1
2
3
4
5
6
7
8
<form action="/createUser" method="post">
<div>用户名:<input type="text" required patter="[a-zA-Z0-9]+"></div>
<div>手机号:<input type="text"></div>
<div>邮政编码:<input type="number" min="100000" max="999999"></div>
<div>密码:<input type="password"></div>
<div>确认密码:<input type="password"></div>
<button type="submit">注册</button>
</form>

与本地打开会被 post 提交不同,开启 Angular 服务后,点击“注册”按钮是没有任何反应的,因为 Angular 接管了相关的操作。

NgForm 指令

NgForm 指令会自动创建一个 NgGroup 对象,它的子元素中含有 NgModel 指令的值会被添加到表单数据模型中:

  • NgForm 标签可以在 <form> 标签以外使用,例如:<div ngForm>,这与创建一个 <form> 表单的效果是完全相同的。如果不希望用 Angular 自动处理表单,可以 <form ngNoForm>,这样 angular 不再接管相关操作。
  • 通过创建模板本地变量引用,NgForm 表单可以被引用。
1
2
3
4
5
6
7
<form #myForm="ngForm" action="/createUser" method="post">
...
</form>

<div>
{{myForm.value}}
</div>

声明了一个 myForm 模板变量,通过它访问 ngForm 对象的属性,例如 valuevalue 属性是一个 js 对象,保存当前表单所有对象的值。

如果你希望把每个字段显示在页面上,可以将这个对象传给 Angular 的 json 管道。

1
2
3
<div>
{{myForm.value | json}}
</div>

然后我们在页面中输入一些内容,会发现并没有同步输出:

这是因为 ngForm 只会将标有 ngModel 的子元素的值添加到数据模型中。我们为一个”用户名”这个子元素添加 ngModel

1
2
3
4
5
6
7
8
<form #myForm="ngForm" action="/createUser" method="post" ngForm>
<div>用户名:<input ngModel name="nickname" type="text" required patter="[a-zA-Z0-9]+"></div>
...
</form>

<div>
{{myForm.value | json}}
</div>

结果显而易见:

刚才我们注意到,点击“注册”按钮页面是没有任何反应的。这是因为提交会导致页面刷新,而 SPA 应用不能让页面刷新,因此 Angular 会拦截 HTML 的表单提交事件,而使用 ngSubmit 事件来代替:

template-form.component.html

1
2
3
4
<form #myForm="ngForm" (ngSubmit)="createUser(myForm.value)">
...
</form>
...

template-form.component.ts

1
2
3
4
5
6
export class TemplateFormComponent implements OnInit {
...
createUser(info: any){
console.log(info);
}
}

然后我们在页面中进行输入,然后点击“注册”,会发现后台打出相应字段的内容:

NgModel 指令

NgModel 指令代表表单的一个字段,它会隐式地创建一个 FormControl 实例代表字段的数据模型,FormControl 对象存储字段的值。

使用 NgModel 指令时,不需要使用 ()、[] 等将 NgModel 括起来,它不是一个双向绑定,而是表单字段的指令。也不需要绑定到组件的属性上,但需要为添加 NgModelHTML 元素为它指定一个 name 属性和属性值。这个属性会成为 NgForm 对象 value 属性对应的对象中的属性。

NgForm 类似,NgModel 也可以通过模版变量来进行引用。

1
2
3
4
5
6
7
8
<form #myForm="ngForm" action="/createUser" method="post" (ngSubmit)="createUser(myForm.value)">
<div>用户名:<input #myNickName="ngModel" ngModel name="nickname" type="text" required patter="[a-zA-Z0-9]+"></div>
...
</form>

<div>
昵称的值是:{{myNickName.value | json}}
</div>

NgModelGroup 指令

NgModelGroup 代表表单的一部分,它允许将表单的一部分组织在一起,使关系层次更清晰。

NgForm 相同,NgModelGroup 也会创建一个 NgGroup 类型的对象。它会在 NgForm 的 value 属性中,表现为一个嵌套对象。在这个对象中,我们也可以对其它字段加到嵌套的对象中。

最终写法:

1
2
3
4
5
6
7
8
9
10
<form #myForm="ngForm" (ngSubmit)="createUser(myForm.value)">
<div>用户名:<input ngModel name="nickname" type="text" required patter="[a-zA-Z0-9]+"></div>
<div>邮箱:<input ngModel name="email" type="email"></div>
<div>手机号:<input ngModel name="mobile" type="number"></div>
<div ngModelGroup="passwordInfo">
<div>密码:<input ngModel name="password" type="password"></div>
<div>确认密码:<input ngModel name="passwordComfirm" type="password"></div>
</div>
<button type="submit">注册</button>
</form>

响应式表单

响应式表单是通过编写 TypeScript 代码而不是 HTML 的代码来创建一个底层的数据模型,在这个模型定义好后使用一些特定的指令,将模版上的 HTML 元素与底层的数据模型连接在一起。

响应式表单指令

类名 属性绑定指令 属性名指令
FormGroup formGroup formGroupName
FormControl formControl formControlName
FormArray - formArrayName

这些指令全部来自于 ReactiveFormsModule 模块。第二列所示的“属性绑定指令”是指用方括号绑定的属性指令,属性名指令不需要使用属性绑定语法,它指属性的值。

所有响应式表单指令都是以 Form 开头的,而模版式表单是以 ng 开头的,因此可以通过查看模版判断是哪种表单。

注:

响应式表单指令是不可在通过在模版创建变量被引用的。模版式表单可以在模版中取到变量并进行操作,但不能在 ts 文件中拿到引用变量。与此相反,响应式表单可以在 ts 拿到引用的类的,却不能在模版中获取到变量,因此无法在模版中进行操作。

属性名指令要求该变量一定要被绑定到某个变量上,

formGroup

reactived-form.component.html

1
2
3
<form  [formGroup]="formModel" (click)="createUser()">
...
</form>

reactive-form.component.ts

1
2
3
4
5
6
7
8
9
10
...
import { FormGroup } from '@angular/forms';

@Component({
...
})
export class ReactivedFormComponent implements OnInit {
...
private formModel: FormGroup
}

[formGroup]="formModel" 表示将 formGroup 绑定到 component 中一个名为 formModel 的属性上。

在模版表单中,我们用(ngSubmit)="createUser(myForm.value)" 向后台传入数据。但在响应式表单中,createUser() 中不需要传入数据,因为 ts 无法获取引用对象。其次,我们在 ts 中可以通过 formModel: formGroup; 拿到数据。

formGroupName、formControlName和formArrayName

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<form [formGroup]="formModel" (submit)="createUser()">
<div>用户名:<input formControlName="nickname" name="nickname" type="text" required patter="[a-zA-Z0-9]+"></div>
<div>邮箱:
<ul formArrayName="emails">
<li *ngFor="let email of formModel.get('emails').controls;let i = index">
<input [formControlName]="i">
</li>
</ul>
<button type="button" (click)="addEmail()">增加 email</button>
</div>
<div>手机号:<input formControlName="mobile" name="mobile" type="number"></div>
<div formGroupName="passwordInfo">
<div>密码:<input formControlName="password" name="password" type="password"></div>
<div>确认密码:<input formControlName="passwordConfirm" name="passwordComfirm" type="password"></div>
</div>
<button type="submit">注册</button>
</form>
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
26
27
28
29
30
31
32
33
34
35
36
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, FormArray } from '@angular/forms';

@Component({
...
})
export class ReactivedFormComponent implements OnInit {

private formModel: FormGroup

constructor() {
this.formModel = new FormGroup({
nickname: new FormControl(),
emails: new FormArray([
new FormControl()
]),
mobile: new FormControl(),
passwordInfo: new FormGroup({
password: new FormControl(),
passwordConfirm: new FormControl()
})
})
}

createUser(){
console.log(this.formModel.value);
}

addEmail(){
let emails = this.formModel.get('emails') as FormArray;
emails.push(new FormControl());
}

ngOnInit() {
}
}

app.module.ts

1
2
3
4
5
6
7
8
9
10
...
import { ReactiveFormsModule } from '@angular/forms'

@NgModule({
...
imports: [
...
ReactiveFormsModule
],
})

邮箱使用了 formArrayName 绑定,formModel.get('emails') 得到的是一个数组。由于其中的 FormControlName 在 ts 的模型中没有相应 key,因此需要使用方括号声明。

this.formModel.get('emails') 拿到的是 FormArrayFormControlFormGroup共有的抽象类类型,因此要强转成 FormArray 类型。

formControl

在上面的例子中,我们并没有使用 formControl 属性。我们不能将 formControlNmae 改为 formControl,是因为它和 formGroup 的用法一样,如果我们写做 [formControl]="nick" 表示将 formControl 绑定到 component 中一个名为 nick 的属性上,然而在我们的模型中并没有这样的属性。

我们可以在表单以外使用该属性,例如:

reactive-form.component.html

1
<input [formControl]="xxx">

reactive-form.component.ts

1
2
3
export class ReactivedFormComponent implements OnInit {
private xxx : FormControl;
}

FormBuilder

FormBuilderFormArrayFormControlFormGroup 的功能完全相同,但它简化了语法。

我们来改写上面的 ts 文件:

new FormControl() 改写为 ['']
new FormArray() 改写为 this.fb.array()
new FormGroup() 改写为 this.fb.group()

FormBuilder 除了简化语法,还提供了一些校验方法。
[''] 第一个参数是 form 初始默认值,第二个值是校验方法,第三个是异步校验方法。

reactived-form-component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export class ReactivedFormComponent implements OnInit {
...
private fb: FormBuilder = new FormBuilder();

constructor() {
this.formModel = this.fb.group({
nickname: [''],
emails: this.fb.array([
['']
]),
mobile: [''],
passwordInfo: this.fb.group({
password: [''],
passwordConfirm: ['']
})
})
}