【问题标题】:Angular2 Dynamic Component Injection in Root根中的Angular2动态组件注入
【发布时间】:2017-02-12 21:50:40
【问题描述】:

问题

我正在寻找将已知/定义的组件注入应用程序的根目录并将@Input() 选项投射到该组件上的最佳方法。

要求

这是在应用程序主体中创建模态框/工具提示等内容所必需的,这样overflow:hidden/etc 就不会扭曲或完全切断位置。

研究

我发现我可以得到ApplicationRef's 然后粗暴地向上遍历并找到ViewContainerRef

constructor(private applicationRef: ApplicationRef) {
}

getRootViewContainerRef(): ViewContainerRef {
  return this.applicationRef['_rootComponents'][0]['_hostElement'].vcRef;
}

一旦我有了它,我就可以在 ref 上调用 createComponent,例如:

appendNextToLocation<T>(componentClass: Type<T>, location: ViewContainerRef): ComponentRef<T> {
  const componentFactory = this.componentFactoryResolver.resolveComponentFactory(componentClass);
  const parentInjector = location.parentInjector;
  return location.createComponent(componentFactory, location.length, parentInjector);
}

但现在我已经创建了组件,但我的 Input 属性都没有实现。为了实现这一点,我必须手动遍历我的选项并将其设置在appendNextToLocation 实例的结果上,例如:

const props = Object.getOwnPropertyNames(options);
for(const prop of props) {
  component.instance[prop] = options[prop];
}

现在我确实意识到你可以做一些 DI 来注入选项,但这使得它在尝试用作普通组件时无法重用。这是供参考的外观:

let componentFactory = this.componentFactoryResolver.resolveComponentFactory(ComponentClass);
let parentInjector = location.parentInjector;

let providers = ReflectiveInjector.resolve([
  { provide: ComponentOptionsClass, useValue: options }
]);

childInjector = ReflectiveInjector.fromResolvedProviders(providers, parentInjector);

return location.createComponent(componentFactory, location.length, childInjector);

话虽如此,上述所有方法实际上都有效,但有时感觉有点老套。我还担心像上面那样设置输入属性的生命周期时间,因为它发生在创建之后。

重要参考文献

【问题讨论】:

  • 您不能对动态添加的组件使用绑定。您的方法是目前可以从 Angular2 获得的最佳方法。我认为 Angular2 团队会尝试在这方面进行改进,但目前还不清楚什么时候可以期待它。

标签: angular angular2-components


【解决方案1】:

在 2.3.0 中,引入了 attachView,它允许您将更改检测附加到 ApplicationRef,但是,您仍然需要手动将元素附加到根容器。这是因为对于 Angular2,它运行的环境可能是 web worker、universal、nativescript 等,所以我们需要明确地告诉它我们想在哪里/如何将它添加到视图中。

下面是一个示例服务,它允许您动态插入组件并自动投影组件的Input

import {
  ApplicationRef, ComponentFactoryResolver, ComponentRef, Injectable,
  Injector, ViewContainerRef, EmbeddedViewRef, Type
} from '@angular/core';

/**
 * Injection service is a helper to append components
 * dynamically to a known location in the DOM, most
 * noteably for dialogs/tooltips appending to body.
 * 
 * @export
 * @class InjectionService
 */
@Injectable()
export class InjectionService {
  private _container: ComponentRef<any>;

  constructor(
    private applicationRef: ApplicationRef,
    private componentFactoryResolver: ComponentFactoryResolver,
    private injector: Injector) {
  }

  /**
   * Gets the root view container to inject the component to.
   * 
   * @returns {ComponentRef<any>}
   * 
   * @memberOf InjectionService
   */
  getRootViewContainer(): ComponentRef<any> {
    if(this._container) return this._container;

    const rootComponents = this.applicationRef['_rootComponents'];
    if (rootComponents.length) return rootComponents[0];

    throw new Error('View Container not found! ngUpgrade needs to manually set this via setRootViewContainer.');
  }

