使用ViewContainerRef探索Angular DOM操做技術

每當我閱讀中遇到,關於Angular中使用DOM的內容時,總會看到一個或幾個這樣的類:ElementRef,TemplateRef,ViewContainerRef等等。 不幸的是,雖然其中的一些被Angular文檔或相關文章所講述,可是我尚未找到完整的描述以及這些它們是如何工做的。 html

若是你來自angular.js世界,那麼你知道操縱DOM是至關容易的。Angular注入DOM elementRef到構造函數中,你能夠查詢組件模板中的任何節點,添加或刪除子節點,修改樣式等。可是,這種方法有一個主要的缺點 - 它牢牢地綁定到瀏覽器平臺。瀏覽器

新的Angular版本運行在不一樣的平臺上 - 瀏覽器,移動平臺等。 所以,站在平臺特定的API和框架接口之間須要抽象層次。Angular中,這些抽象成爲如下引用類型的形式:ElementRef,TemplateRef,ViewRef,ComponentRef和ViewContainerRef。 在本文中,咱們將詳細介紹每種引用類型,並展現如何使用它們來操做DOM。安全

@ViewChild

在咱們探索DOM抽象以前,讓咱們瞭解如何在組件/指令類中訪問這些抽象。 Angular提供了一種稱爲DOM查詢的機制。 它以@ViewChild和@ViewChildren裝飾器的形式出現。 它們的行爲相同,只有前者返回一個引用,後者則返回多個引用做爲QueryList對象。 在這篇文章的例子中,我將主要使用ViewChild裝飾器。框架

一般,這些裝飾器與模板引用變量配對使用。 模板引用變量只是對模板中的DOM元素的命名引用。 您能夠將其視爲與html元素的id屬性相似的東西。 用模板引用標記DOM元素,而後使用ViewChild裝飾器在類中查詢它。 這裏是基本的例子:函數

@Component({
    selector: 'sample',
    template: `
        <span #tref>I am span</span>
    `
})
export class SampleComponent implements AfterViewInit {
    @ViewChild("tref", {read: ElementRef}) tref: ElementRef;

    ngAfterViewInit(): void {
        // outputs `I am span`
        console.log(this.tref.nativeElement.textContent);
    }
}

ViewChild裝飾器的基本語法以下:this

@ViewChild([reference from template], {read: [reference type]});

在這個例子中,你能夠看到我在html中指定了tref做爲模板引用名,而且接收到與這個元素相關的ElementRef。 讀取的第二個參數並不老是必需的,由於Angular能夠經過DOM元素的類型來推斷引用類型。 例如,若是它是一個簡單的HTML元素(如span),那麼angular將返回ElementRef。 若是它是一個模板元素,它將返回TemplateRef。不過一些引用,如ViewContainerRef不能被推斷,而且必須在讀參數中特別要求。 其餘的,像ViewRef不能從DOM返回,必須手動構造。spa

ElementRef

這是最基本的抽象。 若是你觀察它的類結構,你會發現它只保存了它所關聯的本地元素。 對於訪問本地DOM元素很是有用,咱們能夠在這裏看到:code

// outputs `I am span`
console.log(this.tref.nativeElement.textContent);

不過,Angular團隊不鼓勵這種用法。 這不只會帶來安全風險,還會在應用程序和渲染層之間形成緊密耦合,這使得在多個平臺上運行應用程序變得困難。 我相信這不是對nativeElement的訪問,而是打破了抽象,而是像textContent同樣使用特定的DOM API。 可是後面你會看到,在Angular中實現的DOM操做心智模型幾乎不須要這樣一個較低級別的訪問。component

可使用ViewChild裝飾器爲任何DOM元素返回ElementRef。 可是,因爲全部組件都駐留在自定義DOM元素中,而且全部指令都應用於DOM元素,所以組件和指令類能夠經過DI機制獲取與其主機元素關聯的ElementRef實例:htm

