模版式表单(FormsModule)
这个方法只适用于简单的表单需求,因为它受限于html模版。
可以使用的指令为:
- NgForm. ———> 隐式创建FormGroup实例
- NgModel. ———>隐式创建FormControl实例
- NgModelGroup. ————>隐式创建FormGroup实例,与NgForm类似,只不过NgModelGroup相当于在外面又加了一层外壳,然后这个外壳,作为了NgForm的子属性
NgForm相当于传统的页面form表单,表单中所有定义的属性,需要写上NgModel,这样NgForm就知道所有NgModel的存在了。而且NgForm会阻止传统form的提交,因为angular是SPA,如果出现页面跳转是违背这个理论的,但是angular也有取而代之的方法,那就是使用ngSubmit事件绑定。
下面是一个form表单事例:
<form #myForm="ngForm" (ngSubmit)="onSubmit()" >
<div> 用户名:<input type="text" #username="ngModel" ngModel name="username" /></div>
<div ngModelGroup="detail">
<div>微信号:<input type="text" ngModel name="weChatId"/></div>
<div> 手机号:<input type="text" ngModel name="phone"/></div>
</div>
<div> 密码:<input type="password" ngModel name="password" /></div>
<div> 确认密码:<input type="password" ngModel name="rePassword" /></div>
<button type="submit">发送</button>
</form>
<div>
<!--获取表单中所有标有了ngModel属性的值-->
{{myForm.value | json}}
<!--我们在username的输入框中又绑定了一个username的属性绑定,这个绑定意味着我们可以单独拿出username的值来-->
<br/>
{{username.value}}
</div>
下面看一下这个"myForm.value"输出的结果:
{
"username": "",
"detail":
{
"weChatId": "",
"phone": ""
},
"password": "",
"rePassword": ""
}
可以很清楚的看出来,我们输出在表单上定义的别名"myForm"的值,里面嵌套了所有标有ngModel,与ngModelGroup的值,值得注意的是,ngModelGroup中还会嵌套标有ngModel的属性。
响应式表单(ReactiveFormsModule)
通过ts代码去代替传统的html模版去创建底层的数据模型,然后通过双向绑定将ts代码与html界面绑定上
tips: 不管是使用哪一种方法,都是需要有一个数据模型作为载体的。模版式会因为html模版隐式的创建数据模型,而响应式的会通过FormControl,FormGroup,FormArray之类的类去创建数据模型,这些angular为我们提供的类,我们式不能进行直接访问的。另外响应式编程也是需要我们写html模版代码的。
我们从最关键的三个实例讲起:
- FormControl
FormControl其实是与html页面中的input或者是其他可输入元素在component中的一种映射。
事例如下:
username: FormControl = new FormControl('harry');
- FormGroup
FormGroup相当于FormControl的一个集合,大多数的情况下,我们都是把属于同一组的元素,放在一个group中。并且一个Group中只要有一个元素不合法,整个Group都会"受牵连"。
事例如下:
time: FormGroup = new FormGroup({
from: new FormControl(),
to: new FormControl()
});
Tips:刚刚FormControl传入的属性是一个字符串,FormGroup传入的则是一个json
- FormArray
FormArray与FormGroup类似,只不过FormArray装载的是相同属性的不同的值,如果我们把FormGroup理解为一个对象集合(因为有key, 有value),那么FormArray就相当于一个数组结合(因为没有key,我们只能通过序号辨别)。
事例如下:
hobbies: FormArray = new FormArray([
new FormControl('singing'),
new FormControl('swimming')
]);
模版式表单和响应式表达最大的差别就在于模版式只能在界面中进行操作,而响应式只能在typescript代码中进行使用
紧接着我们介绍一下响应式表单的五个指令:
- formControl
首先先从这个指令开始说起,这个指令需要和input指令相绑定,上面的第一个实例已经介绍过了,如何在component中的代码中进行定义。有了在component中的定义,那么还需要有一个在html页面中的展示,代码如下:
<input type="text" [formControl] = "username"/>
formControl需要括号因为存在属性绑定,后面跟的值,就是我们在component中定义的FormControl的名字。
️特别值得注意的是这个这个指令一定不能在下方的formGroup中使用,他们水火不融,具体的原因会在下面说。
- formGroup
这个指令最值得我们说一说,他可以说是form表单的一个灵魂指令,相当于form中的大boss,为什么这么说呢?我们先看代码。
首先给出的是html代码:
<form [formGroup]="formModel">
<!--formGroupName不需要加括号,是因为它是隶属于最外层formGroup中的一个元素-->
<div formGroupName="time" >
开始日期:<input type="date" formControlName="from"/>
截止日期:<input type="date" formControlName="to"/>
</div>
<div formArrayName="hobbies">
<ul>
<li *ngFor="let item of this.formModel.get('hobbies').controls; let i = index;">
兴趣爱好:<input type="text" [formControlName]="i"/>
</li>
</ul>
<button (click)="addHobby()" >添加爱好</button>
</div>
<button type="submit">提交</button>
</form>
然后给出component中的代码:
formModel: FormGroup = new FormGroup({
time: new FormGroup({
from: new FormControl(),
to: new FormControl()
}),
hobbies: new FormArray([
new FormControl('singing'),
new FormControl('swimming')
])
我们从component中的代码,对照着html代码看。
我们首先定义了一个FormGroup,它其中包含了本章开始的说过的那个三个实例,他们在html中的映射指令,会在下面的三个条目中说明。
️需要注意的是下面的三个条目必须包含在FormGroup中。
- formGroupName
可以参考第二条中两段代码中的formGroupName,它其实相当于在最外层form表单中又放入了一个FormGroup,类型相同,然后对应的参数相同就可以(在这里就是我们指定的那个time) - formControlName
同样可以参考第二段中的formControlName,它是作为FormGroup的小弟所存在的,在component中是这样,在html中亦是如此。对应的类型参数相同即可(具体可以参考我们指定的from和to) - formArrayName
我们可以看到这里有关formArrayName的代码相对复杂,但是其实ngFor我们在这里暂时可以不用看,只是为了可以讲遍历出来的爱好的数组展示在界面上而已,addHobby方法不需要看,只是为了可以后台可以通过按钮添加新的爱好而已。formArrayName与FormArray相互可以对照上。
总结:我愿意给一些概念具像化,这样可以清楚的理解他们五个之间的关系。
首先我们要知道html与component相当于两个"平行世界",两者之间是有一定联系的。
FormControl是一个独立的个体,不仅无父无母,而且膝下还无儿无女。
FormGroup也是一个独立的个体,它是表单中的"女娲",他有三个孩子,分别是FormGroupName,FormControlName,和FormArrayName。
看起来就像是下面的树状图一样:
|— FormControl
|— FormGroup
|— FormGroupName
|— FormControlName
|— FormArrayName
模版式表单与响应式表单对比
模版式表单是从html页面的角度出发的,我们可以不借助任何的component中的代码帮忙实现,除了form表单需要的submit方法。就如同他的名字一样,只是依附在模版上的。
而响应式表单则更注重的是绑定,思想应该是将model以及view进行了分离,虽然写法上不够简单,但是这样做更易于以后的维护。
既然响应式表单这么可取,那有没有什么方法可以简化实现构成呢?我既然这么说了,那么一定是有了,他就是FormBuilder。
那么FormBuilder到底有多简洁呢?我们先看一下正常情况下使用一个响应式表单是什么样子的。
this.formModel = new FormGroup({
username: new FormControl(),
phone: new FormControl(),
passwordGroup: new FormGroup({
password: new FormControl(),
confirmPassword: new FormControl()
})
});
而如果使用了FormBuilder之后呢?
<!--这之前我们需要先在构造函数中注入FormBuilder, 这里的fb就是FormBuilder-->
this.formModel = fb.group({
username: [''],
phone: [''],
passwordGroup: fb.group({
password: [''],
confirmPassword: ['']
})
});
一下子感觉干练了许多,看FormBuilder就知道,使用了建造者模式的设计模式,这样做的好处就是可以像拼乐高积木一样快乐的组装自己想要的组件。
不光如此,我们可以看到FormGroup被替换成了fb.group,而formControl被替换成了数组,为什么是个数组呢?因为第二个参数可以填入验证的匹配规则,等等,formbuilder的group同样也是可以传入第二个参数的。
响应式表单校验
校验器
angular的校验器其实就是一个个的方法,这个方法的参数需要是AbstractControl类型的,返回值需要是key为string的任意对象即可。除此之外angular还有自己的校验器,都是Validators下的方法,包含以下几类:
- Validators.required()
- Validators.minLength()
- Validators.maxLength()
- Validators.pattern()
- 等等...
angular自带校验器的使用
那么校验器应该如何使用呢?我们接着上一节使用的FormBuilder代码,进行进一步改装:
//username 在数组的第二个参数上又添加了一个新的数组,数组中放的就是需要的验证器,这里使用了一个必填验证,还有一个最小长度验证。
this.formModel = fb.group({
username: ['', [Validators.required, Validators.minLength(6)]],
phone: [''],
passwordGroup: fb.group({
password: [''],
confirmPassword: ['']
})
});
检验器,添加完毕之后,一定需要有一个地方去拿到验证结果:
//拿到username验证结果,返回的是一个boolean类型,只是告诉我们验证失败了,但不会说因为什么失败了
let isValid: boolean = this.formModel.get('username').valid;
//拿到username错误信息,返回的是一个json的结果,可以具体看到到底错在了哪个验证上。
let errorMsg: any = this.formModel.get('username').errors;
自定义校验器使用
上面我们使用到了angular自带的检验器,那么接下来我们看看自定义的校验器应该如何使用:
//下面写一个自定义的验证器的方法,在component方法中定义
validatePhone(control: FormControl) {
//验证手机号是否合法的正则表达式
let phoneReg = /^1[3456789]\d{9}$/;
let valid = phoneReg.test(control.value);
console.log(valid);
//如果合法返回空,如果不合法返回一个json对象
return valid ? null : {mobileValid: true};
}
然后只需要把这个校验器放到你需要验证的那个字段上即可,就像这样:
//注意下面phone字段的使用
this.formModel = fb.group({
username: ['', [Validators.required, Validators.minLength(6)]],
phone: ['', [this.validatePhone]],
passwordGroup: fb.group({
password: [''],
confirmPassword: ['']
})
});
接下来我们说一说如何针对一个formGroup进行验证:
我们还是需要首先定义一个验证方法:
//下面是一个验证controlGroup的例子
validatePasswordGroup(group: FormGroup) {
let password = group.get('password') as FormControl;
let confirmPassword = group.get('confirmPassword') as FormControl;
let valid = (password.value === confirmPassword.value);
console.log('两个密码相同:' + valid);
return valid ? null : {passwordGroup: true};
}
下面把这个方法安排在需要的地方上:
this.formModel = fb.group({
username: ['', [Validators.required, Validators.minLength(6)]],
phone: ['', [this.validatePhone]],
passwordGroup: fb.group({
password: [''],
confirmPassword: ['']
}, {validator: this.validatePasswordGroup})
});
️我们在fb.group方法中加入了一个json作为第二个参数,然后给了一个key叫做validator,这是和添加formControl验证器最大的一个区别,一定要注意。
html模版中使用检验器显示错误信息
下面直接上代码:
<form [formGroup] = "formModel" (ngSubmit)="onSubmit()" >
<div> 用户名:<input type="text" formControlName="username"/></div>
<!--hasError方法一共需要填入两个参数,第一个是你检验器返回的结果中的key的名字,而不是校验器的方法名-->
<!--第二个参数就是你要监控哪一个参数校验,就是formControlName的名称-->
<div [hidden] = "!formModel.hasError('required', 'username')" style=" color: red;font-size: 0.5em;">username 是必填项</div>
<div [hidden] = "!formModel.hasError('minlength', 'username')" style=" color: red;font-size: 0.5em;">username 的长度必须要大于6</div>
<div> 手机号:<input type="text" formControlName="phone"/></div>
<div [hidden] = "!formModel.hasError('mobileValid', 'phone')" style=" color: red;font-size: 0.5em;">手机号需要符合要求</div>
<div formGroupName="passwordGroup">
<div> 密码:<input type="password" formControlName="password"/></div>
<div [hidden] = "!formModel.hasError('minlength', ['passwordGroup', 'password'])" style=" color: red;font-size: 0.5em;">password 的长度必须要大于6</div>
<div> 确认密码:<input type="password" formControlName="confirmPassword"/></div>
</div>
<div [hidden] = "!formModel.hasError('passwordGroup', 'passwordGroup')" style=" color: red;font-size: 0.5em;">密码与确认密码需要一致</div>
<button type="submit">发送</button>
</form>
但是有的时候,我们不希望我们的错误信息是硬编码写死的,这个时候应该找点别的出路,首先把自定义的校验器的返回值进行改装:
//下面写一个自定义的验证器
export function validatePhone(control: FormControl) {
//验证手机号是否合法的正则表达式
let phoneReg = /^1[3456789]\d{9}$/;
let valid = phoneReg.test(control.value);
console.log(valid);
//如果合法返回空,如果不合法返回一个json对象
return valid ? null : {mobileValid: {msg: '请输入正确的手机号'}};
}
//下面是一个验证controlGroup的例子
export function validatePasswordGroup(group: FormGroup) {
let password = group.get('password') as FormControl;
let confirmPassword = group.get('confirmPassword') as FormControl;
let valid = (password.value === confirmPassword.value);
console.log('两个密码相同:' + valid);
return valid ? null : {passwordGroup: {msg: '两次输入的手机号需要一致'}};
}
可以看到如果不符合校验器返回的结果从 {mobileValid: true} —变成— >{mobileValid: {msg: '请输入正确的手机号'}}。
然后html中只需要取出message就可以,下面用手机号验证,作为例子,代码如下:
<div [hidden] = "!formModel.hasError('mobileValid', 'phone')" style=" color: red;font-size: 0.5em;">{{formModel.getError('mobileValid', 'phone')?.msg}}</div>
状态字段
- touched 和untouched
判断依据是当前这个字段是否获取过焦点,如果获取过就是touched - pristine 和dirty
判断依据是当前这个字段的值是否被修改过,如果没修改过pristine为true,dirty为false。
- pending
判断依据是如果当前我们需要校验的这个字段会发送一个异步的请求校验其结果,但是这个结果还没回来的时候,这个pending字段的属性就会为true
:alarm_clock:angular为这些状态值,还添加了他们所对应的样式:
- ng-touched
- ng-untouched
- ng-pristine
- ng-dirty
- ng-pending
- ng-valid
- ng-unvalid
我们可以根据自己的需求给这些样式提供定制化的样式
模版式表单校验
我们如果相对模版式表单进行校验,最关键是得需要指令进行辅助,因为在html中我们是没有一个模型与之呼应的。 (指令其实就是一个没有模版的组件)
自带的相关angular的校验器,我们直接可以在模版中进行使用,如下所示:
<form #myForm="ngForm" (ngSubmit)="onSubmit()" novalidate>
<div> 用户名:<input type="text" required minlength='6' #username="ngModel" ngModel name="username" /></div>
<div [hidden]="myForm.form.get('username').valid || myForm.form.get('username').untouched">
<!--hasError方法一共需要填入两个参数,第一个是你检验器返回的结果中的key的名字,而不是校验器的方法名-->
<!--第二个参数就是你要监控哪一个参数校验,就是formControlName的名称-->
<div [hidden] = "!myForm.form.hasError('required', 'username')" style=" color: red;font-size: 0.5em;">username 是必填项</div>
<div [hidden] = "!myForm.form.hasError('minlength', 'username')" style=" color: red;font-size: 0.5em;">username 的长度必须要大于6</div>
</div>
</form>
直接在标签上加入 required minlength='6' 就是对当前这个字段做了一个必填的验证和一个最小长度的验证,然后下方拿到对应的validate的验证值即可判断错误信息是否显示。
那么自定义的验证我们应该怎么搞呢?我们使用手机号码验证来做一个样例:
首先我们需要定义一个指令,通过命令 ng g directive directive/phone-validate
然后我们就需要在这个指令里,告诉他应该如何使用我们的验证方法了,代码如下:
import { Directive } from '@angular/core';
import { NG_VALIDATORS } from '@angular/forms';
import { validatePhone } from '../validator/validators';
@Directive({
selector: '[appPhoneValidate]',
providers: [{provide: NG_VALIDATORS, useValue: validatePhone, multi: true}]
})
export class PhoneValidateDirective {
constructor() { }
}
这其实用到的就是我们之前使用过的依赖注入的知识了,重点说一下providers属性,provide提供了token的名称,useValue说明具体哪个方法实现这个token,multi为true说明一个token可以被多个实现使用。
最后使用selector的值作为指令,在html模版中使用就可以了。如下:
<div> 手机号:<input type="text" ngModel name="phone" appPhoneValidate /></div>
<!--下面的代码是验证如果失败可以提示的消息-->
<div [hidden]="myForm.form.get('phone')?.valid || myForm.form.get('phone')?.untouched">
<div [hidden] = "!myForm.form.hasError('mobileValid', 'phone')" style=" color: red;font-size: 0.5em;">{{myForm.form.getError('mobileValid', 'phone')?.msg}}</div>
</div>
如果需要源码的话,可以直接访问github https://github.com/luckypoison/Angular4Form