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('aa@.com'),
new FormControl('bb@.com')
])
});
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。
一天下来,很辛苦,但是终于搞完了,还不错。