今天要来用 Reactive Forms 的方式再来实作一次昨天的表单。
具体的规格需求跟昨天差不多,如下所示:
姓名至少需两个字以上
姓名最多只能十个字
此栏位为必填
规格需求看清楚之後,我们就来开始实作吧!
首先我们一样先准备好基本的 HTML :
<form *ngIf="formGroup" [formGroup]="formGroup" (ngSubmit)="submit()">
<fieldset>
<legend>被保人</legend>
<p>
<label for="name">姓名:</label>
<input type="text" id="name" formControlName="name" />
<span class="error-message">{{ getErrorMessage("name") }}</span>
</p>
<p>
性别:
<input type="radio" id="male" value="male" formControlName="gender" />
<label for="male">男</label>
<input type="radio" id="female" value="female" formControlName="gender" />
<label for="female">女</label>
</p>
<p>
<label for="age">年龄:</label>
<select id="age" formControlName="age">
<option value="">请选择</option>
<option value="18">18岁</option>
<option value="20">20岁</option>
<option value="70">70岁</option>
<option value="75">75岁</option>
</select>
<span class="error-message">{{ getErrorMessage("age") }}</span>
</p>
<p><button type="button">删除</button></p>
</fieldset>
<p>
<button type="button">新增被保险人</button>
<button type="submit">送出</button>
</p>
</form>
未经美化的画面跟昨天长得一样:
接着跟昨天一样先把它当成静态表单来准备相关的属性与方法:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-template-driven-forms-async-insured',
templateUrl: './template-driven-forms-async-insured.component.html',
styleUrls: ['./template-driven-forms-async-insured.component.scss']
})
export class TemplateDrivenFormsAsyncInsuredComponent {
/**
* 绑定在表单上
*
* @type {(FormGroup | undefined)}
* @memberof ReactiveFormsAsyncInsuredComponent
*/
formGroup: FormGroup | undefined;
/**
* 透过 DI 取得 FromBuilder 物件,用以建立表单
*
* @param {FormBuilder} formBuilder
* @memberof ReactiveFormsAsyncInsuredComponent
*/
constructor(private formBuilder: FormBuilder) {}
/**
* 当 Component 初始化的时候初始化表单
*
* @memberof ReactiveFormsAsyncInsuredComponent
*/
ngOnInit(): void {
this.formGroup = this.formBuilder.group({
name: [
'',
[Validators.required, Validators.minLength(2), Validators.maxLength(10)]
],
gender: ['', Validators.required],
age: ['', Validators.required]
});
}
/**
* 透过栏位的 Errors 来取得对应的错误讯息
*
* @param {string} key
* @param {number} index
* @return {*} {string}
* @memberof ReactiveFormsAsyncInsuredComponent
*/
getErrorMessage(key: string): string {
const formControl = this.formGroup?.get(key);
let errorMessage: string;
if (!formControl || !formControl.errors || formControl.pristine) {
errorMessage = '';
} else if (formControl.errors.required) {
errorMessage = '此栏位必填';
} else if (formControl.errors.minlength) {
errorMessage = '姓名至少需两个字以上';
} else if (formControl.errors.maxlength) {
errorMessage = '姓名至多只能输入十个字';
}
return errorMessage!;
}
/**
* 绑定在表单上,当按下送出按钮时会触发此函式
*
* @memberof TemplateDrivenFormsAsyncInsuredComponent
*/
submit(): void {
// do submit...
}
}
准备好相关的属性和方法之後,我们直接把他们跟 Template 绑定:
<form *ngIf="formGroup" [formGroup]="formGroup" (ngSubmit)="submit()">
<fieldset>
<legend>被保人</legend>
<p>
<label for="name">姓名:</label>
<input
type="text"
id="name"
formControlName="name"
/>
<span class="error-message">{{ getErrorMessage('name') }}</span>
</p>
<p>
性别:
<input type="radio" id="male" value="male" formControlName="gender">
<label for="male">男</label>
<input type="radio" id="female" value="female" formControlName="gender">
<label for="female">女</label>
</p>
<p>
<label for="age">年龄:</label>
<select id="age" formControlName="age">
<option value="">请选择</option>
<option value="18">18岁</option>
<option value="20">20岁</option>
<option value="70">70岁</option>
<option value="75">75岁</option>
</select>
<span class="error-message">{{ getErrorMessage('age') }}</span>
</p>
<p><button type="button">删除</button></p>
</fieldset>
<p>
<button type="button">新增被保险人</button>
<button type="submit">送出</button>
</p>
</form>
目前为止,大体上跟我们上次的实作差不多,应该没有什麽难度。
不过这次绑定 FormControl
的方式,我改成用 formControlName="name"
,而不是上次的 [formControl]="nameControl"
,大家可以自行选用喜欢的方式。
如果大家在这边有遇到问题,可以检查看看自己有没有引入
FormsModule
与ReactiveFormsModule
,我就不再赘述罗。
目前的结果:
有了基本的互动效果之後,我们就可以开始来思考怎麽样把这个表单变成动态的。
跟昨天一样的是,既然我们要让被保人可以被新增或删除,表示我们应该是会用阵列来表达这些被保人的资料,也就是说,我们现在的 FormGroup
要从 1 个变成 N 个。
之前曾经提到,我们如果从资料面来看, {}
代表表单,也就是 FormGroup
; ''
代表表单里的子栏位,也就是 FormControl
;那 []
呢?
答案是 ─ FormArray
!
不过 FormArray
不能直接跟 form
元素绑定,唯一可以跟 form
元素绑定的只有 FormGroup
,所以 FormArray
一定要在 FormGroup
里面,就像这样:
this.formGroup = this.formBuilder.group({
insuredList: this.formBuilder.array([])
});
这边要注意的是, FormArray
一定要透过 FormBuilder
或是 FormArray
的建构式来建立,像上面示范的那样,或是这样:
this.formGroup = this.formBuilder.group({
insuredList: new FormArray([])
});
绝对不能偷懒写成这样:
this.formGroup = this.formBuilder.group({
insuredList: []
});
这样的话,就会变成普通的 FormControl
罗!切记切记!
接着我们就可以将原本的程序码修改成用阵列的方式,并把新增被保人、删除被保人与判断表单是否有效的函式都补上:
@Component({
// 省略...
})
export class AppComponent implements OnInit {
/**
* 绑定在表单上
*
* @type {(FormGroup | undefined)}
* @memberof ReactiveFormsAsyncInsuredComponent
*/
formGroup: FormGroup | undefined;
/**
* 用以取得 FormArray
*
* @readonly
* @type {FormArray}
* @memberof ReactiveFormsAsyncInsuredComponent
*/
get formArray(): FormArray {
return this.formGroup?.get('insuredList')! as FormArray;
}
/**
* 绑定在送出按钮上,判断表单是不是无效
*
* @readonly
* @type {boolean}
* @memberof ReactiveFormsAsyncInsuredComponent
*/
get isFormInvalid(): boolean {
return this.formArray.controls.length === 0 || this.formGroup!.invalid;
}
/**
* 透过 DI 取得 FromBuilder 物件,用以建立表单
*
* @param {FormBuilder} formBuilder
* @memberof ReactiveFormsAsyncInsuredComponent
*/
constructor(private formBuilder: FormBuilder) {}
/**
* 当 Component 初始化的时候初始化表单
*
* @memberof ReactiveFormsAsyncInsuredComponent
*/
ngOnInit(): void {
this.formGroup = this.formBuilder.group({
insuredList: this.formBuilder.array([])
});
}
/**
* 新增被保人
*
* @memberof ReactiveFormsAsyncInsuredComponent
*/
addInsured(): void {
const formGroup = this.createInsuredFormGroup();
this.formArray.push(formGroup);
}
/**
* 删除被保人
*
* @param {number} index
* @memberof ReactiveFormsAsyncInsuredComponent
*/
deleteInsured(index: number): void {
this.formArray.controls.splice(index, 1);
this.formArray.updateValueAndValidity();
}
/**
* 送出表单
*
* @memberof ReactiveFormsAsyncInsuredComponent
*/
submit(): void {
// do login...
}
/**
* 透过栏位的 Errors 来取得对应的错误讯息
*
* @param {string} key
* @param {number} index
* @return {*} {string}
* @memberof ReactiveFormsAsyncInsuredComponent
*/
getErrorMessage(key: string, index: number): string {
const formGroup = this.formArray.controls[index];
const formControl = formGroup.get(key);
let errorMessage: string;
if (!formControl || !formControl.errors || formControl.pristine) {
errorMessage = '';
} else if (formControl.errors.required) {
errorMessage = '此栏位必填';
} else if (formControl.errors.minlength) {
errorMessage = '姓名至少需两个字以上';
} else if (formControl.errors.maxlength) {
errorMessage = '姓名至多只能输入十个字';
}
return errorMessage!;
}
/**
* 建立被保人的表单
*
* @private
* @return {*} {FormGroup}
* @memberof ReactiveFormsAsyncInsuredComponent
*/
private createInsuredFormGroup(): FormGroup {
return this.formBuilder.group({
name: [
'',
[Validators.required, Validators.minLength(2), Validators.maxLength(10)]
],
gender: ['', Validators.required],
age: ['', Validators.required]
});
}
}
接着我们到 Template 里,把原本绑定的方式调整一下:
<form *ngIf="formGroup" [formGroup]="formGroup" (submit)="submit()">
<ng-container
formArrayName="insuredList"
*ngFor="let control of formArray.controls; let index = index"
>
<fieldset [formGroupName]="index">
<legend>被保人</legend>
<p>
<label [for]="'name-' + index">姓名:</label>
<input type="text" [id]="'name-' + index" formControlName="name" />
<span class="error">{{ getErrorMessage("name", index) }}</span>
</p>
<p>
性别:
<input
type="radio"
[id]="'male-' + index"
value="male"
formControlName="gender"
/>
<label [for]="'male-' + index">男</label>
<input
type="radio"
[id]="'female-' + index"
value="female"
formControlName="gender"
/>
<label [for]="'female-' + index">女</label>
</p>
<p>
<label [for]="'age-' + index">年龄:</label>
<select name="age" [id]="'age-' + index" formControlName="age">
<option value="">请选择</option>
<option value="18">18岁</option>
<option value="20">20岁</option>
<option value="70">70岁</option>
<option value="75">75岁</option>
</select>
<span class="error">{{ getErrorMessage("age", index) }}</span>
</p>
<p><button type="button" (click)="deleteInsured(index)">删除</button></p>
</fieldset>
</ng-container>
<p>
<button type="button" (click)="addInsured()">新增被保险人</button>
<button type="submit" [disabled]="isFormInvalid">送出</button>
</p>
</form>
初次看到这种绑定方式的 Angular 初学者可能会傻眼,不过静下心来看之後你会发现,其实这只是我们所建立的 FormGroup
里的阶层关系,这样绑定 Angular 才能从一层层的表单之中开始往下找。
如果我们把其他的 HTML 都拿掉的话其实会清楚很多:
<form *ngIf="formGroup" [formGroup]="formGroup" (submit)="submit()">
<!-- 其他省略 -->
</form>
最外层的这个大家应该都知道,就是我们在 .ts
里的 formGroup
。
<form *ngIf="formGroup" [formGroup]="formGroup" (submit)="submit()">
<ng-container
formArrayName="insuredList"
*ngFor="let control of formArray.controls; let index = index"
>
<!-- 其他省略 -->
</ng-container>
</form>
而这里呢,就像我们写静态表单的时候,会从 FormGroup
里根据对应的 key
值找到对应的 FormControl
一样,这里则是把对应的 FormArray
找出来。
然後再用 *ngFor
的方式,把 FormArray
底下的 AbstractControl
都回圈出来。
关於
AbstractControl
,它其实是一个抽象类别,而FormGroup
、FormArray
与FormControl
这三种类型其实都继承於这个类别,所以大家不知道有没有注意到,一般我们在.ts
里使用的时候,我们会特别用as FormControl
或是as FormArray
的方式来让编译器知道现在取得的物件实体是什麽型别,以便後续使用。想知道更多
AbstractControl
的资讯的话,请参考官方 API 文件: https://angular.io/api/forms/AbstractControl 。
<form *ngIf="formGroup" [formGroup]="formGroup" (submit)="submit()">
<ng-container
formArrayName="insuredList"
*ngFor="let control of formArray.controls; let index = index"
>
<fieldset [formGroupName]="index">
</fieldset>
</ng-container>
</form>
最後再用索引值 index
找出对应的 FormGroup
。
而要做这件事情其实要有相对应的阶层关系的 HTML 来帮忙,但因为我的 HTML 的阶层关系少一层,所以我才会用 ng-container
多做一层阶层,好让我的表单可以顺利绑上去。
如果今天你做的 HTML 的阶层数是足够的,就可以不用用 ng-container
多做一层阶层,例如把上面的 HTML 改成这样其实也可以:
<form *ngIf="formGroup" [formGroup]="formGroup" (submit)="submit()">
<div
formArrayName="insuredList"
*ngFor="let control of formArray.controls; let index = index"
>
<fieldset [formGroupName]="index">
</fieldset>
</div>
</form>
不过用 ng-container
的好处是这个元素并不会真的出现在画面上,大家可以视情况斟酌使用。
改完之後就大功告成罗!来看看最後的结果:
今天的学习重点主要是在围绕在 FormArray
上,因为多了这个阶层的关系,所以在与 Template 的绑定上看起来会较为复杂一点点。
话虽如此,大家可以拿今天的 template 与昨天的 template 互相比较一下,除了 for
与 id
这两个属性因为天生局限的关系真的没办法之外,但 name
的部份就不用再去处理了,还是很方便的。
今天的程序码我会放在 Github - Branch: day11 上供大家参考,建议大家在看我的实作之前,先按照需求规格自己做一遍,之後再跟我的对照,看看自己的实作跟我的实作不同的地方在哪里、有什麽好处与坏处,如此反覆咀嚼消化後,我相信你一定可以进步地非常快!
如果有任何的问题或是回馈,也都非常欢迎留言给我让我知道噢!
Monitor applications and services Azure Monitor An...
Ceph and OpenStack 现在已经是 IaaS 中成双成对的一个组合。根据 2017 年...
这是 Roblox 从零开始系列,入门章节的第十二个单元,今天你将学会如何把分数显示在右上角的玩家仪...
完赛的内心小剧场 心得 感谢坚持到最後的自己,本次铁人赛完赛了,最後一篇就来点软性的心得文吧! 因...
Button和Toast 今天要介绍的是Button这个常在程序中能看到的元件,在Button的属性...