1. 纯html5 表单
纯HTML5表单提供功能如下显示表单项先看一个纯html5表单的例子:
校验用户输入
提交表单数据
<form action="/register" method="post"> <div>UserName: <input type="text"></div> <div>TelePhone: <input type="text"></div> <div>Password: <input type="password"></div> <div>datetime: <input type="month"></div> <div>ConfimrPassword: <input type="password"></div> <button type="submit">Register</button> </form>对于一个SPA应用来说,一般我们会需要我们的表单具有以下的几个功能:
a. 每一个输入字段都可以独立的指定校验规则
b. 如果输入内容不符合校验规则,应该对应的字段应该演示错误信息(用户可以明确理解的)
c. 彼此依赖的字段应该一起校验(上面例子中的密码和确认密码)
d. 用户的输入可以提交到服务端,并且在提交之前应用可以校验用户的输入以及格式化用户的输入。
e. 应用可以控制表单如何提交到服务,可以是默认的http请求,或者ajax异步请求,或者是websocket请求方式。
对于上面的例子对于上面的几个要求,可以部分满足前两个要求,使用html5的表单属性去进行验证,例如required, pattern(title显示错误的信息),和使用input标签的type属性来规范用户的输入格式。
为什么说是部分呢,举一个例子假如用户需要输入自己所在的城市,这个其实是需要和相关数据一起验证,此时这个验证单靠纯html5就无法实现了。(当然,就算是angular表单也是需要html与js集成封装去完成验证,但是不需要开发人员去做相关的处理了)。
还有最重要的一点,对于html5表单属性的浏览器兼容性,其实并不是所有的表单属性在各大浏览器都可以使用的,在这里可以使用HTML5测试 – 你的浏览器能有多好的支持HTML5查看属性的支持。
2. Angular表单的分类
模板式表单表单的数据模型是通过组件模板中的相关指令来定义的,因为使用这种凡是定义的表单数据模型时,我们首先于HTML的语法,所以,模板驱动范式只适合简单的场景。
响应式表单
使用响应式表单时,你通过编写TypeScript的代码而不是html代码来创建一个底层的数据模型,在这个模型定义好之后,使用一些特定的指令,将木板上的html元素与底层的数据模型链接在一起。3. Angular两种表单的不同
a.表单数据模型的不同
模板式表单的数据模型是由angular基于组件模板中的指令隐式创建的。响应式表单时通过代码明确的创建数据模型,然后将木板上的html元素与底层的数据模型链接在一起。
b. 表单数据模型的访问
数据模型并不是一个任意的对象,它是由angular/forms模块中的一些特性的类,如FormControl,FormGroup,FormArray等组成的。在模板式表单中,是不能直接访问到这些累的,但是在响应式表单中是可以的。
c. 响应式表单不会生成html,模板仍然需要自己来编写。(模板式表单同样)。
d. 引入的模块不同
模板式表单引入FormsModule,响应式表单引入ReactiveFormsModule.
e. 异步 vs. 同步(摘抄与官方文档)
响应式表单是同步的。模板驱动表单是异步的。这个不同点很重要。
使用响应式表单,我们会在代码中创建整个表单控件树。 我们可以立即更新一个值或者深入到表单中的任意节点,因为所有的控件都始终是可用的。
模板驱动表单会委托指令来创建它们的表单控件。 为了消除“检查完后又变化了”的错误,这些指令需要消耗一个以上的变更检测周期来构建整个控件树。 这意味着在从组件类中操纵任何控件之前,我们都必须先等待一个节拍。
f. 模板中访问数据(本地模板变量)
模板式表单可以通过本地模板量访问,但是响应式表单不行4. 模板式表单
模板式表单是使用指令来定义数据模型,可以使用NgForm,NgModel,NgModelGroup指令。
(1) NgForm
Angular会在<form>标签上自动创建并附加一个NgForm指令。那么和原先的form表单有什么不同呢?
首先引进FormsModule,这个时候当我们在点击提交button的时候,form表单不会在提交这个表单。因为现在整个form的处理都已经交给angular了。angular会拦截自动提交事件,因为form表单的自动提交会造成页面的刷新,对于单页面来说,这很不友好,所以会通过自定义的事件ngSubmit来进行表单的提交。
然后我们先将上面的表单改写一下:
<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm.value)"> <div>UserName: <input type="text"></div> <div>TelePhone: <input type="text"></div> <div>Password: <input type="password"></div> <div>ConfimrPassword: <input type="password"></div> <button type="submit">Register</button> </form> <div> {{myForm.value | json}} </div>
div是将form的值打出来,便于我们观察,这里使用本地模板变量(前面模板语法说过)来访问form。
NgForm会隐式的创建FormGroup这个类的实例,这个类用来代表表单的数据模型。
它会控制那些带有ngModel指令和name属性的元素,监听他们的属性(包括其有效性)。 它还有自己的valid属性,这个属性只有在它包含的每个控件都有效时才是真。
如果不想让angular处理form表单,需要在form上标明ngNoForm
<form action="/register" method="post" ngNoForm>
(2) NgModel
但是在上面的例子中当我们填入input内容的时候,内容却不会打出来,这是因为这个input还没有假如数据模型中去,这时候我们就需要NgModel指令了。
在angular表单API中NgModel指令代表表单中一个字段,这个指令会隐式的创建一个FormControl类的实例,来代表字段的数据类型,并用这个FormControl来存储字段的值。然后我们在改写一下表单
<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm.value)"> <div>UserName: <input ngModel #name="ngModel" name="username" type="text"></div> <div>TelePhone: <input ngModel name="phone" type="text"></div> <div>Password: <input ngModel name="password" type="password"></div> <div>ConfimrPassword: <input ngModel name="confimrPassword" type="password"></div> <button type="submit">Register</button> </form> <div> {{myForm.value | json}} </div>这时候页面的输入会打出来,可以使用#name="ngModel"本地模板变量来访问具体的字段的值。
(3) NgModelGroup
代表表单的一部分,与NgForm一样也会创建FormGroup的实例。会在NgForm.value中以嵌套的形式存在。用于我们去形成表单的层次关系。
改写form如下:
<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm.value)"> <div ngModelGroup="userInfo"> <div>UserName: <input ngModel name="username" type="text"></div> <div>TelePhone: <input ngModel name="phone" type="text"></div> </div> <div>Password: <input ngModel name="password" type="password"></div> <div>ConfimrPassword: <input ngModel name="confimrPassword" type="password"></div> <button type="submit">Register</button> </form> <div> {{myForm.value | json}} </div>看一下页面:
(4) 重构表单
<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm.value)"> <div>UserName: <input ngModel name="username" type="text"></div> <div>TelePhone: <input ngModel name="phone" type="text"></div> <div ngModelGroup="passwordGroup"> <div>Password: <input ngModel name="password" type="password"></div> <div>ConfimrPassword: <input ngModel name="confimrPassword" type="password"></div> </div> <button type="submit">Register</button> </form>将password形成一个group便于我们进行验证,然后控制台打出提交的value:
具体的验证放在后面了解。
5. 响应式表单
创建响应式表单的两个步骤a. 创建数据模型
b. 使用指令将数据模型链接到html上
创建数据模型有定义在angular中三个类构成FormControl,FormGroup,FormArray。
AbstractControl是三个具体表单类的抽象基类。 并为它们提供了一些共同的行为和属性,其中有些是可观察对象(Observable)。
FormControl 用于跟踪一个单独的表单控件的值和有效性状态。它对应于一个HTML表单控件,比如输入框和下拉框。
FormGroup用于 跟踪一组AbstractControl的实例的值和有效性状态。 该组的属性中包含了它的子控件。 组件中的顶级表单就是一个FormGroup。
FormArray用于跟踪AbstractControl实例组成的有序数组的值和有效性状态。
//template <form [formGroup]="formModel" (ngSubmit)="onSubmit()"> <div>UserName: <input formControlName="username" type="text"></div> <div>TelePhone: <input formControlName="phone" type="text"></div> <div formGroupName="passwordGroup"> <div>Password: <input formControlName="password" type="password"></div> <div>ConfimrPassword: <input formControlName="confimrPassword" type="password"></div> </div> <button type="submit">Register</button> </form>
//ts import { Component, OnInit } from '@angular/core'; import {FormControl, FormGroup} from '@angular/forms'; @Component({ selector: 'app-template-form', templateUrl: './template-form.component.html', styleUrls: ['./template-form.component.css'] }) export class TemplateFormComponent implements OnInit { formModel: FormGroup; constructor() { } ngOnInit() { this.formModel = new FormGroup({ username: new FormControl(), phone: new FormControl(), passwordGroup : new FormGroup({ password: new FormControl(), confimrPassword: new FormControl(), }) }); } onSubmit() { console.log(this.formModel.value); } }接下来会以这个例子进行学习。
(1)FormGroup
FormGroup中有两个指令,FormGroup和FormGroupName.formGroup是一个响应式表单的指令,它拿到一个现有FormGroup实例,并把它关联到一个HTML元素上。 这种情况下,它关联到的是form元素上的FormGroup实例。
FormGroupName可以将已有的表单组合绑定到一个DOM元素上,它仅作为FormGroupDirective指令的子元素使用。如上面的passwordGroup。对应模板式表单中的ngModelGroup。
(2) FormControl
FormControl类中有两个指令,FormControl和FormControlName.formControlName 指令,绑定我们创建的 FormControl 控件到html元素上,用法请看上面例子。
FormControl:FormControlDirective指令可以将一个已有的FormControl控件绑定到一个DOM元素。
FormControl指令不能使用到FormGroup指令中去。以前没有父FormGroup的时候,[formControl]="name"也能正常工作,因为该指令可以独立工作,也就是说,不在FormGroup中时它也能用。 有了FormGroup,name输入框就需要再添加一个语法formControlName=name,以便让它关联到类中正确的FormControl上。 这个语法告诉Angular,查阅父FormGroup,然后在这个FormGroup中查阅一个名叫name的FormControl。但是不能再FormGroup中使用FormControl.
(3) FormArray
FormArray只有一个指令FormArrayName。用于操控FormGroup中的数组。先看一个例子:我想在提交表单的时候按照用户的自定义来确定由多少个email需要提交到server。这个时候就需要用到FormArrayName。
HTML模板显示单个的地址FormGroup。 我们要把它修改成能显示0、1或更多个表示英雄地址的FormGroup。
要改的部分主要是把以前表示地址的HTML模板包裹进一个<div>中,并且使用*ngFor来重复渲染这个<div>。
诀窍在于要知道如何编写*ngFor。主要有三点:
a. 在*ngFor的<div>之外套上另一个包装<div>,并且把它的formArrayName指令设为"secretLairs"。 这一步为内部的表单控件建立了一个FormArray型的secretLairs作为上下文,以便重复渲染HTML模板。b. 这些重复条目的数据源是FormArray.controls而不是FormArray本身。 每个控件都是一个FormGroup型的地址对象,与以前的模板HTML所期望的格式完全一样。
c. 每个被重复渲染的FormGroup都需要一个独一无二的formGroupName,它必须是FormGroup在这个FormArray中的索引。 我们将复用这个索引,以便为每个地址组合出一个独一无二的标签。
//template <form [formGroup]="formModel" (ngSubmit)="onSubmit()"> <div formGroupName="dateRange"> <div>start time: <input formControlName="from" type="date"></div> <div>end time: <input formControlName="to" type="date"></div> </div> <div> <ul formArrayName="emails"> <li *ngFor="let e of formModel.get('emails').controls; let i = index"> <input type="text" [formControlName]="i"> </li> </ul> <button type="button" (click)="addEmail()">Add Email</button> </div> <button type="submit">Submit</button> </form>
//ts import { Component, OnInit } from '@angular/core'; import {FormArray, FormControl, FormGroup} from '@angular/forms'; @Component({ selector: 'app-template-form', templateUrl: './template-form.component.html', styleUrls: ['./template-form.component.css'] }) export class TemplateFormComponent implements OnInit { formModel: FormGroup = new FormGroup({ dateRange: new FormGroup({ from: new FormControl(), to: new FormControl(), }), emails: new FormArray([ new FormControl('[email protected]'), new FormControl('[email protected]') ]) }); constructor() { } ngOnInit() { } onSubmit() { console.log(this.formModel.value); } addEmail() { (this.formModel.get('emails') as FormArray).push(new FormControl()); } }假如我是用户,我在添加两个email,然后点击提交,看一些页面输出:
* 在ts代码的编写中。对于FormArray中的FormControl只能通过索引来访问,所以template的formControlName的值是索引值i。
* this.formModel.get('emails') 方式获取FormGroup中的FormControl字段或者FormGroup数组。
(4) FormBuilder
再回到原先的例子中去,现在我们看到走到现在响应式表单的代码比模板式表单的代码要多。我们可以使用FormBuild去简化代码。FormBuilder类能通过处理控件创建的细节问题来帮我们减少重复劳动。
先看一下FormBuild内部的方法:
上面的方法分别实现FormGroup,FromControl,FormArray。
先改写一下:
import { Component, OnInit } from '@angular/core'; import {FormBuilder, FormControl, FormGroup} from '@angular/forms'; @Component({ selector: 'app-template-form', templateUrl: './template-form.component.html', styleUrls: ['./template-form.component.css'] }) export class TemplateFormComponent implements OnInit { formModel: FormGroup; constructor(private formBuild: FormBuilder) { } ngOnInit() { this.formModel = this.formBuild.group({ username: this.formBuild.control(''), phone: this.formBuild.control(''), passwordGroup : this.formBuild.group({ password: this.formBuild.control(''), confimrPassword: this.formBuild.control(''), }) }); } onSubmit() { console.log(this.formModel.value); } }但是这样看起来还是不太简单,反而更复杂了,this.formBuild.control()在formBuild中允许我们使用数组进行创建,如下:
import { Component, OnInit } from '@angular/core'; import {FormBuilder, FormControl, FormGroup} from '@angular/forms'; @Component({ selector: 'app-template-form', templateUrl: './template-form.component.html', styleUrls: ['./template-form.component.css'] }) export class TemplateFormComponent implements OnInit { formModel: FormGroup; constructor(private formBuild: FormBuilder) { } ngOnInit() { this.formModel = this.formBuild.group({ username: [], phone: [], passwordGroup : this.formBuild.group({ password: [], confimrPassword: [], }) }); } onSubmit() { console.log(this.formModel.value); } }终于看起来简单多了,对于这些方法的用法说两点:
group方法允许在多加一个参数,用于对于整个表单的校验。
[]内部的三个参数,第一个为初始值,第二个参数为校验方法,第三个为异步的校验方法。具体的校验方式后面会说到。
(5) 使用setValue和patchValue来操纵表单模型
a. setValue
setValue方法会在赋值给任何表单控件之前先检查数据对象的值。
它不会接受一个与FormGroup结构不同或缺少表单组中任何一个控件的数据对象。 这种方式下,如果我们有什么拼写错误或控件嵌套的不正确,它就能返回一些有用的错误信息。 patchValue会默默地失败。而setValue会捕获错误,并清晰的报告它。
this.formModel.setValue({ username: '1', phone: 1, passwordGroup: { password: 1, confimrPassword: 1 } });
b. patchValue
借助patchValue,我们可以通过提供一个只包含要更新的控件的键值对象来把值赋给FormGroup中的指定控件。 但是和setValue不同,patchValue不会检查缺失的控件值,并且不会抛出有用的错误信息。this.formModel.patchValue({ username: '1' });
c. 什么时候设置表单的模型值(ngOnChanges)
设置表单的模型值在生命周期钩子ngOnChanges。6. 表单校验
(1) 响应式表单的验证
a. 内置验证器
class Validators { static min(min: number): ValidatorFn static max(max: number): ValidatorFn static required(control: AbstractControl): ValidationErrors | null static requiredTrue(control: AbstractControl): ValidationErrors | null static email(control: AbstractControl): ValidationErrors | null static minLength(minLength: number): ValidatorFn static maxLength(maxLength: number): ValidatorFn static pattern(pattern: string | RegExp): ValidatorFn static nullValidator(c: AbstractControl): ValidationErrors | null static compose(validators: (ValidatorFn | null | undefined)[] | null): ValidatorFn | null static composeAsync(validators: (AsyncValidatorFn | null)[]): AsyncValidatorFn | null }我们还是将上面的例子拿下来:
this.formModel = this.formBuild.group({ username: ['', [Validators.required, Validators.minLength(6)]], phone: ['', Validators.compose([Validators.required, Validators.minLength(4)])], passwordGroup : this.formBuild.group({ password: [], confimrPassword: [], }) });这里就简单的举个例子,可以看到FormBuild的comtrol方法内部的第二个参数集成了Validators.compose的方法。
b. 验证状态码
valid - 表单控件有效invalid - 表单控件无效
pristine - 表单控件值未改变
dirty - 表单控件值已改变
touched - 表单控件已被访问过
untouched - 表单控件未被访问过
errors - 表单控件校验错误的信息
pending - 进行异步校验的时候
* formModel的valid属性依赖于所有的控件的valid属性,所有的valid属性都为true的时候,表单的valid属性才是true。
this.formModel.get('username').errors
c. 自定义同步验证
export class TemplateFormComponent implements OnInit { formModel: FormGroup; mobileValidator(control: FormControl): any { let myreq = /^(((13[0-9])|(14[0-9])|(15([0-9]))|(18[0-9]))+\d{8})$/; let valid = myreq.test(control.value); console.log('phone的校验结果是:' + valid); return valid ? null : {phone: true}; } constructor(private formBuild: FormBuilder) { } ngOnInit() { this.formModel = this.formBuild.group({ username: ['', [Validators.required, Validators.minLength(6)]], phone: ['', [this.mobileValidator]], passwordGroup : this.formBuild.group({ password: [], confimrPassword: [], }) }); } onSubmit() { } }然后看一下界面的校验:
如图,每一次的输入都会有校验,直到最后的字母输入进来,才返回true。
* 自定义校验的返回值null的时候是证明校验成功。
对于上面的password group的验证如下:
export class TemplateFormComponent implements OnInit { formModel: FormGroup; equalValidator(group: FormGroup): any { let password: FormControl = group.get('password') as FormControl; let confirmPassword: FormControl = group.get('confimrPassword') as FormControl; let valid: boolean = (password.value === confirmPassword.value); console.log('password的校验结果是:' + valid); return valid ? null : {equal: true}; } constructor(private formBuild: FormBuilder) { } ngOnInit() { this.formModel = this.formBuild.group({ username: ['', [Validators.required, Validators.minLength(6)]], phone: ['', []], passwordGroup : this.formBuild.group({ password: [], confimrPassword: [], }, {validator: this.equalValidator}) }); } onSubmit() { } }这里的用法是在group的第二个方法中传入自定义的校验器,并且格式是这样的{ validator: this.equalValidator}
d. 自定义异步校验器
angular允许校验器通过server异步校验用户的输入。其实写法和同步校验器相同,只是在内部我们可以通过向server发送请求验证。export class TemplateFormComponent implements OnInit { formModel: FormGroup; mobileValidator(control: FormControl): any { let myreq = /^(((13[0-9])|(14[0-9])|(15([0-9]))|(18[0-9]))+\d{8})$/; let valid = myreq.test(control.value); console.log('phone的校验结果是:' + valid); return valid ? null : {phone: true}; } mobileAsynxValidator(control: FormControl): any { let myreq = /^(((13[0-9])|(14[0-9])|(15([0-9]))|(18[0-9]))+\d{8})$/; let valid = myreq.test(control.value); return Observable.of(valid ? null : {phone: true}).delay(5000); } constructor(private formBuild: FormBuilder) { } ngOnInit() { this.formModel = this.formBuild.group({ username: ['', [Validators.required, Validators.minLength(6)]], phone: ['', [this.mobileValidator], this.mobileAsynxValidator], passwordGroup : this.formBuild.group({ password: [], confimrPassword: [], }) }); } onSubmit() { } }上面的例子中mobileAsynxValidator为异步校验,当进行异步校验的时候,表单的status是pending。这里是使用Rxjs来模拟一个5S之后验证结果返回。
e. 表示控件状态的 CSS 类
像 AngularJS 中一样,Angular 会自动把很多控件属性作为 CSS 类映射到控件所在的元素上。我们可以使用这些类来根据表单状态给表单控件元素添加样式。目前支持下列类:.ng-valid
.ng-invalid
.ng-pending
.ng-pristine
.ng-dirty
.ng-untouched
.ng-touched
在组件中加上对应的class,则会在对应的表单控件的状态的时候,适应对应的css样式。
f. 显示相应的错误信息
举个简单的例子吧<div>UserName: <input formControlName="username" type="text"></div> <div [hidden]="!formModel.hasError('required','username')"> 请输入用户名! </div>这里也可以使用其他的属性值去判断,valid。说一下这个方法hasError(),第一个参数是校验器的返回结果,还记得自定义校验器的时候返回true吗!就是这个值。第二个参数是校验对应的控件。所以这里的formModel前面加了一个取反。
(2) 模板式表单的验证
模板式表单的验证呢,由于本人用的也不多,加上今天的时间有限,这里就说上面不同的点吧。a. 内置校验器对应的指令。如required, minlength都有着对应的指令,可以应用。
<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm.value)" novalidate> <div>UserName: <input ngModel required name="username" type="text"></div> <div [hidden]="!myForm.form.hasError('required','username')"> 请输入用户名! </div> <div>TelePhone: <input ngModel name="phone" type="text"></div> <div ngModelGroup="passwordGroup"> <div>Password: <input ngModel name="password" type="password"></div> <div>ConfirmPassword: <input ngModel name="ConfirmPassword" type="password"></div> </div> <button type="submit">Register</button> </form>
* 模板式表单是不可以使用表单状态的值的,像valid,如下:
TemplateFormComponent.html:9 ERROR TypeError: Cannot read property 'valid' of null
想要使用的话需要使用事件绑定和本地模板变量传值到ts代码中去操作。
* 使用required的时候如果担心浏览器的默认行为,可以使用novalidate禁止浏览器的默认校验行为。
b. 自定义校验器的使用
我们只有将我们的自定义校验器以指令的形式创建,然后应用到html中去。import { Directive } from '@angular/core'; import {FormControl, NG_VALIDATORS} from '@angular/forms'; @Directive({ selector: '[Mobile]', providers: [{provide: NG_VALIDATORS, useValue: mobileValidator, multi:true}] }) export class MobileDirective { constructor() { } } export function mobileValidator(control: FormControl): any { let myreq = /^(((13[0-9])|(14[0-9])|(15([0-9]))|(18[0-9]))+\d{8})$/; let valid = myreq.test(control.value); console.log('phone的校验结果是:' + valid); return valid ? null : {phone: true}; }这里说两点
NG_VALIDATORS是校验器的默认token,都需要使用这个token。
multi:true是因为如果项目中有多个校验器指令的话,但是使用的是同一个token,那么就需要加上multi:true。
一天下来,很辛苦,但是终于搞完了,还不错。