这篇文章本来早就该写完的,但由于Angular的表单模块实在太过复杂,导致断断续续摸索了近半个月,这其中又牵扯了诸多其它知识点,因此现在才勉强整理完。Angular的表单非常强大,这里会挑重点的来。
序章
知识点概览
WEB应用中,表单或许是最重要的部分,大多数的“富数据”都是通过表单从用户那里获得的。
从表面上看,创建一个input标签,填入数据,点击提交,这有什么难的?但事实上,表单处理可能是非常复杂的,常常会占据日常开发的大块编码时间。原因如下:
- 某项表单输入后是否需要在页面和服务端同时修改这项数据?
- 通过表单修改的内容往往需要在页面其它地方反映出来。
- 用户输入的内容可能会存在诸多问题,所以需要验证这些内容。
- 用户界面需要清晰地显示出表单的预期结果和错误信息。
- 表单中每个字段的依赖是否存在复杂的业务逻辑?
- 一般我们不希望通过DOM选择器来测试表单。
为此,Angular为forms模块设计了诸多API和概念,以下是主要知识点:
- FormControl:封装了表单中的输入框,并提供了一些可供操作的对象。
- FormGroup:一组FormControl,提供了一些总包接口。
- Validator:验证器,用来验证表单的输入内容。可自定义。
- FormBuilder:一个表单构建助手,用于快速创建一个响应式表单。
想要用到Angular中表单特性,首先需要在app.module.ts引入此模块:

FormsModule为我们提供了一些模板驱动的指令,例如:
ReactiveFormsModule提供的指令则主要用于响应式表单,例如:
模板式表单(模板驱动表单)
模板式表单的写法是,把表单相关的逻辑,包括校验逻辑全部写在模板里面,组件内部几乎没写什么代码。
快速上手
创建一个组件,内容如下:
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
| import { Component, OnInit } from '@angular/core';
@Component({ selector: 'form-quick-start', template: ` <input type="email" (keyup)="userNameChange($event)"> <input #pwd type="password" (keyup)="0"> <!-- 输出结果 --> <div> <p>用户名:{{userName}}</p> <p>密码:{{pwd.value}}</p> </div> ` }) export class FormQuickStartComponent implements OnInit { public userName:string;
constructor() { }
ngOnInit() { }
public userNameChange(event):void{ this.userName=event.target.value; } }
|
输出结果里的两个值都会根据输入的内容而变化。过程是这样的:
- 第一个 input:用事件绑定的方式,把input的值传递给组件内部定义的 userName 属性,然后页面上再用 获取数据。
- 第二个 input:我们定义了一个模板局部变量
#pwd,然后底部直接用这个名字来获取 input 的值 。这里有一个小小的注意点,标签里面必须写 (keyup)=”0”,要不然 Angular 不会启动变更检测机制, 取不到值。
ngModel与双向绑定
快速上手中的例子实际上没有用到任何Angular的forms模块。下面将会改造上述代码,使用表单模块的ngModel来实现双向数据绑定。
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
| import { Component, OnInit } from '@angular/core';
@Component({ selector: 'form-quick-start', template: ` <input type="email" [(ngModel)]="regModel.email" name="email"> <input type="password" [(ngModel)]="regModel.password" name="password"> <input type="checkbox" name="rememberMe" [(ngModel)]="regModel.rememberMe">记住我 <!-- 输出结果 --> <div> <p>用户名:{{regModel.email}}</p> <p>密码:{{regModel.password}}</p> <p>记住我:{{regModel.rememberMe}}</p> </div> ` }) export class FormQuickStartComponent implements OnInit { public regModel:RegisterModel=new RegisterModel();
constructor() { }
ngOnInit() { } }
export class RegisterModel { email: string; password: string; rememberMe:boolean=false; }
|
需要注意的是,用双向绑定的时候,必须给 <input> 标签设置 name 或者 id,否则会报错。原因:
ngModel会给该输入框元素创建一个表单控制器(FormControl),并把这个FormControl对象绑定到DOM上。也就是说,它会在视图中的Input标签和FormControl之间建立关联,这种关联通过name属性或id属性建立。
表单上面展现的字段和处理业务用的数据模型不一定完全一致,推荐设计两个 Model,一个用来给表单进行绑定操作,一个用来处理业务。
表单校验
快速上手
继续更改上述模板式表单中的代码,把模板抽出去,组件内部保持原样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { Component, OnInit } from '@angular/core';
@Component({ selector: 'form-quick-start', templateUrl: './form-quick-start.component.html', }) export class FormQuickStartComponent implements OnInit { public regModel:RegisterModel=new RegisterModel();
constructor() { }
ngOnInit() { } }
export class RegisterModel { email: string; password: string; rememberMe:boolean=false; }
|
修改模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <form #registerForm="ngForm"> <div [ngClass]="{'has-error': userName.invalid && (userName.dirty || userName.touched) }"> <label>用户名:</label> <div> <input #userName="ngModel" [(ngModel)]="regModel.userName" name="userName" type="email" required minlength="12" maxlength="32"> <div *ngIf="userName.invalid && (userName.dirty || userName.touched)"> <div *ngIf="userName.errors.required"> 用户名不能为空 </div> <div *ngIf="userName.errors.minlength"> 最小长度不能小于12个字符 </div> <div *ngIf="userName.errors.maxlength"> 最大长度不能大于32个字符 </div> </div> </div> </div> </form>
|
效果如图:

