英文原版:
Exploring Angular DOM manipulation techniques using ViewContainerRefhtml
_翻譯:giscafer
說明:根據我的理解翻譯,不徹底詞詞對應。_git
每當我讀到關於使用Angular DOM的操做時,我老是會看到其中的一個或幾個類: ElementRef
, TemplateRef
, ViewContainerRef
等。遺憾的是,儘管Angular文檔或相關文章當中提到這三者的一些內容,但我尚未發現關於這三者如何協做的完整的理想模型和示例的描述。本文旨在描述這種模型。github
若是你學習過angular.js
的話,你就會知道在angular.js
中很容易去操做DOM。Angular注入DOM element
到 link
函數中,你能夠查詢組件模板內的任何節點,添加或刪除子節點,修改樣式等等。然而,這種方法有一個主要缺點——它被牢牢綁定到一個瀏覽器平臺上(意思是脫離瀏覽器就不能玩了)。web
新的 Angular 版本運行在不一樣的平臺上——在瀏覽器上,在移動平臺上,或者在 web worker 中。所以,須要在平臺特定API 和框架接口之間進行抽象級別的抽象。從 Angular 來看,這些抽象的形式有如下的參考類型: ElementRef
, TemplateRef
, ViewRef
, ComponentRef
和 ViewContainerRef
。在本文中,咱們將詳細介紹每一個引用類型,並展現如何使用它們來操做DOM。api
在探索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
這是最基本的抽象概念。若是您觀察它的類結構,您將看到它只包含與之關聯的原生元素(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),他們一般直接與他們所依附的元素一塊兒工做。
對於大多數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
表示一個Angular 視圖。在 Angular 框架中,視圖(View)是應用程序UI的基本構件。它是構成和毀滅在一塊兒的最小元素組合。Angular 鼓勵開發人員將UI看做是視圖的組成,而不是獨立的html標記樹。
Angular 支持兩種視圖:
Embedded Views which are linked to a Template (鏈接到模板的嵌入視圖)
Host Views which are linked to a Component (鏈接到組件的宿主視圖)
模板僅包含視圖的藍圖。可使用前面提到的 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 中,每一個組件都被綁定到一個注入器(injector)的特定實例,所以咱們在建立組件時傳遞當前的注入器實例。另外,不要忘記必須將動態實例化的組件添加到模塊或託管組件的 EntryComponents
中。
所以,咱們已經看到了如何建立嵌入式視圖和宿主視圖。一旦建立了視圖,就可使用 ViewContainer
將其插入到DOM中。下一節將探討其功能。
表示一個容器,其中能夠附加一個或多個視圖。
這裏要提到的第一件事是,任何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={}
。
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
方法。全部其餘方法都是自解釋性的,可用於獲取索引視圖的引用,將視圖移到另外一個位置,或者從容器中刪除全部視圖。
ViewContainer
還提供了自動建立視圖的API:
class ViewContainerRef { element: ElementRef length: number createComponent(componentFactory...): ComponentRef<C> createEmbeddedView(templateRef...): EmbeddedViewRef<C> ... }
這些都是咱們在上面手工完成的簡單方便的包裝。它們從模板或組件建立視圖,並將其插入指定的位置。
雖然知道底層機制是如何工做的老是很好,但一般都但願有某種快捷方式。此快捷方式以兩種指令形式出現: ngTemplateOutlet
和 ngComponentOutlet
。在撰寫本文時,二者都是實驗性的,ngComponentOutlet
將在版本4中可用(angular4+已能夠隨意使用)。但若是你已經讀過上面全部的內容,就很容易理解它們的做用。
它將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 {}
您能夠看到,咱們在組件類中不使用任何實例化代碼的視圖。很是方便。
該指令相似於 ngTemplateOutlet
,其不一樣之處在於它建立了一個宿主視圖(實例化一個組件),而不是一個嵌入式視圖。你能夠這樣使用:
<ng-container *ngComponentOutlet="ColorComponent"></ng-container>
如今,全部這些信息彷佛都很容易消化,但實際上它是至關連貫的,並在經過視圖操做DOM的過程當中造成了一個清晰的理想模型。您能夠經過使用 ViewChild
查詢和模板變量引用來得到 Angular DOM 抽象的引用。圍繞DOM元素的最簡單的包裝是 ElementRef
。對於模板,您有 TemplateRef
,它容許您建立一個嵌入式視圖。 能夠經過使用 ComponentFactoryResolver
建立的 componentRef
訪問宿主視圖。視圖可使用 ViewContainerRef
進行操做。有兩種指令使手動過程變爲自動化:ngTemplateOutlet
——操做嵌入視圖 和 ngComponentOutlet
—— 建立宿主視圖(動態組件)。