利用 Angular 動態元件,製作高彈性客製化頁面! (上)
前言
大家好! 不知道各位有沒有看過儀表板 (Dashboard) 類型的網頁?
主要是由多個圖表或是數據視覺化的元件組成的頁面
例如這種:
(來源: ngx-admin)
我接觸到的業務會大量製作這類型的網頁
隨著遇到的需求越來越複雜,用到的功能也越來越進階
遇到比較特別的需求是
希望可以讓使用者自訂要顯示哪一些圖表,客製化屬於自己想看的頁面
這時候「先在 HTML 寫好要顯示什麼內容」這種常規的方式就沒有辦法滿足需求了更不用說需求往往會在功能做完之後才出現 :)
所以本系列會和大家分享如何用 Angular 製作高彈性的頁面
本篇最後完成的結果如下:
https://huskylin.github.io/dynamic-component-demo-1/
了解這個功能後
也可以達成像是 CakeResume 的客製化履歷、 Wix 的拖拉式網站功能
(都是由小元件拖拉而成)
核心的知識會用到:
本篇會先一步一步帶大家做出動態新增元件這個功能
架構
要做的部分可以拆成兩部分來理解
第一部分,要在頁面上的哪個地方來產生元件?
- 透過 directive 在 .html 檔 上標示之後要產生元件的地方
- 透過 ViewChild 或是 ViewChildren 來尋找標記的位置
註: 如果只有一個地方要生成,用 ViewChild 即可
如果有多個才需要 ViewChildren ( 陣列版的 ViewChild )
第二部分,製作要動態生成的元件
- 注入 ComponentFactoryResolver
- 用 resolveComponentFactory 方法,產生對應的元件
最後則是將這兩部分合併起來
在找到標記的位置上,將產生出的元件放進來
程式碼
先製作 Directive
dynamic-component-host.directive.ts
import { Directive, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[appDynamicComponentHost]',
})
export class DynamicComponentHostDirective {
public viewContainerRef = this._viewContainerRef;
constructor(private _viewContainerRef: ViewContainerRef) { }
}
第一部分,找到要標記要生成的位置
custom.component.ts
// 透過 ViewChild 或是 ViewChildren 來尋找標記的位置
@ViewChildren(DynamicComponentHostDirective) dcHosts: QueryList<DynamicComponentHostDirective>;
constructor() { }
createComponent(component) {
this.dcHosts.forEach(host => {
// 要拿來放元件,view 的參照位置
const vcRef = host.viewContainerRef;
});
}
第二部分,製作要動態生成的元件
custom.component.ts
// 透過 ViewChild 或是 ViewChildren 來尋找標記的位置
@ViewChildren(DynamicComponentHostDirective) dcHosts: QueryList<DynamicComponentHostDirective>;
constructor(
private cfr: ComponentFactoryResolver,
private utilsService: UtilsService,
) { }
createComponent(component) {
this.dcHosts.forEach(host => {
// 要拿來放元件,view 的參照位置
const vcRef = host.viewContainerRef;
// 從全部的元件中,選擇要製作出來的元件
const allComponents = this.utilsService.getAllComponents();
const targetComponent: any = allComponents.filter(e => e.name === component.name)[0].val;
// 用 resolveComponentFactory 方法,產生對應的元件
const componentFactory = this.cfr.resolveComponentFactory(targetComponent);
});
}
最後產生出元件
custom.component.ts
// 透過 ViewChild 或是 ViewChildren 來尋找標記的位置
@ViewChildren(DynamicComponentHostDirective) dcHosts: QueryList<DynamicComponentHostDirective>;
constructor(
private cfr: ComponentFactoryResolver,
private utilsService: UtilsService,
) { }
createComponent(component) {
this.dcHosts.forEach(host => {
// 要拿來放元件,view 的參照位置
const vcRef = host.viewContainerRef;
// 從全部的元件中,選擇要製作出來的元件
const allComponents = this.utilsService.getAllComponents();
const targetComponent: any = allComponents.filter(e => e.name === component.name)[0].val;
// 用 resolveComponentFactory 方法,產生對應的元件
const componentFactory = this.cfr.resolveComponentFactory(targetComponent);
// 在要正確的位置上,產生正確的元件
const targetRef = vcRef.createComponent(componentFactory);
});
}
稍作延伸
上面的步驟可以簡單的做出動態產生的功能
個人在實際應用上,多做了一些延伸
- 製作一個容器元件,由容器元件來決定尺寸大小
(因為原本的圖表元件大小為 height: 100%, width: 100%)
這樣同一個元件在不同頁面中可以套用不同大小,使用上會更有彈性 - 利用 renderer2 替動態產生出來的元件,加上 CSS class
- 儲存產生的 componentRef,以供之後刪除、修改
1. 容器元件
custom.component.ts
承上
...
// 可以自訂不同尺寸的容器元件
wrappers = {
small: SmallComponent,
medium: MediumComponent,
big: BigComponent,
};
createComponent(component) {
...
// 製作被選擇要出來的元件,targetComponent 目標功能元件, wrapperComponent 決定大小的外框元件
const allComponents = this.utilsService.getAllComponents();
const targetComponent: any = allComponents.filter(e => e.name === component.name)[0].val;
const wrapperComponent: any = this.wrappers.small;
const wrapperFactory = this.cfr.resolveComponentFactory(wrapperComponent);
const componentFactory = this.cfr.resolveComponentFactory(targetComponent);
// 先產生目標圖表元件
const targetRef = vcRef.createComponent(componentFactory);
// 在參照位置產生外框元件,並且透過 ng-content,將目標圖表元件放進來
const wrapperRef = vcRef.createComponent(wrapperFactory, vcRef.length, undefined, [[targetRef.location.nativeElement]]);
}
關於在 viewContainerRef.createComponent() 時使用 ng-content
可以進一步參考這篇 SatckOverflow
這可以讓你動態產生的元件做出更複雜的變化
2. 加上 CSS class
renderer2 可以幫助我們在事後才替元件加上 class、改變樣式
用在動態元件上面也是很方便
承上
...
// 注入renderer2
constructor(private renderer2: Renderer2) { }
createComponent(component) {
...
const wrapperRef = vcRef.createComponent(wrapperFactory, vcRef.length, undefined, [[targetRef.location.nativeElement]]);
// 用 renderer2 在產生出來的元件上 加上 class
this.renderer2.addClass(wrapperRef.location.nativeElement, YOUR_CLASSNAME_HERE);
}
3. 儲存產生的 componentRef
替每次產生出來的元件加上 uniqueKey
方便之後在頁面上動態刪除元件
(這邊另外開一個 custom.service 來操作)
承上
...
// 簡單用一個 counter 來作為 uniqueKey
childUniqueKey: number = 0;
createComponent(component) {
...
// 在元件的instance內增加一個 uniqueKey 屬性,用來記錄唯一值
wrapperRef.instance['uniqueKey'] = ++this.childUniqueKey;
// 儲存 wrapper 跟 target 的 ref,之後更新資料、刪除元件時會用
this.customService.pushWrapperRefs(wrapperRef);
this.customService.pushChartRefs(targetRef);
}
綜合以上的程式碼
createComponent(component, isNew) {
this.dcHosts.forEach(host => {
// 要拿來放元件,view 的參照位置
const vcRef = host.viewContainerRef;
// 製作被選擇要出來的元件,targetComponent 目標功能元件, wrapperComponent 決定大小的外框元件
const allComponents = this.utilsService.getAllComponents();
const targetComponent: any = allComponents.filter(e => e.name === component.name)[0].val;
const wrapperComponent: any = this.wrappers['small'];
const wrapperFactory = this.cfr.resolveComponentFactory(wrapperComponent);
const componentFactory = this.cfr.resolveComponentFactory(targetComponent);
// 先產生目標圖表元件
const targetRef = vcRef.createComponent(componentFactory);
// 在參照位置產生外框元件,並且透過 ng-content,將目標圖表元件放進來
const wrapperRef = vcRef.createComponent(wrapperFactory, vcRef.length, undefined, [[targetRef.location.nativeElement]]);
// 用 renderer2 在產生出來的元件上 加上 class
this.renderer2.addClass(wrapperRef.location.nativeElement, YOUR_CLASSNAME_HERE);
// 記錄 component 唯一值
wrapperRef.instance['uniqueKey'] = ++this.childUniqueKey;
// 儲存 wrapper 跟 target 的 ref,之後更新資料、刪除元件時會用
this.customService.pushWrapperRefs(wrapperRef);
this.customService.pushChartRefs(targetRef);
});
}
成果
https://huskylin.github.io/dynamic-component-demo-1/
大家可以上去玩玩看
這裡用三個圖表元件來做簡單的示範
可以在原先空白的頁面上
自定義想要顯示哪些圖表、如何排列
如果每個圖表有不同的篩選條件時(例如:不同時間範圍、地區)
更可以達到交叉分析的效果,在數據分析上非常實用
完整的程式碼在Github 專案上
這次做了動態新增的部分
下一篇會延續本篇,完成下列功能
- 刪除已經產生出的元件
- 在創立 component 時,傳入 Input 資料
2/19 更新,下一篇在這裡 利用Angular動態元件,製作高彈性客製化頁面! (下)