教你如何在@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);
    ...
  }
相关文章
相关标签/搜索