美文网首页让前端飞程序员
教你如何在@ViewChild查询之前获取ViewContain

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

作者: 吃不了辣椒 | 来源:发表于2019-03-16 18:05 被阅读5次

    原文: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);
        ...
      }
    

    相关文章

      网友评论

        本文标题:教你如何在@ViewChild查询之前获取ViewContain

        本文链接:https://www.haomeiwen.com/subject/ggwsmqtx.html