  /**
   * Overrides the default root view container. This is useful for 
   * things like ngUpgrade that doesn't have a ApplicationRef root.
   * 
   * @param {any} container
   * 
   * @memberOf InjectionService
   */
  setRootViewContainer(container): void {
    this._container = container;
  }

  /**
   * Gets the html element for a component ref.
   * 
   * @param {ComponentRef<any>} componentRef
   * @returns {HTMLElement}
   * 
   * @memberOf InjectionService
   */
  getComponentRootNode(componentRef: ComponentRef<any>): HTMLElement {
    return (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
  }

  /**
   * Gets the root component container html element.
   * 
   * @returns {HTMLElement}
   * 
   * @memberOf InjectionService
   */
  getRootViewContainerNode(): HTMLElement {
    return this.getComponentRootNode(this.getRootViewContainer());
  }

  /**
   * Projects the inputs onto the component
   * 
   * @param {ComponentRef<any>} component
   * @param {*} options
   * @returns {ComponentRef<any>}
   * 
   * @memberOf InjectionService
   */
  projectComponentInputs(component: ComponentRef<any>, options: any): ComponentRef<any> {
    if(options) {
      const props = Object.getOwnPropertyNames(options);
      for(const prop of props) {
        component.instance[prop] = options[prop];
      }
    }

    return component;
  }

  /**
   * Appends a component to a adjacent location
   * 
   * @template T
   * @param {Type<T>} componentClass
   * @param {*} [options={}]
   * @param {Element} [location=this.getRootViewContainerNode()]
   * @returns {ComponentRef<any>}
   * 
   * @memberOf InjectionService
   */
  appendComponent<T>(
    componentClass: Type<T>, 
    options: any = {}, 
    location: Element = this.getRootViewContainerNode()): ComponentRef<any> {

    let componentFactory = this.componentFactoryResolver.resolveComponentFactory(componentClass);
    let componentRef = componentFactory.create(this.injector);
    let appRef: any = this.applicationRef;
    let componentRootNode = this.getComponentRootNode(componentRef);

    // project the options passed to the component instance
    this.projectComponentInputs(componentRef, options);

    appRef.attachView(componentRef.hostView);

    componentRef.onDestroy(() => {
      appRef.detachView(componentRef.hostView);
    });

    location.appendChild(componentRootNode);

    return componentRef;
  }
}

【讨论】:

  • 根据angular.ioattachView 的描述,它说并且我引用''视图被销毁时将自动分离'' ...所以我得出结论,订阅@987654328 @ 分离视图已过时?
  • amcdnl,您能否举例说明如何使用上述服务。我正在尝试将组件动态添加到应用程序根目录,并观察更改以动态删除它(与 Angular Material 2 将背景添加到对话框和菜单的方式类似)。另外,我在 Angular 文档中找不到对 attachView() 的引用。它已被弃用吗?
  • @Rhumbus 您应该查看cdk portals 以简化此操作
  • @amcdnl 只有一个问题:创建的组件何时会被清除。目前,当我移动到不同的 URL 路由时,不会调用 componentRef.onDestroy 事件...
  • 你能更新你的脚本,让它在最新的角度版本下工作吗? 2020 年更新 :)
【解决方案2】:

getRootViewContainer 需要进行如下修改以适应较新版本的 Angular。其余的就像一个魅力。

getRootViewContainer(): ComponentRef<any> {
    if(this._container) return this._container;

    return (this.applicationRef.components[0].hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
}

【讨论】:

  • 根据我们现在的情况,您的代码看起来合乎逻辑,但我收到错误:error TS2740: Type 'HTMLElement' is missing the following properties from type 'ComponentRef&lt;any&gt;': location, injector, instance, hostView, and 4 more.
  • 如果您使用下面的答案删除相同的文件会很好,它适用于您最新的角度,谢谢:)
猜你喜欢
  • 1970-01-01
  • 2017-08-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-02-06
  • 1970-01-01
  • 2016-12-21
  • 1970-01-01
相关资源
最近更新 更多