经过了这段时间的练习与学习,相信大家应该越来越能体会 Angular 表单的强大与便利。
不过既然 Angular 表单这麽好用,如果能让自己做的 Component 也像 Angular 表单那样一般使用该有多好?
因此,今天想要跟大家分享的是 ─ 如何自订表单元件。
大家跟我一起想像一下,假设我们今天需要做一个管理平台,在这个管理平台里,会有很多地方都会需要用到我们昨天做的 DateRangeComponent
,但不一定会是在同一个表单里,只是刚好也需要 startDate
与 endDate
这两个栏位,而且画面与栏位验证的规则也都是一样。
例如:
A 页面是一个查询订单系统, B 页面是查询会员系统,虽然这两个页面的查询条件可能都不太一样,但恰好都可以根据起迄日来查询相应的资料。
这时,我们很有可能就会将我们做好的 DateRangeComponent
做成表单元件,让 A 跟 B 在使用它的时候,就像使用一般的表单元件一样轻松、自然。
那究竟要怎麽做呢?
首先要介绍给大家认识的是 ControlValueAccessor
,它是个 Interface
,而它定义了以下四个函式:
interface ControlValueAccessor {
writeValue(obj: any): void
registerOnChange(fn: any): void
registerOnTouched(fn: any): void
setDisabledState(isDisabled: boolean)?: void
}
writeValue(obj: any): void
─ 表单控件想要将值写入时,会呼叫此函式registerOnChange(fn: any): void
─ 表单控件初始化时会呼叫此函式,并传入一个回呼函式,让实作此介面的类别在其值有变动时,使用该回呼函式并传入欲变动的值
registerOnTouched(fn: any): void
─ 表单控件初始化时会呼叫此函式,并传入一个回呼函式,让实作此介面的类别在失去焦点时,使用该回呼函式以通知表单控件
setDisabledState(isDisabled: boolean)?: void
─ 当表单控件的状态变成 DISABLED
抑或是从 DISABLED
改变成其他状态时,会呼叫此函式以通知实作此介面的类别虽然我觉得我说的满清楚的,但大家应该还是觉得很模糊,对吧?
不要紧,我只是先让大家有个印象,待会实作时大家就会更加理解了。
首先,我们需要另一个 Component 来用我们昨天做好的 DateRangeComponent
,像这样:
<form *ngIf="formGroup" [formGroup]="formGroup">
<app-date-range formControlName="dateRange"></app-date-range>
</form>
然後在 Component 的 .ts
里准备好 FormGroup
,像这样:
export class ReactiveFormsDateRangeComponent implements OnInit {
formGroup: FormGroup | undefined;
constructor(private formBuilder: FormBuilder) { }
ngOnInit(): void {
this.formGroup = this.formBuilder.group({ dateRange: '' });
}
}
接着打开昨天做的 DateRangeComponent
,并在 implements
的後方加上 ControlValueAccessor
,像这样:
export class DateRangeComponent implements OnInit, ControlValueAccessor {
// ...
}
这时你应该会发现 DateRangeComponent
出现了一条红色毛毛虫,当你把滑鼠游标移到上面的时候,它说:
这是因为我们为 DateRangeComponent
加上实作 ControlValueAccessor
的宣告後,编辑器提醒我们要记得实作 ControlValueAccessor
的四个函式,才符合该介面的定义。
这就像是我们如果想要 Cosplay 钢铁人,但我什麽盔甲都没穿就说自己是钢铁人,别人只会觉得满脸问号。
但只要我们戴上了头盔,别人就会知道你在扮演钢铁人。
所以我们就在 DateRangeComponent
里加上以下四个函式:
export class DateRangeComponent implements OnInit, ControlValueAccessor {
// ...
writeValue(obj: any): void {
console.log('writeValue', obj);
}
registerOnChange(fn: any): void {
console.log('registerOnChange', fn);
}
registerOnTouched(fn: any): void {
console.log('registerOnTouched', fn);
}
setDisabledState(isDisabled: boolean): void {
console.log('setDisabledState', isDisabled);
}
}
接下来,我们需要在 DateRangeComponent
的 MetaData 里的 providers
里加入一些设定,像这样:
@Component({
selector: 'app-date-range',
templateUrl: './date-range.component.html',
styleUrls: ['./date-range.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DateRangeComponent),
multi: true
}
]
})
export class DateRangeComponent implements OnInit, ControlValueAccessor {
// ...
}
我们之前其实也曾经在第二十五天的文章 ─ 测试进阶技巧 - DI 抽换里用过类似的技巧。
简单来说,这个设定是为了让表单可以透过 NG_VALUE_ACCESSOR
这个 InjectionToken 取得我们这个实作了 ControlValueAccessor
介面的 DateRangeComponent
实体。
想知道什麽是 InjectionToken 的朋友,可以参考 Mike 的 [Angular 大师之路] Day 23 - 认识 InjectionToken 。
想知道
useExisting
跟useValue
、useClass
与useFactory
有哪里不一样的,也可以参考 Mike 的 [Angular 大师之路] Day 20 - 在 @NgModule 的 providers: [] 自由更换注入内容 (1) 与 [Angular 大师之路] Day 21 - 在 @NgModule 的 providers: [] 自由更换注入内容 (2) 。而
forwardRef()
的部份,我觉得官网的 Dependency injection in action - Break circularities with a forward class reference 讲得比较清楚。最後的
multi: true
,可以参考林颖平 EP 的 [Day 8] 所以我说那个 multi 是? ,如果想要更深入的了解其原理,他也写了一篇 [Day 10] 深度看一下 Angular 建立 multi provider 的机制(真的很深入)
至此,我们就可以储存档案来看一下初始化完後会印出的 Log :
接着我们在使用 DateRangeComponent
的 Component 里加上以下程序码以观察其运作结果:
ngOnInit(): void {
this.formGroup = this.formBuilder.group({
dateRange: ''
});
setTimeout(() => {
console.log('---- 3秒後 ----');
this.formGroup?.setValue({ dateRange: 'Leo' });
this.formGroup?.disable();
}, 3000);
}
然後我们会发现:
这样大家有比较了解一开始关於 ControlValueAccessor
各函式的说明了吗?
如果用图示的话,现在的结构大概像这样:
如果我们设值给 FormControl
时,则会触发 ControlValueAccessor
的函式 writeValue
:
如果我们 disable
或 enable
了该 FormControl
,则会触发 ControlValueAccessor
的函式 setDisabledState
:
而如果使用者改动了自订的表单元件的值,则我们自订的表单元件应该要呼叫透过初始化时所触发的 registerOnChange
所传入的 fn
去通知 FormControl
:
读万卷书不如行万里路。接下来,我们把剩下的实作做完就会更了解这其中的运作流程了!
首先,先加工一下使用 DateRangeComponent
的 Component :
export class ReactiveFormsDateRangeComponent implements OnInit {
formGroup: FormGroup | undefined;
constructor(private formBuilder: FormBuilder) { }
ngOnInit(): void {
const date = new Date();
this.formGroup = this.formBuilder.group({
dateRange: `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
});
}
enable(): void {
this.formGroup?.enable();
}
disable(): void {
this.formGroup?.disable();
}
}
Template 的部份也加工一下:
<form *ngIf="formGroup" [formGroup]="formGroup">
<app-date-range formControlName="dateRange"></app-date-range>
<p>
<button type="button" [disabled]="formGroup.disabled" (click)="disable()">DISABLE</button>
<button type="button" [disabled]="formGroup.enabled" (click)="enable()">ENABLE</button>
</p>
</form>
<pre>{{ formGroup?.getRawValue() | json }}</pre>
然後把 DateRangeComponent
改成这样:
export class DateRangeComponent implements OnInit, ControlValueAccessor {
formGroup: FormGroup | undefined;
fnFormRegisterOnChange: ((dateString: string) => void) | undefined;
fnFormRegisterOnTouched: (() => void) | undefined;
constructor(private formBuilder: FormBuilder) { }
ngOnInit(): void {
this.formGroup = this.formBuilder.group({
startDate: ['', [Validators.required, Validators.pattern(/^\d{4}-\d{2}-\d{2}$/)]],
endDate: ['', Validators.pattern(/^\d{4}-\d{2}-\d{2}$/)]
}, { validators: dateRangeValidator });
this.formGroup.valueChanges.subscribe(({ startDate, endDate }) => {
let dateString = startDate;
if (endDate) {
dateString += `, ${endDate}`;
}
if (this.formGroup?.errors) {
dateString = '';
}
if (this.fnFormRegisterOnChange) {
this.fnFormRegisterOnChange(dateString);
}
});
}
writeValue(dateRangeString: string): void {
const [startDate, endDate] = dateRangeString.split(', ');
this.formGroup?.patchValue({ startDate, endDate }, {
emitEvent: false
});
}
registerOnChange(fn: (dateRangeString: string) => void): void {
this.fnFormRegisterOnChange = fn;
}
registerOnTouched(fn: () => void): void {
this.fnFormRegisterOnTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
if (isDisabled) {
this.formGroup?.disable();
} else {
this.formGroup?.enable();
}
}
}
结果:
对了,这样的作法不仅仅只适用於 Reactive Forms 噢!大家可以在使用
DateRangeComponent
的时候用 Template Driven Forms 的方式试试看,也是行得通的唷!
今天的实作练习应该满好玩的吧?
我能理解大家第一次碰到的时候都会比较难以理解,记得我第一次碰到的时候,也只是复制人家的程序码然後贴上而已,根本就不是了解其运作原理。
因此,希望我今天的文章能让大家可以不仅仅只是复制贴上,而是对於其流程与原理有所掌握与理解。
今天的程序码会放在 Github - Branch: day28 上供大家参考,建议大家在看我的实作之前,先按照需求规格自己做一遍,之後再跟我的对照,看看自己的实作跟我的实作不同的地方在哪里、有什麽好处与坏处,如此反覆咀嚼消化後,我相信你一定可以进步地非常快!
如果有任何的问题或是回馈,还请麻烦留言给我让我知道!
>>: Day 29 : C语言 - 河内塔的程序递回执行顺序为何?
今天要学习的依然是 node.js 的core modules (就是内建的模组啦),主角是:fs ...
延续昨天Day_07 有线网路应用(一)进度 讨论一些我遇到的问题与细节补充。 Troublesho...
我喜欢艺术,也喜欢程序码 一开始因为工作关系强迫自己要学会程序码,原本不能接受,甚至觉得我没有天份在...
-NIST SP800-160 V1 和 ISO 15288 工程 是一种方法,它涉及一系列应用知...
当花了很多时间整理经营作品集後,得到主动来信询问真的会非常感动,可以感受到自己是被社会所需要的,在肯...