Angular2 Динамическая инжекция компонентов в корне

Вопрос

Я ищу наилучший подход для ввода известного/определенного компонента в корень приложения и проектирования @Input() параметров на этот компонент.

Требование

Это необходимо для создания таких вещей, как модалы/всплывающие подсказки в теле приложения, так что overflow:hidden/etc не будет искажать позицию или полностью отключить ее.

Исследование

Я обнаружил, что могу получить ApplicationRef, а затем взломать его вверх и найти ViewContainerRef.

constructor(private applicationRef: ApplicationRef) {
}

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

как только у меня есть, то я могу позвонить createComponent в поле ref:

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);

все, что сказано, все вышеизложенное действительно работает, но время от времени оно бывает немного взломанным. Я также беспокоюсь о сроках жизненного цикла установки параметров ввода, как указано выше, так как это происходит после его создания.

Заметные ссылки

Ответ 1

В версии 2.3.0 был введен attachView, который позволяет вам прикрепить обнаружение изменений к ApplicationRef, однако вам все равно нужно вручную добавить элемент в корневой контейнер. Это связано с тем, что при использовании Angular2 возможностей среды его работы могут быть веб-рабочие, универсальные, 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;
  }
}

Ответ 2

getRootViewContainer необходимо изменить, как getRootViewContainer ниже, для более новых версий Angular. Остальное работает как шарм.

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

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