Angular2 根注入动态组件

15

问题

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

要求

这对于在应用程序主体中创建诸如模态框/工具提示之类的内容是必要的,以便 overflow:hidden/等不会扭曲其位置或完全截断它。

研究

我发现我可以获取 ApplicationRef,然后通过 hackily 向上遍历并找到 ViewContainerRef

constructor(private applicationRef: ApplicationRef) {
}

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

有了这个后,我可以在该引用上调用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实例的结果上,例如:

但现在我已经创建了组件,但我的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);

虽然上述方法都能达到预期效果,但有时会感觉有些hacky。我也担心像上述那样设置输入属性的生命周期时机,因为它是在创建后发生的。

值得注意的参考资料


1
你不能为动态添加的组件使用绑定。你目前在Angular2中的方法是最好的。我认为Angular2团队会努力改进这一点,但目前还不清楚可以期望什么和何时可以期望。 - Günter Zöchbauer
2个回答

20
在2.3.0版本中,引入了attachView,使你能够将变更检测附加到ApplicationRef上,但是,你仍然需要手动将元素附加到根容器中。这是因为在Angular2中,它运行的环境可能是Web Workers、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 的描述,它说:“当视图被销毁时,它将自动分离”……那么我可以得出结论:订阅 onDestroy 以分离视图已过时? - j3ff
amcdnl,您能否示例说明如何使用上述服务。我正在尝试动态向应用程序根添加组件,并观察更改以便动态删除它(类似于Angular Material 2将背景添加到对话框和菜单的方式)。此外,我在angular文档中找不到attachView()的参考。它已被弃用了吗? - Asaf Agranat
@Rhumbus 你应该研究一下cdk portals,以简化这个问题。 - cgatian
1
你能否更新你的脚本,使其在最新的Angular版本下工作?更新到2020年 :) - Honchar Denys
1
找到了如何使其与最新的Angular兼容。在方法getRootViewContainer()中,将const rootComponent的值替换为this.applicationRef['_rootComponents'] || this.applicationRef['_views'];。同时,将getComponentRootNode()替换为以下代码:return componentRef.hostView ? (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] : (componentRef as any).rootNodes[0] as HTMLElement;。 附注:已在外部库中测试,效果非常好。但这可能并不适用于所有情况。 - Andrey Seregin
显示剩余3条评论

1

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: 类型“HTMLElement”缺少类型“ComponentRef<any>”的以下属性:location、injector、instance、hostView和其他4个。 - Honchar Denys
如果您能在下面附上一个使用最新的Angular版本可以正常工作的相同文件,那就太好了,谢谢 :) - Honchar Denys

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接