由于最近公司框架升级,抛弃了原来手动检验表单的方式,将所有的表单改为响应式,由于之前没用过,在一开始我以为只有我没有用过,了解了小组里的其他同事得知基本都不是很熟悉
后面时间比较紧,没办法只能边做边学边改了,所以难免踩了一些坑,当然也花了一些时间学习,虽然对于熟悉的人来说可能很简单,但是还是将学习的过程和小结以及解决的问题的方法总结一下,也算是一种提炼。在这里更多的是理论结合实际业务需求来说,而不是一味的按照官方文档的方式写API介绍,如果那样就是学习笔记,而不是总结了。
为什么主要介绍响应式表单呢?因为响应式表单提供对底层表单对象模型直接、显式的访问。它们与模板驱动表单相比,更加健壮:它们的可扩展性、可复用性和可测试性都更高。适用于比较复杂的表单,其实最重要的是其他的我也不会呀。
一、响应式表单基本概念
1.FormControl 、FormArray 、FormGroup
1.FormControl: 用于追踪单个表单控件的值和验证状态,例如一个栏位绑定
//初始化一个栏位的值为测试名字,并且不可用
const Name:FormControl = new FormControl({value:'测试名字', disabled: true });
2.FormArray:用于追踪表单控件数组的值和状态,例如几个栏位一起,常用的表格或者在表单中嵌入表格
//定义表单对象的属性为aliases的FormArray
this.validateForm = this.fb.group({aliases: this.fb.array([]),});
//获取FormArray
get aliases() {return this.validateForm.get('aliases') as FormArray;}
//给FormArray 添加item
this.aliases.push(
this.fb.group({Id: 0,Name: [null],})
);
3.FormGroup:用于追踪单个表单控件的值和验证状态,它可以包含单个或多个FormControl 和 FormArray ,一般一个表单对应一个FormGroup实例,而表单的各个栏位对应FormControl 和FormArray ,当然他们可以互相嵌套,例如FormArray 中可以嵌套FormGroup,它的灵活性就是如此。
validateForm = new FormGroup({Name: new FormControl({value:'测试名字', disabled: true }),});
validateForm = this.fb.group({});
4.FormBuilder:是一个可注入的服务提供者,手动创建多个表单控件实例会非常繁琐,FormBuilder 服务提供了一些便捷方法来生成表单控件,以前每一个创建要先生成FormGroup 然后生成FormControl,而使用FormBuilder的group方法可以减少重复代码,说白了就是帮助方便生成表单
validateForm!: FormGroup;
//手动创建
validateForm = new FormGroup({
Name: new FormControl('测试名字'),
});
//FormBuilder表单构建器
validateForm = this.fb.group({
Name:[ { value:'测试名字',disabled:true}],
});
2.Validator 表单验证
表单验证用于确保用户的输入是完整和正确的。如何把单个验证器添加到表单控件中,以及如何显示表单的整体状态,通常验证器返回null表示所有的验证通过。
1.同步验证器:同步验证器函数接受一个控件实例,然后返回一组验证错误或 null
,在实例化FormControl 时可以将他作为第二个参数传入
//formControlName的值必须和ts代码中FormControl 的实例一致
<input type="text" id="name" class="form-control" formControlName="name" required>
//判断对应的FormControl 是否没通过校验 而有错误信息
<div *ngIf="name.errors?.['required']">
Name is required.
</div>
//初始化一个栏位并且加入必填校验验证器
const name:FormControl = new FormControl({'测试名字', disabled: true },Validators.required,);
//获取这个FormControl
get name() { return this.heroForm.get('name'); }
2.异步验证器:异步函数接受一个控件实例并返回一个 Promise 或 Observable ,只有在所有同步验证器都通过之后,Angular 才会运行异步验证器,在实例化FormControl 时可以将他作为第三个参数传入
3.内置验证器:例如验证一些长度,不能为空可以使用已经提供的Validator 类来实现
4.自定义验证器:系统内部提供的验证器不能满足现有需求,可以使用自定义验证器做一些个性化的校验,自定义校验器必须返回ValidationErrors类型或者空
//formControlName的值必须和ts代码中FormControl 的实例一致
<input type="text" id="name" class="form-control" formControlName="name" required>
//判断对应的FormControl 是否没通过校验 而有错误信息
<div *ngIf="name.hasError('Invalid')">
名字也太长了吧....
</div>
//初始化一个栏位并且加入必填校验验证器
const name:FormControl = new FormControl({'测试名字', disabled: true },this.CustomValidators());
CustomValidators() {
return (control: AbstractControl): ValidationErrors | null => {
if(control.value.length!=10)
{
return {Invalid:true}
}
return null;
};
}
3.表单及元素的基本方法和属性
- 方法
方法 | 使用效果 |
---|---|
setValue() | 使用setVlue可以设置控件FormControl 的值,但是使用时必须FormGroup所有的属性一起赋值,不能单个赋值,常用在修改加载赋值。 |
patchValue() | 使用patchValue也可以设置FormControl的值,可以根据需要设置指定的FormControl,而不需要全部设置,常用在更新某个字段值 |
reset () | FormControl 中使用重置当前控件所有状态,FormGroup中使用就是重置表单对象里的内容,例如控件被设为不可用disabled,control.reset({ value: 'Drew', disabled: true }); |
markAsPristine() | 是将表单控件值标记为未改变,这个方法主要用在表单重置时,此时它的状态pristine为true |
markAsDirty() | 是将表单FormControl 控件值标记为已改变,此时它的状态Dirty为true |
updateValueAndValidity() | 重新计算表单FormControl 控件的值和验证状态等 |
setValidators() | 给表单FormControl 控件设置验证器,如果设置多个就用数组"setValidators([v1,v2,v3])" ,串行设置是覆盖的关系,不是追加 |
disable() | 给表单FormControl 控件设置不可用,注意当FormControl 是disabled时,表单的常规取值getValue()对应值会为空,可用getRawValue()取原始值对象得到对应FormControl 的值 |
enable() | 给表单FormControl 控件设置启用 |
- 属性
属性 | 使用方法说明 |
---|---|
touched | 当表单FormControl 控件 的touched为true表示控件已经被获取焦点,反之同理 |
untouched | 当untouched 为true表示控件未被获取焦点,反之同理 |
pristine | 表示表单元素是纯净的,用户未操作过,可以使用markAsPristine方法设为true |
dirty | 表示表单元素是已被用户操作过,可以使用markAsDirty方法设为true |
status | 获取表单FormControl 控件上的的状态 |
Errors | 获取当前控件的错误信息 |
二.实例分析及应用
1. 简单的表单实现
需求1
我们主要用到的框架版本是Angular 12 + NG-ZORRO, 所以在下面很多实现和示例代码将与他们有关,虽然可能代码不一样,但也只是在UI层面的区别稍微大一点点,但对于TS代码,只是换汤不换药,稍微注意一下就好了,其实下面实例中的需求,基本就是我在工作时需要做的的一些基本内容和遇到的问题,经过查阅资料后解决的思路和过程,甚至截图都一模一样。
实现最基本的表单新增功能并且校验员工ID为必填以及长度不能超过50,要实现的效果图如下
分析
1.首先需求未提出有特殊注意点,基本都是简单的输入框赋值然后保存,只要基本的概念搞清楚实现这种最简单
2.我们用一个FormGroup和6个FormControl 完成和界面绑定即可
3.绑定验证器用于校验长度和必填
实现步骤
1.定义html 表单结构
<!-- formGroup 属性绑定表单对象 -->
<form nz-form [formGroup]="validateForm" nzLayout="vertical">
<nz-form-label nzRequired>Employee ID
</nz-form-label>
<!-- Employee_ErrorTrip为验证不通过弹出的提示信息 -->
<!-- formControlName绑定表单元素FormControl -->
<nz-form-control [nzErrorTip]="Employee_ErrorTrip">
<input nz-input formControlName="EmployeeID" placeholder="" />
</nz-form-control>
<ng-template #Employee_ErrorTrip let-control>
<ng-container *ngIf="control.hasError('required')">
员工编号为必填项目
</ng-container>
</ng-template>
</form>
2.在TypeScript代码中声明表单对象,在构造函数中注入FormBuilder,并且在ngOnInit中进行表单初始化
//定义表单对象
validateForm:FormGroup;
//构造函数注入FormBuilder
constructor(private fb: FormBuilder){}
//在声明周期钩子函数中初始化表单
ngOnInit() {
//初始化并且绑定必填验证器和长度验证器
this.validateForm = this.fb.group({
EmployeeID: ['', [Validators.required, Validators.maxLength(50)]],
})
}
2.在表格中应用表单
需求2
需要实现表格的表单新增和提交以及个性化定制需求,要实现的效果图和需求描述如下
1.点击Add 添加一行表格 ,编辑完毕,点击Save保存数据,点击Revoke取消编辑
2.默认开始时间和结束时间禁止使用
3.当选择Contract Type为 “短期合同” Contract start date 和Contract end date可用,当选择Contract Type为 “长期合同”不可用
4.如果Contract start date 和Contract end date可用,需要验证开始结束时间合法性,例如开始事件不能超过结束时间
分析
1.在表格中使用表单,虽然表单在表格中,但是他的每一列同样都是一个个FormControl
2.一共4列需要输入值,就说明有4个FormControl 然后最后一列就是2个按钮
3.我们根据上面的基础知识知道,FormControl 不能单独使用,所以需要被FormGroup包裹,此时说明一行对应一个FormGroup
4.由一行对应一个FormGroup知道,我们的表格时多行的,也就是有多个FormGroup,我们可以使用FormArray来存储,因为他代表一组表单组
5.根据需求第2点默认开始时间和结束时间禁止使用,我们知道在一开始初始化时,设置开始结束时间对应的FormControl 为disabled就行了
6.第3点需求需要涉及联动,也就是当Contract Type对应的FormControl 的值为“短期合同”时,需要将 “开始结束时间”对应的FormControl设置为可用,这个需要自定义验证器来完成
实现步骤
1.首先定义Html表单结构
<nz-table [nzData]="CONTRACTS" nzTableLayout="fixed" [nzShowQuickJumper]="true">
<thead>
<tr>
<th>Contract type</th>
<th>Contract start date</th>
<th>Contract end date</th>
<th>Agreement item</th>
<th>Operation</th>
</tr>
</thead>
<tbody>
<!-- 绑定表单组属性aliases -->
<ng-container formArrayName="aliases">
<!-- 将表单组中当前行的索引与formGroup绑定 -->
<tr [formGroupName]="i" *ngFor="let data of aliases.controls;index as i">
<td>
<nz-form-item>
<nz-form-control nzSpan="1-24">
<!-- AccountName绑定FormControl -->
<nz-select nzShowSearch nzAllowClear nzPlaceHolder="" formControlName="Type">
<nz-option *ngFor="let option of Type" [nzValue]="option.Code" [nzLabel]="option.Value">
</nz-option>
</nz-select>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control nzSpan="1-24" [nzErrorTip]="StartDate">
<nz-date-picker id="StartDate" formControlName="StartDate" nzPlaceHolder="">
</nz-date-picker>
<!-- 校验提示模板用于时间验证器 -->
<ng-template #StartDate let-control>
<!-- 判断时间验证器是否存在beginGtendDate属性,如果有说明没有通过验证 然后展示提示信息 -->
<ng-container *ngIf="control.hasError('beginGtendDate')">
开始时间不能晚于结束时间
</ng-container>
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control nzSpan="1-24" [nzErrorTip]="EndDate">
<nz-date-picker style="width: 100%;" formControlName="EndDate" nzPlaceHolder="">
</nz-date-picker>
<ng-template #EndDate let-control>
<ng-container *ngIf="control.hasError('beginGtendDate')">
开始时间不能晚于结束时间
</ng-container>
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control nzSpan="1-24">
<nz-select nzShowSearch nzAllowClear nzPlaceHolder="" formControlName="ContractType">
<nz-option *ngFor="let option of ContractTypes" [nzValue]="option.Code" [nzLabel]="option.Value">
</nz-option>
</nz-select>
</nz-form-control>
</nz-form-item>
</td>
<td>
<button style="color: #009688;" nz-button nzType="text">
<i nz-icon nzType="save"></i>Save
</button>
<button nz-button nzType="text" nzDanger>
<i nz-icon nzType="redo"></i>Revoke
</button>
</td>
</tr>
</ng-container>
</tbody>
</nz-table>
2.在TypeScript代码中声明表单对象validateForm,然后初始化一个FormArray类型的属性aliases的实例作为表格formArrayName的值
3.点击Add按钮时向表单对象validateForm的属性aliases添加一条数据
4.定义Contract Type 联动的自定义校验器 contractTypeValidation()方法
5.定义时间校验器 timeValidation()方法,如果时间不合法,将FormControl的错误状态设置属性beginGtendDate,然后在模板中根据这个属性来选择是否渲染日式信息
//定义表单对象
validateForm:FormGroup;
//构造函数注入FormBuilder
constructor(private fb: FormBuilder){}
//在声明周期钩子函数中初始化一个表单对象validateForm
ngOnInit() {
this.validateForm = this.fb.group({
aliases: this.fb.array([]),
});
}
//声明aliases属性用作界面formArrayName绑定
get aliases(){
return this.validateForm.get('aliases') as FormArray;
}
addNewRow() {
const group = this.fb.group({
//添加给Type初始化验证器
Type: [null, [CommonValidators.required, this.contractTypeValidation()]],
//初始化禁用StartDate和EndDate的FormControl
StartDate: [{ value: null, disabled: true }, []],
EndDate: [{ value: null, disabled: true },[]],
ContractType: [null, [CommonValidators.required, CommonValidators.maxLength(20)]],
})
this.aliases.push(group);
}
//自定义Contract Type验证器
contractTypeValidation() {
return (control: AbstractControl): ValidationErrors | null => {
let contents: any[] = this.validateForm.value.aliases;
if (control.touched && !control.pristine) {
//获取表单组
const formArray: any = this.validateForm.controls.aliases;
//找到正在编辑的行的索引
const index = contents.findIndex((x) => !x.isShowEdit);
//获取开始结束时间FormControl 实例
const StartDate: AbstractControl =
formArray.controls[index].get('StartDate'),
EndDate: AbstractControl = formArray.controls[index].get('EndDate');
if (control.value === "短期合同") {
//给开始结束时间设置验证器用于验证时间合法性
StartDate.setValidators([CommonValidators.required, this.timeValidation()]);
EndDate.setValidators([this.timeValidation()]);
//启动开始结束时间控件
EndDate.enable();
StartDate.enable();
} else {
//Contract Type不是短期合同就清除验证器
StartDate.clearValidators();
EndDate.clearValidators();
//禁用开始结束时间
EndDate.disable();
StartDate.disable();
}
}
return null;
}
}
//自定义时间验证器
timeValidation()
{
return (control: AbstractControl): ValidationErrors | null => {
if (!control.pristine) {
let contents: any[] = this.validateForm.value.aliases;
const formArray: any = this.validateForm.controls.aliases;
const index = contents.findIndex((x) => !x.isShowEdit);
//获取开始结束时间FormControl实例
const EndDate: string = formArray.controls[index].get('EndDate').value;
const StartDate: string =formArray.controls[index].get('StartDate').value;
if (EndDate === null || StartDate === null) return null;
//如果时间不合法,那就设置当前控件的错误状态 beginGtendDate为true
if (
Date.parse(control.value) > Date.parse(EndDate) ||
Date.parse(control.value) < Date.parse(StartDate)
) {
return { beginGtendDate: true };
}
}
return null;
}
}