原文: https://blog.angularindepth.c...
做者: Max Koretskyi
譯者: 而井
【翻譯】教你如何在@ViewChild查詢以前獲取ViewContainerRefhtml
在我最新的一篇關於動態組件實例化的文章《在Angular中關於動態組件你所須要知道的》中,我已經展現瞭如何將一個子組件動態地添加到父組件中的方法。全部動態的組件經過使用ViewContainerRef
的引用被插入到指定的位置。這個引用經過指定一些模版引用變量來得到,而後在組件中使用相似ViewChild
的查詢來獲取它(模版引用變量)。typescript
在此快速的複習一下。假設咱們有一個父組件App
,而且咱們須要將子組件A
插入到(父組件)模版的指定位置。在此咱們會這麼幹。bootstrap
咱們來建立組件Asegmentfault
@Component({ selector: 'a-comp', template: ` <span>I am A component</span> `, }) export class AComponent { }
而後將(組件A)它在declarations
和entryComponents
中進行註冊:app
@NgModule({ imports: [BrowserModule], declarations: [AppComponent, AComponent], entryComponents: [AComponent], bootstrap: [AppComponent] }) export class AppModule { }
而後在父組件App
中,咱們添加建立組件A
實例和插入它(到指定位置)的代碼。ide
@Component({ moduleId: module.id, selector: 'my-app', template: ` <h1>I am parent App component</h1> <div class="insert-a-component-inside"> <ng-container #vc></ng-container> </div> `, }) export class AppComponent { @ViewChild('vc', {read: ViewContainerRef}) vc: ViewContainerRef; constructor(private r: ComponentFactoryResolver) {} ngAfterViewInit() { const factory = this.r.resolveComponentFactory(AComponent); this.vc.createComponent(factory); } }
在plunker中有能夠運行例子(譯者注:這個連接中的代碼已經沒法運行,因此譯者把代碼整理了一下,放到了stackblitz上了,能夠點擊查看預覽)。若是有什麼你不能理解的,我建議你閱讀我一開始提到過的文章。this
使用上述的方法是正確的,也能夠運行,可是有一個限制:咱們不得不等到ViewChild
查詢執行後,那時正處於變動檢測期間。咱們只能在ngAfterViewInit
生命週期以後來訪問(ViewContainerRef
的)引用。若是咱們不想等到Angular運行完變動檢測以後,而是想在變動檢測以前擁有一個完整的組件視圖呢?咱們惟一能夠作到這一步的就是:用directive
指令來代替模版引用和ViewChild
查詢。lua
directive
指令代替ViewChild
查詢每個指令均可以在它的構造器中注入ViewContainerRef
引用。這個將是與一個視圖容器相關的引用,並且是指令的宿主元素的一個錨地。讓咱們聲明這樣一個指令:spa
import { Directive, Inject, ViewContainerRef } from '@angular/core'; @Directive({ selector: '[app-component-container]', }) export class AppComponentContainer { constructor(vc: ViewContainerRef) { vc.constructor.name === "ViewContainerRef_"; // true } }
我已經在構造器中添加了檢查(代碼)來保證視圖容器在指令實例化的時候是可用的。如今咱們須要在組件App
的模版中使用它(指令)來代替#vc
模版引用:翻譯
<div class="insert-a-component-inside"> <ng-container app-component-container></ng-container> </div>
若是你運行它,你會看到它是能夠運行的。好的,咱們如今知道在變動檢查以前,指令是如何訪問視圖容器的了。如今咱們須要作的就是把組件傳遞給它(指令)。咱們要怎麼作呢?一個指令能夠注入一個父組件,而且直接調用(父)組件的方法。然而,這裏有一個限制,就是組件不得不要知道父組件的名稱。或者使用這裏描述的方法。
一個更好的選擇就是:用一個在組件及其子指令之間共享服務,並經過它來溝通!咱們能夠直接在組件中實現這個服務並將其本地化。爲了簡化(這一操做),我也將使用定製的字符串token:
const AppComponentService= { createListeners: [], destroyListeners: [], onContainerCreated(fn) { this.createListeners.push(fn); }, onContainerDestroyed(fn) { this.destroyListeners.push(fn); }, registerContainer(container) { this.createListeners.forEach((fn) => { fn(container); }) }, destroyContainer(container) { this.destroyListeners.forEach((fn) => { fn(container); }) } }; @Component({ providers: [ { provide: 'app-component-service', useValue: AppComponentService } ], ... }) export class AppComponent { }
這個服務簡單地實現了原始的發佈/訂閱模式,而且當容器註冊後會通知訂閱者們。
如今咱們能夠將這個服務注入AppComponentContainer
指令之中,而且註冊(指令相關的)視圖容器了:
export class AppComponentContainer { constructor(vc: ViewContainerRef, @Inject('app-component-service') shared) { shared.registerContainer(vc); } }
剩下惟一要作的事情就是當容器註冊時,在組件App
中進行監聽,而且動態地建立一個組件了:
export class AppComponent { vc: ViewContainerRef; constructor(private r: ComponentFactoryResolver, @Inject('app-component-service') shared) { shared.onContainerCreated((container) => { this.vc = container; const factory = this.r.resolveComponentFactory(AComponent); this.vc.createComponent(factory); }); shared.onContainerDestroyed(() => { this.vc = undefined; }) } }
在plunker中有能夠運行例子(譯者注:這個連接中的代碼已經沒法運行,因此譯者把代碼整理了一下,放到了stackblitz上了,能夠點擊查看預覽)。你能夠看到,已經沒有ViewChild
查詢(的代碼)了。若是你新增一個ngOnInit
生命週期,你將看到組件A
在它(ngOnInit
生命週期)觸發前就已經渲染好了。
也許你以爲這個辦法十分駭人聽聞,其實不是的,咱們只需看看Angular中router-outlet
指令的源代碼就行了。這個指令在構造器中注入了viewContainerRef
,而且使用了一個叫parentContexts
的共享服務在路由器配置中註冊自身(即:指令)和視圖容器:
export class RouterOutlet implements OnDestroy, OnInit { ... private name: string; constructor(parentContexts, private location: ViewContainerRef) { this.name = name || PRIMARY_OUTLET; parentContexts.onChildOutletCreated(this.name, this); ... }