天天看點

Angular2 動态元件

        在前端開發過程中,經常會遇到一種情況,根據不同的類型,需要加載不同的元件,一般的做法是通過ngswitch文法來進行類型判斷,在裡面寫元件。例如下面的代碼,會根據不同的報表節點類型加載不同的component來繪制不同的圖表

<div class="height100" [ngSwitch]="node.reportNodeType">
        <div class="height100" *ngSwitchCase="reportNodeType.handsonTable">
            <hotTable [data]="node.reportNodeValue.value?.data"
                      [columns]="node.reportNodeValue.value?.columns"
                      [colHeaders]="node.reportNodeValue.value?.colHeaders"
                      [options]="node.reportNodeValue.value?.options">
            </hotTable>
        </div>
        <div class="height100" *ngSwitchCase="reportNodeType.bar">
            <bi-drill-down-charts #biChart [node]="node" [nodeValue]="node.reportNodeValue" class="height100"
                                  [loading]="node.loading">
            </bi-drill-down-charts>
        </div>
        <div class="height100" *ngSwitchCase="reportNodeType.pie">
            <bi-drill-down-charts #biChart [node]="node" class="height100"
                                  [loading]="node.loading">
            </bi-drill-down-charts>
        </div>
        <div class="height100" *ngSwitchDefault>
            <bi-charts #biChart [node]="node" class="height100"
                       [loading]="node.loading"></bi-charts>
        </div>
    </div>
           

伴随的問題是随着圖表類型的增加,這裡面的ngSwitchCase語句也會相應增加,變得難以維護;最好的方式是動态的加載需要的元件資訊;     具體的做法:

1>report-node.component.html,report-node.component.ts來封裝報表節點,對外所有使用報表節點的地方,直接引入ReportNodeComponent元件即可,這樣,外界不需要關心到底使用哪種元件,以及如何顯示的内部細節;report-node.component.html内容

<div #container></div>
           

report-node.component内容

@Component({
    selector: 'bi-report-node',
    templateUrl: './report-node.component.html',
    styles: []
})
export class ReportNodeComponent implements OnInit, OnDestroy {
    @Input() node: ReportNode;
    @ViewChild('container', {read: ViewContainerRef})
    container: ViewContainerRef;
    private componentRef: ComponentRef<{}>;

    constructor(private componentFactoryResolver: ComponentFactoryResolver,
                private reportNodeTypeService: ReportNodeTypeService) {

    }

    ngOnInit() {
        let type = this.node.reportNodeType;
        let component = this.reportNodeTypeService.getComponentType(type);
        let factory = this.componentFactoryResolver.resolveComponentFactory(component);
        this.componentRef = this.container.createComponent(factory);
        let instance = <AbstractCommonNodeComponent>this.componentRef.instance;
        instance.node = this.node;
    }


    ngOnDestroy(): void {
        if (this.componentRef) {
            this.componentRef.destroy();
            this.componentRef = null;
        }
    }

    resize(event: INgWidgetEvent) {
        if (this.componentRef) {
            let nodeComponent = <AbstractCommonNodeComponent>this.componentRef.instance;
            nodeComponent.resize(event);
        }
    }
           

元件内容中隻有一個container辨別,通過init方法根據節點類型動态建立元件,插入到container辨別div中;

@ViewChild

Angular提供了一種稱為DOM查詢的機制。它以@ViewChild和@ViewChildren裝飾器的形式出現.它們的行為相同,隻有前者傳回一個引用,而後者傳回多個引用作為 QueryList 對象;通常這些裝飾器和模闆引用變量一起工作。模闆引用變量可以了解為Dom元素的引用,可以将它看成html元素id類似的東西。這裡通過ViewContainerRef的createComponent方法,在指定dom元素内插入元件;ViewContainerRef類的createComponent方法

abstract createComponent<C>(componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, projectableNodes?: any[][], ngModule?: NgModuleRef<any>): ComponentRef<C>;      

第一個參數要求插入元件的componentFactory對象,這裡通過

let factory = this.componentFactoryResolver.resolveComponentFactory(component);      

來擷取;這裡的component對象我們通過一個服務來擷取,代碼如下

import {Injectable} from '@angular/core';
import {ChartsComponent} from '../../charts/charts.component';
import {DrillDownChartComponent} from '../../charts/drill-down-chart/drill-down-chart.component';
import {HandsonTableNodeComponent} from './handson-table-node/handson-table-node.component';
import {QueryNodeViewComponent} from './query-node-view/query-node-view.component';

@Injectable()
export class ReportNodeTypeService {