userName输入框就是一个FormControl。
Angular为它提供了一些状态标志位(如userName.dirty)和校验规则(如minlength="12")
状态标志位
状态标志位用来判断一个或一组Angular表单控件的状态:
| 状态名称 |
描述 |
| valid |
校验成功 |
| invalid |
校验失败 |
| pending |
表单正在提交过程中 |
| pristine |
数据依然处于原始状态,用户没有修改过 |
| dirty |
数据已经变脏了,被用户改过了 |
| touched |
被触摸或者点击过 |
| untouched |
未被触摸或者点击 |
| enabled |
启用状态 |
| disabled |
禁用状态 |
| submitted |
判断表单已经被提交 |
内置校验规则
Angular 内置了几种常用校验规则:
| 校验规则 |
描述 |
| required |
输入结果非空 |
| requiredTrue |
输入结果为true |
| minLength |
设置最小长度 |
| maxLength |
设置最大长度 |
| pattern |
设置需匹配的正则表达式 |
| nullValidator |
禁用验证器 |
| email |
输入结果为电子邮件格式 |
| 详细的 API 描述在这里。 |
|
如果没有通过校验,那么该表单控件下的errors列表中就会获得相应的错误名称:
1 2 3
| <div *ngIf="userName.errors.required"> 用户名不能为空 </div>
|
自定义校验规则
内置的校验规则经常不够用,尤其在需要多条件联合校验的时候,所以我们需要自己定义校验规则。
在上一章节中,我们已经实现了一个简单的手机号验证规则:利用指令实现表单校验器,这一次,将会实现一个稍难的校验规则:确认密码
命令行或手工创建文件equal-validator.directive.ts,内容如下:
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 37 38 39 40
| import { Directive,Input } from '@angular/core'; import { Validator, AbstractControl, NG_VALIDATORS } from '@angular/forms';
@Directive({ selector: '[validateEqual]', providers: [ { provide: NG_VALIDATORS, useExisting: EqualValidator, multi: true } ] }) export class EqualValidator implements Validator { @Input()validateEqual: string; @Input()reverse: boolean; constructor() { } validate(control: AbstractControl): { [key: string]: any } { let selfValue = control.value; let targetControl = control.root.get(this.validateEqual); if (targetControl && selfValue !== targetControl.value ) { if(!this.reverse){ return { validateEqual: false } }else{ targetControl.setErrors({ validateEqual: false }) } }else{ targetControl.setErrors(null); } return null; } }
|
用法(详细代码见文首):
1 2 3
| <input formControlName="password" validateEqual="confirmPassword" reverse=true type="password"> <input formControlName="confirmPassword" validateEqual="password" type="password">
|
如果两个输入框的密码不一致,confirmPassword的errors列表就会获得一个名为validateEqual的错误。
效果演示:

