教你如何在@ViewChild查詢之前獲取ViewContainerRef
【翻譯】教你如何在@ViewChild查詢之前獲取ViewContainerRef

image
在我最新的一篇關於動態元件例項化的文章《 在Angular中關於動態元件你所需要知道的 》中,我已經展示瞭如何將一個子元件動態地新增到父元件中的方法。所有動態的元件通過使用 ViewContainerRef
的引用被插入到指定的位置。這個引用通過指定一些模版引用變數來獲得,然後在元件中使用類似 ViewChild
的查詢來獲取它(模版引用變數)。
在此快速的複習一下。假設我們有一個父元件 App
,並且我們需要將子元件 A
插入到(父元件)模版的指定位置。在此我們會這麼幹。
元件A
我們來建立元件A
@Component({ selector: 'a-comp', template: ` <span>I am A component</span> `, }) export class AComponent { }
App根模組
然後將(元件A)它在 declarations
和 entryComponents
中進行註冊:
@NgModule({ imports: [BrowserModule], declarations: [AppComponent, AComponent], entryComponents: [AComponent], bootstrap: [AppComponent] }) export class AppModule { }
元件App
然後在父元件 App
中,我們新增建立元件 A
例項和插入它(到指定位置)的程式碼。
@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 上了,可以點選檢視預覽)。如果有什麼你不能理解的,我建議你閱讀我一開始提到過的文章。
使用上述的方法是正確的,也可以執行,但是有一個限制:我們不得不等到 ViewChild
查詢執行後,那時正處於變更檢測期間。我們只能在 ngAfterViewInit
生命週期之後來訪問( ViewContainerRef
的)引用。如果我們不想等到Angular執行完變更檢測之後,而是想在變更檢測之前擁有一個完整的元件檢視呢?我們唯一可以做到這一步的就是:用 directive
指令來代替模版引用和 ViewChild
查詢。
使用 directive
指令代替 ViewChild
查詢
每一個指令都可以在它的構造器中注入 ViewContainerRef
引用。這個將是與一個檢視容器相關的引用,而且是指令的宿主元素的一個錨地。讓我們宣告這樣一個指令:
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
生命週期)觸發前就已經渲染好了。
RouterOutlet
也許你覺得這個辦法十分駭人聽聞,其實不是的,我們只需看看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); ... }