【翻譯】教你如何在@ViewChild查詢之前獲取ViewContainerRef
在我最新的一篇關於動態元件例項化的文章《在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);
...
}