使用 ViewContainerRef 探索Angular DOM操做

英文原版:
Exploring Angular DOM manipulation techniques using ViewContainerRefhtml

_翻譯:giscafer
說明:根據我的理解翻譯,不徹底詞詞對應。_git


每當我讀到關於使用Angular DOM的操做時,我老是會看到其中的一個或幾個類: ElementRef, TemplateRef, ViewContainerRef等。遺憾的是,儘管Angular文檔或相關文章當中提到這三者的一些內容,但我尚未發現關於這三者如何協做的完整的理想模型和示例的描述。本文旨在描述這種模型。github

若是你學習過angular.js的話,你就會知道在angular.js中很容易去操做DOM。Angular注入DOM elementlink 函數中,你能夠查詢組件模板內的任何節點,添加或刪除子節點,修改樣式等等。然而,這種方法有一個主要缺點——它被牢牢綁定到一個瀏覽器平臺上(意思是脫離瀏覽器就不能玩了)。web

新的 Angular 版本運行在不一樣的平臺上——在瀏覽器上,在移動平臺上,或者在 web worker 中。所以,須要在平臺特定API 和框架接口之間進行抽象級別的抽象。從 Angular 來看,這些抽象的形式有如下的參考類型: ElementRef, TemplateRef, ViewRef, ComponentRefViewContainerRef。在本文中,咱們將詳細介紹每一個引用類型,並展現如何使用它們來操做DOM。api

@ViewChild

在探索DOM抽象以前,讓咱們瞭解一下如何在組件/指令類( component/directive class)中訪問這些抽象。Angular 提供了一個稱爲DOM查詢的機制。它以 @ViewChild@ViewChildren 裝飾器的形式出現。它們的行爲相同,只有前者返回一個引用,然後者返回多個引用做爲 QueryList 對象。在本文中的例子中,我將主要使用 ViewChild 裝飾器,而不會在它以前使用@符號。瀏覽器

一般,這些裝飾器與模板引用變量一塊兒工做。模板引用變量(template reference variable) 僅僅是模板中的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 decorator 的基本語法以下:框架

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

在這個示例中,您能夠看到,我將 tref 指定爲 html 中的模板引用名稱,並接收與此元素關聯的
ElementRef 。第二個參數 read 並不老是必需的,由於 Angular 能夠經過DOM元素的類型推斷引用類型。例如,若是它是一個簡單的 html 元素,好比 span,Angular 返回 ElementRef。若是它是一個 template 模板,它將返回 TemplateRef 。一些引用,如 ViewContainerRef 不能被推斷,而且必須在
read 參數中被聲明。其餘的,如 ViewRef 不能從 DOM 接收返回,必須手動構造。dom

好了,如今咱們知道了如何查詢引用,讓咱們開始探索它們。angular4

ElementRef

這是最基本的抽象概念。若是您觀察它的類結構,您將看到它只包含與之關聯的原生元素(native element)。它對於訪問原生DOM元素很是有用,正如咱們在這裏看到的:

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

然而,這種用法卻被 Angular 團隊 所勸阻。它不只會帶來安全風險,並且還會在應用程序和呈現層之間產生緊密耦合,使得在多個平臺上運行應用程序變得困難。我認爲,它不是訪問 nativeElement 來打破抽象,而是使用特定的DOM API,好比 textContent 。可是,稍後您將看到,在 Angular 上實現的DOM操做思想模型幾乎不須要這樣一個較低級別的訪問。

ElementRef 能夠經過使用 ViewChild decorator做爲任何 DOM元素被返回 。可是因爲全部組件都駐留在一個自定義DOM元素中,而且全部的指令都被應用於DOM元素,組件和指令類能夠經過DI機制(依賴注入機制)得到與它們的宿主元素(host element)相關聯的元素的實例:

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

所以,雖然組件能夠經過DI訪問它的宿主元素,但 ViewChild decorator 一般會在其視圖(模板)(view (template))中得到對DOM元素的引用。指令的反作用——他們沒有任何視圖模板(views),他們一般直接與他們所依附的元素一塊兒工做。

TemplateRef

對於大多數web開發人員來講,模板的概念應該是熟悉的。模板是一組DOM元素,在應用程序的視圖中能夠重用。在HTML5標準引入模板標籤template以前,大多數模板都是在一個帶有一些 type 屬性變化的腳本標記的瀏覽器中完成的:

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

這種方法固然有許多缺點,好比語義和手動去建立DOM模型的必要性。使用模板標籤 template 瀏覽器解析 html 並建立 DOM 樹,但不會渲染它。而後能夠經過 content 屬性訪問它:

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

Angular 擁抱HTML5的這種方法並實現 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

ViewRef 表示一個Angular 視圖。在 Angular 框架中,視圖(View)是應用程序UI的基本構件。它是構成和毀滅在一塊兒的最小元素組合。Angular 鼓勵開發人員將UI看做是視圖的組成,而不是獨立的html標記樹。

Angular 支持兩種視圖:

  • Embedded Views which are linked to a Template (鏈接到模板的嵌入視圖)

  • Host Views which are linked to a Component (鏈接到組件的宿主視圖)

Creating embedded view (建立嵌入視圖)

模板僅包含視圖的藍圖。可使用前面提到的 createEmbeddedView 方法從模板中實例化一個視圖:

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

Creating host view(建立宿主視圖)

當組件被動態實例化時,會建立宿主視圖。使用 ComponentFactoryResolver 能夠動態地建立一個組件:

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

在 Angular 中,每一個組件都被綁定到一個注入器(injector)的特定實例,所以咱們在建立組件時傳遞當前的注入器實例。另外,不要忘記必須將動態實例化的組件添加到模塊或託管組件的 EntryComponents
中。

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

ViewContainerRef

表示一個容器,其中能夠附加一個或多個視圖。

這裏要提到的第一件事是,任何DOM元素均可以用做視圖容器。有趣的是,Angular 在元素內部沒有插入視圖,而是在元素綁定到 ViewContainer 以後附加它們。這相似於 router-outlet 插入組件。

一般,一個好的候選對象能夠標記一個 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 被綁定到經過 element 屬性訪問的特定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
}

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

@Component({
    selector: 'sample',
    template: `
        <span>I am first span</span>
        <ng-container #vc></ng-container>
        <span>I am last span</span>
        <template #tpl>
            <span>I am span in template</span>
        </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 和 ngComponentOutlet

雖然知道底層機制是如何工做的老是很好,但一般都但願有某種快捷方式。此快捷方式以兩種指令形式出現: ngTemplateOutletngComponentOutlet 。在撰寫本文時,二者都是實驗性的,ngComponentOutlet 將在版本4中可用(angular4+已能夠隨意使用)。但若是你已經讀過上面全部的內容,就很容易理解它們的做用。

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>
        <template #tpl>
            <span>I am span in template</span>
        </template>
    `
})
export class SampleComponent {}

您能夠看到,咱們在組件類中不使用任何實例化代碼的視圖。很是方便。

ngComponentOutlet

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

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

總結

如今,全部這些信息彷佛都很容易消化,但實際上它是至關連貫的,並在經過視圖操做DOM的過程當中造成了一個清晰的理想模型。您能夠經過使用 ViewChild 查詢和模板變量引用來得到 Angular DOM 抽象的引用。圍繞DOM元素的最簡單的包裝是 ElementRef 。對於模板,您有 TemplateRef,它容許您建立一個嵌入式視圖。 能夠經過使用 ComponentFactoryResolver建立的 componentRef 訪問宿主視圖。視圖可使用 ViewContainerRef 進行操做。有兩種指令使手動過程變爲自動化:ngTemplateOutlet  ——操做嵌入視圖 和 ngComponentOutlet —— 建立宿主視圖(動態組件)。

原文:https://github.com/giscafer/g...

相關文章
相關標籤/搜索