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

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

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

圖片描述

在我最新的一篇關於動態組件實例化的文章《在Angular中關於動態組件你所須要知道的》中,我已經展現瞭如何將一個子組件動態地添加到父組件中的方法。全部動態的組件經過使用ViewContainerRef的引用被插入到指定的位置。這個引用經過指定一些模版引用變量來得到,而後在組件中使用相似ViewChild的查詢來獲取它(模版引用變量)。typescript

在此快速的複習一下。假設咱們有一個父組件App,而且咱們須要將子組件A插入到(父組件)模版的指定位置。在此咱們會這麼幹。bootstrap

組件A

咱們來建立組件Asegmentfault

@Component({
  selector: 'a-comp',
  template: `
      <span>I am A component</span>
  `,
})
export class AComponent {
}

App根模塊

而後將(組件A)它在declarationsentryComponents中進行註冊:app

@NgModule({
  imports: [BrowserModule],
  declarations: [AppComponent, AComponent],
  entryComponents: [AComponent],
  bootstrap: [AppComponent]
})
export class AppModule {
}

組件App

而後在父組件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生命週期)觸發前就已經渲染好了。

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);
    ...
  }
相關文章
相關標籤/搜索