教你如何在@ViewChild查詢之前獲取ViewContainerRef

RIOLI發表於2019-02-16

原文:https://blog.angularindepth.c…
作者:Max Koretskyi
譯者:而井

【翻譯】教你如何在@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)它在declarationsentryComponents中進行註冊:

@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);
    ...
  }

相關文章