响应式表单(模型驱动表单)
响应式表单的特点是:把表单的创建、校验等逻辑全部用代码写到组件里面,让 HTML 模板变得很简单。
观察以下代码:
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
| import { Component, OnInit, ViewEncapsulation } from '@angular/core'; import { FormGroup, FormBuilder, Validators } from "@angular/forms";
@Component({ selector: 'form-register', templateUrl: './register.component.html', styleUrls: ['./register.component.css'], encapsulation: ViewEncapsulation.Emulated }) export class RegisterComponent implements OnInit {
public userForm:FormGroup; private userInfo:User = new User();
constructor( private fb:FormBuilder ) { }
ngOnInit() { this.buidForm(); } private buidForm() { this.userForm = this.fb.group({ userName: [ this.userInfo.userName, [ Validators.required, Validators.minLength(2), Validators.maxLength(32) ] ], email: [ this.userInfo.email, [ Validators.required, Validators.pattern('^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((\.[a-zA-Z0-9_-]{2,3}){1,2})$') ] ], telephone: [ this.userInfo.telephone, [ Validators.required ] ], nickName: [ this.userInfo.nickName, [ Validators.required, Validators.minLength(4), Validators.maxLength(32) ] ], password: [ this.userInfo.password, [ Validators.required, Validators.minLength(8) ] ], confirmPassword: [ this.userInfo.confirmPassword, [ Validators.required, Validators.minLength(8) ] ], vcode: [ this.userInfo.vcode, [ Validators.required, Validators.minLength(6), Validators.maxLength(6) ] ] }) } } export class User { userId:string; userName: string; nickName: string; password: string; remeberMe:boolean; email: string; telephone:string; confirmPassword: string; vcode:string; }
|
上述代码在组件中利用FormBuilder构建了一个完整的表单,并在每个FormControl里设置了一些校验规则。
在模板中,把这些表单控件手动添加到各个表单元素上:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <form [formGroup]="userForm" novalidate> <div class="form-group"> <label class="col-sm-3 control-label">用户名</label> <div class="col-sm-8"> <input formControlName="userName" type="text"> </div> </div> <form>
|
不难看出,响应式表单的本质和模板式标签是一样的,只不过脱离了模板的束缚,可以在组件内部中自由DIY。
实际应用中的业务逻辑会比上述代码复杂得多,详细代码见文首链接。代码量太大这里就不帖了。
动态表单
有这样一种业务场景:你不知道表单里面的输入项都应该有哪些内容,需要根据服务端返回的数据动态进行创建。
这样的话html模板是没办法写死的,我们需要把常用的表单项抽象出来,在需要的时候调用。
抽象
首先命令行生成一个空的动态组件:
我们现在先不用忙着给这个组件内部添加内容,而是先缕清思路:
- 思考常用组件都应具备哪些属性
- 创建一个基类
- 根据这个基类进行细分
- 输出各个类型的表单控件
在dynamic-form文件夹中,新建forms文件夹,我们将会把各类表单控件抽象并存放到里面:

其中,base.ts用来存放基类。其它的类都要继承它的属性和基本规则:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| export class Base<T>{ value: T; key: string; label: string; controlType: string; placeholder: string;
constructor(options: { value?: T; key?: string; label?: string; required?: boolean; order?: number; controlType?: string; placeholder?: string; } = {}) { this.value = options.value; this.key = options.key || ''; this.label = options.label || ''; this.controlType = options.controlType || ''; this.placeholder = options.placeholder || ''; } }
|
其它的类根据需求可设置不同的属性:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { Base } from './base';
export class Image extends Base<string>{
controlType: string = 'image' src: string
constructor(options: {} = {}) { super(options); this.src = options['src'] || ''; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { Base } from './base';
export class TextBox extends Base<string>{
controlType: string = 'textbox'; type: string;
constructor(options: {} = {}) { super(options); this.type = options['type'] || ''; } }
|
最后,以index.ts作为出口:
1 2 3 4 5
| export { Base } from './base'; export { Image } from './image'; export { Textarea } from './textarea'; export { TextBox } from './textbox';
|
完善组件
完善dynamic-form-component.html模板如下:
1 2 3 4 5 6 7 8 9 10 11 12
| <div [formGroup]="form"> <label class="col-sm-2 control-label">{{field.label}}</label> <div class="col-sm-10"> <div [ngSwitch]="field.controlType"> <input *ngSwitchCase="'textbox'" class="form-control" value="{{field.value}}" [formControlName]="field.key" [type]="field.type" placeholder="{{field.placeholder}}" /> <textarea *ngSwitchCase="'textarea'" value="{{field.value}}" rows="{{field.rows}}" class="form-control" [formControlName]="field.key" placeholder="{{field.placeholder}}"></textarea> <img *ngSwitchCase="'image'" src="{{field.src}}"> </div> </div> </div>
|
我们利用了ngSwitch指令,来区分需要提供哪种类型的表单控件。
组件内部代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { Component, OnInit, ViewEncapsulation, Input } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { Base } from '../dynamic-form/forms';
@Component({ selector: 'form-dynamic-form', templateUrl: './dynamic-form.component.html', styleUrls: ['./dynamic-form.component.css'], encapsulation: ViewEncapsulation.None }) export class DynamicFormComponent implements OnInit {
@Input() form: FormGroup; @Input() field: Base<any>
constructor() { }
ngOnInit() { }
}
|
form和field通过调用此组件的父组件实例化后传进来。
调用组件
想要调用上述组件,必须传入两个对象,form对象和field对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <div class="user-profile-container"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">修改个人资料</h3> </div> <div class="panel-body"> <form class="form-horizontal" role="form" [formGroup]="form"> <div *ngFor="let field of fields" class="form-group"> <form-dynamic-form [form]="form" [field]="field"></form-dynamic-form> </div> <div class="form-group"> <div class="col-md-offset-2 col-md-10"> <button class="btn btn-success">保存</button> </div> </div> </form> </div> </div> </div>
|
模板中的form绑定到了<form>标签,它是一个formGroup对象;field对象来自fields数组。它们都会在组件内部完成实例化:
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| import { Component, OnInit, ViewEncapsulation } from '@angular/core'; import { FormGroup, FormControl } from "@angular/forms"; import { Base, Image, Textarea, TextBox } from './dynamic-form/forms';
@Component({ selector: 'form-user-data', templateUrl: './user-data.component.html', styleUrls: ['./user-data.component.css'], encapsulation: ViewEncapsulation.None }) export class UserDataComponent implements OnInit {
public fields: Base<any>[] = [ new Image({ key: "logo", src: 'https://angular.io/assets/images/logos/angular/logo-nav@2x.png' }), new TextBox({ key: "avatar", label: "头像:", placeholder: "上传头像", type: "file" }), new TextBox({ key: "userName", label: "用户名:", placeholder: "用户名" }), new TextBox({ key: "email", label: "常用邮箱:", placeholder: "常用邮箱" }), new TextBox({ key: "password", label: "密码:", type: "password", placeholder: "密码,至少8位" }), new TextBox({ key: "confirmPassword", label: "重复密码:", type: "password", placeholder: "重复密码" }), new Textarea({ key: "description", label: "个人简介:", placeholder: "个人简介,最多140字,不能放链接。", rows: 3, }) ]
public form:FormGroup;
constructor() { }
ngOnInit() { this.form = this.toFormGroup(this.fields); console.log(this.form); }
toFormGroup(fields: Base<any>[]) { let group: any = {};
fields.forEach(field => { group[field.key] = new FormControl(field.value || ''); }); console.log(group) return new FormGroup(group); }
}
|
代码中,我们首先在fields数组中初始化了各个所需的表单控件,然后遍历这个数组存入一个对象,利用这个对象构建了一个完整了formGroup对象。
一个表单就这样被动态构建出来了,它和其它类型的表单没有任何区别:
