[NGX]使用ViewContainerRef來操作Angular中的DOM

我愛吃南瓜發表於2018-05-11

https://blog.angularindepth.com/exploring-angular-dom-abstractions-80b3ebcfc02

原文連結,牆裂推薦閱讀原文,事實上這篇文章網上已有的翻譯都有或多或少的錯誤,我這篇肯定也不例外。

Angular文件中關於使用Angular DOM的操作,總是會提到一個或幾個類: ElementRef, TemplateRef, ViewContainerRef等。本文旨在描述這種模型。

在Angular中DOM被抽象出ElementRef, TemplateRef, ViewRef, ComponentRefViewContainerRef

@ViewChild | @ViewChildren

在深入這些DOM抽象之前,我們先了解一下如何在元件/指令類中訪問這些DOM抽象。Angular提供了一個DOM查詢機制。這個機制由@ViewChild@ViewChildren兩個裝飾器完成。他倆的使用方法是一毛一樣的,只是返回結果有所不同,顯而易見,後折返回一個列表,前者只返回一個引用。

通常情況下,這些裝飾器都和模板引用標識(template reference variable)同時出現,模板引用標識(template reference variable)是一個在模板檔案裡給dom元素命名的東西,你也可以把它理解成類似於dom元素的id的存在。給一個元素增加一個引用標識,你就可以通過這兩個裝飾器來獲取它們。

@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的基本語法為@ViewChild([reference from template], {read: [reference type]});第二個引數並不是必須的,假如他是一個簡單的dom元素,類似span這樣的,angular會推斷為ElementRef。如果是一個template元素,則會推斷為TemplateRef。當然,也有一些型別比如ViewContainerRef是不能被推斷出來的,需要手動宣告在read的值中,Others, like ViewRef cannot be returned from the DOM and have to be constructed manually.

ok,現在我們知道怎麼執行dom查詢了,我們來開始深入這些dom抽象吧~

ElementRef

ElementRef可以說是墜基本的dom抽象了。

class ElementRef<T> {
    constructor(nativeElement: T)
    nativeElement: T
}
複製程式碼

這個類中只包含了與之關聯的原生元素,通過它你可以很輕鬆的查詢到dom元素。

console.log(this.tref.nativeElement.textContent);

但是Angular並不推薦這種直接dom元素進行操作的方法,不光是安全原因,更是因為這樣做會使得一套程式碼多平臺執行的原則受到了打破,它使得應用和渲染層緊密耦合。我嚼的這並不是因為使用nativeElement導致的,而是因為使用了textContent這樣的DOM API導致的。事實上Angular所實現的DOM操作模型幾乎沒有用到這麼底層的dom訪問。

對任何dom元素使用@ViewChild裝飾器都能返回ElementRef。因為所有的元件其實都是被host在普通的DOM元素上,所有的指令都是作用於DOM元素,所以藉助於Angular的依賴注入機制,所有的元件/指令類可以獲取它們的**宿主元素(host element)**的ElementRef。方法如下:

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

所以既然一個元件可以通過DI機制輕鬆的獲取它的宿主元素,@ViewChild裝飾器一般就用來獲取模板中的一個子元素了。對指令而言則是完全相反的,它們沒有檢視、沒有子元素,所以它們通常與他們所依附的元素一起工作。

TemplateRef

模板的概念對於廣大web開發者而言可以說是見的多了。它是在應用中能被多次複用的一個DOM元素的集合。在HTML5將template便籤列入標準之前,多數模板都是通過script標籤包裹的方式實現的。這種實現方式不是本文的重點,所以我們按住不表。

我們來講講template標籤,作為HTML5新增加的標籤,瀏覽器會解析這個標籤並生成對應的DOM元素,但是並不會直接渲染到頁面上。通過template元素的content屬性我們就可以獲取到這個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類來與**模板(template)**一起工作。使用方法如下:

@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);
    }
}
複製程式碼

Angular在渲染過程中移除了template標籤。並在其位置插入了一條註釋,

<sample>
    <!--template bindings={}-->
</sample>
複製程式碼

下面是Angular文件中對TemplateRef類的描述 TemplateRef類的資料結構如下:

class TemplateRef<C> {
    get elementRef: ElementRef
    createEmbeddedView(context: C): EmbeddedViewRef<C>
}
複製程式碼

The location in the View where the Embedded View logically belongs to.

他有一個屬性elementRef: ElementRef,代表了這個內嵌檢視在view中所屬的位置,也就是ng-template標籤上的模板引用標識獲取的ElementRef。

The data-binding and injection contexts of Embedded Views created from this TemplateRef inherit from the contexts of this location.

Typically new Embedded Views are attached to the View Container of this location, but in advanced use-cases, the View can be attached to a different container while keeping the data-binding and injection context from the original location.

他有一個方法createEmbeddedView()可以建立一個內嵌檢視並將其駐留在一個檢視容器上,同時還能返回一個對檢視的引用:ViewRef。

ViewRef

ViewRef正是對Angular中最基本的UI構成-View的抽象。他是一系列元素的最小集合,被同時建立出來又被同時銷燬。Angular高度鼓勵開發者們將UI視為一系列View的組成,而不是一個個html標籤組成的樹。

Angular有且僅有兩種檢視型別:內嵌檢視(Embedded Views)宿主檢視(Host Views)。通常情況下,內嵌檢視往往跟Template相關聯,而宿主檢視與元件相關聯。

建立一個內嵌檢視

TemplateRef中的方法createEmbeddedView()方法可以直接建立出一個內嵌檢視,該方法返回的型別正是ViewRef;

ngAfterViewInit() {
    let view = this.tpl.createEmbeddedView(null);
}
複製程式碼

建立一個宿主檢視

當一個元件被動態生成時,宿主檢視也就隨之被建立出來了。通過ComponentFactoryResolver類你可以輕鬆的動態建立出一個元件。

constructor(
    private _injector: Injector,
    private _r: ComponentFactoryResolver,
) {
    let factory = this._r.resolveComponentFactory(aComponent);
    let componentRef = factory.create(this._injector);
    let view = componentRef.hostView;
}
複製程式碼

這裡薛薇的解釋一下以上程式碼,在Angular中每個元件都和一個**注入器(Injector)**的例項繫結的,因此當我們動態建立一個元件的時候,我們會把當前的注入器例項傳遞進create()方法中。當然,除了上述程式碼,如果你想要獲取一個元件的元件工廠,你需要在當前模組的entryComponents中宣告這個元件。

下面是Angular官網中對VIewRef類的一點描述:

class ViewRef extends ChangeDetectorRef {
    get destroyed: boolean
    destroy(): void
    onDestroy(callback: Function): any

    // 自ChangeDetectorRef繼承來的屬性方法
    markForCheck(): void
    detach(): void
    detectChanges(): void
    checkNoChanges(): void
    reattach(): void
}
複製程式碼

好的,現在我們知道了如何建立內嵌檢視和宿主檢視。當這些檢視建立完畢之後我們就可以通過ViewContainer將他們插入到DOM中。

ViewContainerRef

首先要宣告的是,任何DOM元素都可以被用作為檢視容器。英催思挺的是Angular不會在元素內部插入檢視,而是把這些檢視新增到繫結到ViewContainer的元素後面。這跟router-outlet插入元件的方法灰常相似。

通常情況下,一個墜佳的建立ViewContainer的地方是<ng-container></ng-container>標籤。最終它會被渲染為一行註釋而不會在dom中引入冗餘的元素。

@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 one line of comment
        console.log(this.vc.element.nativeElement.textContent);
    }
}
複製程式碼

Angular文件中對於ViewContainerRef類的描述如下:

class ViewContainerRef {
    get element: ElementRef
    get injector: Injector
    get parentInjector: Injector
    get length: number
    clear(): void
    get(index: number): ViewRef | null
    createEmbeddedView<C>(templateRef: TemplateRef<C>, context?: C, index?: number): EmbeddedViewRef<C>
    createComponent<C>(componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, projectableNodes?: any[][], ngModule?: NgModuleRef<any>): ComponentRef<C>
    insert(viewRef: ViewRef, index?: number): ViewRef
    move(viewRef: ViewRef, currentIndex: number): ViewRef
    indexOf(viewRef: ViewRef): number
    remove(index?: number): void
    detach(index?: number): ViewRef | null
}
複製程式碼

操作檢視

前面我們已經知道了兩種檢視型別是如何從template和component建立出來的,一旦我們有了view,就可以通過viewContainer的insert()方法將其插入到dom中。

import {
  AfterViewInit,
  Component,
  TemplateRef,
  ViewChild,
  ViewContainerRef
} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <span>firsr para</span>
    <ng-container #vc></ng-container>
    <span>second para</span>
    <ng-template #tpl>
      <span>the para in template</span>
    </ng-template>
  `
})
export class AppComponent implements AfterViewInit {
  @ViewChild('vc', {read: ViewContainerRef}) viewContainer: ViewContainerRef;
  @ViewChild('tpl') tpl: TemplateRef<any>;

  ngAfterViewInit() {
    const tp_view = this.tpl.createEmbeddedView(null);
    this.viewContainer.insert(tp_view);
    set
  }
}
// 輸出
// <span>firsr para</span>
// <!---->
// <span>the para in template</span>
// <span>second para</span>
// <!---->
複製程式碼

要移除這個被插入的元素,只要呼叫viewContainer的detach()方法。所有其他方法都是自解釋性的,可用於獲取索引檢視的引用,將檢視移到另一個位置,或者從容器中刪除所有檢視。

建立檢視

createEmbeddedView<C>(templateRef: TemplateRef<C>, context?: C, index?: number): EmbeddedViewRef<C>
createComponent<C>(componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, 
複製程式碼

這兩個方法相當於對我們上面的程式碼進行了一層封裝,他會從template或component中建立一個view出來並插入到dom中相應的位置。

總結

現在看起來似乎有很多概念需要去理解吸收。但是實際上這些概念的調條理都十分清晰,並且這些概念組成了一個十分清晰的檢視操作DOM的模型。

通過@ViewChild和模板引用識別符號你可以獲取到Angular DOM抽象的引用。圍繞DOM元素最簡單的包裹是ElementRef。

對於模板(template)而言,你可以通過TemplateRef來建立一個內嵌檢視(Embedded View);宿主檢視則可以通過ComponentFactoryResolver建立的componentRef來獲取。

通過ViewContainerRef我們則可以操作這些檢視

結束!哈!

相關文章