@Component({
    selector: 'sample',
    ...
export class SampleComponent{
    constructor(private hostElement: ElementRef) {
        //outputs <sample>...</sample>
        console.log(this.hostElement.nativeElement.outerHTML);
    }

所以,雖然組件能夠經過DI訪問其主機元素,但ViewChild裝飾器一般用於在其視圖(模板)中獲取對DOM元素的引用。 反之亦然,指令沒有視圖,他們一般直接與他們所附的元素。

模板的概念應該是大多數Web開發人員熟悉的。 這是一組DOM元素,在整個應用程序的視圖中被重用。 在HTML5標準引入了模板標籤以前,大多數模板都被包含在script標籤中。

<script id="tpl" type="text/template">
  <span>I am span in template</span>
</script>

這種方法固然有許多缺點,如語義和手動建立DOM模型的必要性。 使用模板標籤瀏覽器解析HTML並建立DOM樹,但不呈現它。 而後能夠經過內容屬性訪問:

<script>
    let tpl = document.querySelector('#tpl');
    let container = document.querySelector('.insert-after-me');
    insertAfter(container, tpl.content);
</script>
<div class="insert-after-me"></div>
<ng-template id="tpl">
    <span>I am span in template</span>
</ng-template>

Angular支持這種方法,並實現TemplateRef類來處理模板。 如下是如何使用它:

@Component({
    selector: 'sample',
    template: `
        <ng-template #tpl>
            <span>I am span in template</span>
        </ng-template>
    `
})
export class SampleComponent implements AfterViewInit {
    @ViewChild("tpl") tpl: TemplateRef<any>;

    ngAfterViewInit() {
        let elementRef = this.tpl.elementRef;
        // outputs `template bindings={}`
        console.log(elementRef.nativeElement.textContent);
    }
}

該框架從DOM中刪除模板元素,並在其位置插入註釋。 這是呈現時的樣子:

<sample>
    <!--template bindings={}-->
</sample>

TemplateRef類自己是一個簡單的類。 它的elementRef屬性擁有對其宿主元素的引用,並具備一個方法createEmbeddedView。 這個方法很是有用,由於它容許咱們建立一個視圖並以ViewRef的形式返回一個引用。

ViewRef

這種抽象表示Angular視圖。 在Angular世界中,View是應用程序UI的基本構建塊。 它是創造和消滅的最小的元素分組。 Angular哲學鼓勵開發人員將UI視爲Views的組合,而不是將其視爲獨立的HTML標籤。

Angular支持兩種類型的視圖:

  • 嵌入視圖連接到模板
  • 連接到組件的主機視圖

建立嵌入的視圖

一個模板只是一個視圖的藍圖。 一個視圖可使用前面提到的createEmbeddedView方法從模板實例化,以下所示:

ngAfterViewInit() {
    let view = this.tpl.createEmbeddedView(null);
}

建立宿主視圖

宿主視圖是在組件動態實例化時建立的。 可使用ComponentFactoryResolver動態建立一個組件:

constructor(private injector: Injector,
            private r: ComponentFactoryResolver) {
    let factory = this.r.resolveComponentFactory(ColorComponent);
    let componentRef = factory.create(injector);
    let view = componentRef.hostView;
}

在Angular中,每一個組件都綁定到一個注入器的特定實例,因此咱們在建立組件時傳遞當前的注入器實例。 此外,不要忘記,動態實例化的組件必須添加到模塊或主機組件的EntryComponents。

因此,咱們已經看到如何建立嵌入和宿主視圖。 一旦建立了視圖,就可使用ViewContainer將其插入到DOM中。 下一節將探討其功能。

ViewContainerRef

表示能夠附加一個或多個視圖的容器。

首先要提到的是,任何DOM元素均可以用做視圖容器。有趣的是,Angular不在元素內插入視圖,而是在綁定到ViewContainer的元素以後附加它們。 這與路由器插座如何插入組件相似。

一般,標記應該建立ViewContainer的地方的好候選者是ng-container元素。 它被渲染爲一個註釋,因此它不會在DOM中引入多餘的html元素。 如下是在組件模板的特定位置建立ViewContainer的示例:

@Component({
    selector: 'sample',
    template: `
        <span>I am first span</span>
        <ng-container #vc></ng-container>
        <span>I am last span</span>
    `
})
export class SampleComponent implements AfterViewInit {
    @ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef;

    ngAfterViewInit(): void {
        // outputs `template bindings={}`
        console.log(this.vc.element.nativeElement.textContent);
    }
}

就像其餘DOM抽象同樣,ViewContainer綁定到經過元素屬性訪問的特定DOM元素。 在這個例子中,ng-container元素被綁定爲註釋的示例中,輸出爲template bindings = {}。

Manipulating views

ViewContainer爲操做視圖提供了一個方便的API:

class ViewContainerRef {
    ...
    clear() : void
    insert(viewRef: ViewRef, index?: number) : ViewRef
    get(index: number) : ViewRef
    indexOf(viewRef: ViewRef) : number
    detach(index?: number) : ViewRef
    move(viewRef: ViewRef, currentIndex: number) : ViewRef
}

咱們以前已經看到,如何從模板和組件手動建立兩種類型的視圖。 一旦咱們有了一個視圖,咱們可使用插入方法將其插入到DOM中。 因此,下面是從模板中建立一個嵌入式視圖並將其插入到由ng-container元素標記的特定位置的示例:

@Component({
    selector: 'sample',
    template: `
        <span>I am first span</span>
        <ng-container #vc></ng-container>
        <span>I am last span</span>
        <ng-template #tpl>
            <span>I am span in template</span>
        </ng-template>
    `
})
export class SampleComponent implements AfterViewInit {
    @ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef;
    @ViewChild("tpl") tpl: TemplateRef<any>;

    ngAfterViewInit() {
        let view = this.tpl.createEmbeddedView(null);
        this.vc.insert(view);
    }
}

經過這個實現,生成的html看起來像這樣:

<sample>
    <span>I am first span</span>
    <!--template bindings={}-->
    <span>I am span in template</span>

    <span>I am last span</span>
    <!--template bindings={}-->
</sample>

要從DOM中刪除視圖,咱們可使用detach方法。 全部其餘方法都是自解釋性的,可用於經過索引獲取對視圖的引用,將視圖移至其餘位置或從容器中移除全部視圖。

Creating Views

ViewContainer還提供API來自動建立視圖:

class ViewContainerRef {
    element: ElementRef
    length: number

    createComponent(componentFactory...): ComponentRef<C>
    createEmbeddedView(templateRef...): EmbeddedViewRef<C>
    ...
}

這些都是咱們上面手動完成的簡單包裝。 他們從模板或組件建立一個視圖,並將其插入到指定位置。

ngTemplateOutlet and ngComponentOutlet

ngTemplateOutlet

這個將一個DOM元素標記爲ViewContainer,並在其中插入一個由模板建立的嵌入視圖,而不須要在組件類中明確地作到這一點。 這意味着上面咱們建立視圖並將其插入到#vc DOM元素的示例能夠像這樣重寫:

@Component({
    selector: 'sample',
    template: `
        <span>I am first span</span>
        <ng-container [ngTemplateOutlet]="tpl"></ng-container>
        <span>I am last span</span>
        <ng-template #tpl>
            <span>I am span in template</span>
        </ng-template>
    `
})
export class SampleComponent {}

正如你所看到的,咱們不使用任何視圖實例化組件類中的代碼。 很是便利。

ngComponentOutlet

該指令相似於ngTemplateOutlet,不一樣之處在於它建立一個宿主視圖(實例化一個組件),而不是嵌入視圖。 你能夠像這樣使用它:

<ng-container *ngComponentOutlet="ColorComponent"></ng-container>

總結

如今,全部這些信息彷佛均可以被消化,但實際上這些信息是很是連貫的,而且經過視圖來顯示操縱DOM的清晰模型。 經過使用ViewChild查詢和模板變量引用,您能夠得到對Angular DOM抽象的引用。 圍繞DOM元素的最簡單的包裝是ElementRef。 對於具備TemplateRef的模板,您能夠建立嵌入式視圖。 主機視圖能夠在使用ComponentFactoryResolver建立的componentRef上訪問。 視圖能夠用ViewContainerRef來操做。 有兩個使自動手動過程的指令:ngTemplateOutlet - 用於嵌入視圖,ngComponentOutlet用於宿主視圖(動態組件)。

相關文章
相關標籤/搜索