教你如何在@ViewChild查询之前获取ViewContainerRef

原文:https://blog.angularindepth.com/here-is-how-to-get-viewcontainerref-before-viewchild-query-is-evaluated-f649e51315fb
作者:Max Koretskyi
译者:而井

【翻译】教你如何在@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)它在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);
    ...
  }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容