    private mappings = {
        'bar': DrillDownChartComponent,
        'pie': DrillDownChartComponent,
        'handsonTable': HandsonTableNodeComponent,
        "query": QueryNodeViewComponent
    };

    constructor() {
    }

    getComponentType(typeName: string) {
        let type = this.mappings[typeName];
        if (!type) {
            type = ChartsComponent;
        }
        return type;
    }

}
           

定義了一個mapping,然後根據類型找到對應的component;

2>第二個問題,如何向動态生成的component對象中傳遞參數以及互動行為?

abstract createComponent<C>(componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, projectableNodes?: any[][], ngModule?: NgModuleRef<any>): ComponentRef<C>;      

方法的傳回結果是一個component對象的引用,我們可以把動态component元件公用的參數抽象出來 為一個parentComponent,然後把componentRef 轉換為這個parentComponent,将其對其指派即可,比如這裡我們将公用的屬性和操作抽象成一個AbstractCommonNodeComponent,封裝起來,如下所示,其他所有的動态生成的元件繼承該元件

import {Component} from '@angular/core';
import {ReportNode} from '../report-node.model';
import {INgWidgetEvent} from 'ngx-draggable-widget-palan';


export abstract class AbstractCommonNodeComponent {
    node: ReportNode;

    constructor() {
    }

    abstract resize(event: INgWidgetEvent);
}
           

比如ChartsComponent,

import {Component, Input, OnInit} from '@angular/core';
import {DataMartService} from '../entities/data-mart/data-mart.service';
import {NzNotificationService} from 'ng-zorro-antd';
import {ReportNodeService} from '../entities/report-node/report-node.service';
import {AbstractCommonNodeComponent} from '../entities/report-node/abstract-common-node/abstract-common-node.component';

@Component({
    selector: 'bi-charts',
    templateUrl: './charts.component.html',
    styleUrls: [
        'charts.scss'
    ]
})
export class ChartsComponent extends AbstractCommonNodeComponent implements OnInit {
    echartsIntance: any;
    constructor(public reportNodeService: ReportNodeService,
                public notification: NzNotificationService,
                public dataMartService: DataMartService) {
        super();
    }

    ngOnInit(): void {
        console.log(this.node);
    }

    private triggerChartSize() {
        if (this.echartsIntance) {
            this.echartsIntance.resize();
        }
    }

    resize() {
        setTimeout(() => {
            this.triggerChartSize();
        }, 0);
    }

    onChartInit(ec) {
        this.echartsIntance = ec;
    }

}
           
最後我們做了類型強制轉換,然後對其進行指派操作
           
let instance = <AbstractCommonNodeComponent>this.componentRef.instance;
        instance.node = this.node;
           

總結:這裡用到了抽象工廠設計模式,把所有的動态元件的具體細節封裝起來,對外的元件是ReportNodeComponent,外部不需要知道具體的某種報表節點的具體顯示方式細節;給定一種報表的類型,該元件就通過對應的service(這裡是reportNodeTypeService)來找到對應的元件并渲染出來;之後擴充報表節點的時候,隻需要在reportNodeTypeService中把新的元件加進去就可以。

另外,這些動态元件要在module的entryComponents: [中聲明