根中的Angular2动态组件注入
Angular2 Dynamic Component Injection in Root
问题
我正在寻找将 known/defined 组件注入应用程序根目录并将 @Input()
选项投影到该组件的最佳方法。
要求
这对于在应用程序主体中创建 modals/tooltips 之类的东西是必要的,这样 overflow:hidden
/etc 就不会扭曲位置或将其完全切断。
研究
我发现我可以得到 ApplicationRef
,然后笨拙地向上遍历并找到 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
属性中的 none 已实现。为此,我必须手动遍历我的选项并将它们设置为 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);
综上所述,以上所有实际上都有效,但有时感觉有点老套。我还担心像上面这样设置输入属性的生命周期时间,因为它发生在创建之后。
著名参考文献
在 2.3.0 中,引入了 attachView
,它允许您能够将更改检测附加到 ApplicationRef
,但是,您仍然需要手动将元素附加到根容器。这是因为对于 Angular2,其 运行 环境的可能性可能是 web workers、universal、nativescript 等,所以我们需要明确地告诉它 where/how 我们想将其添加到视图中。
下面是一个示例服务,它允许您动态插入组件并自动投影组件的 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;
}
}
getRootViewContainer
需要对 Angular 的较新版本进行如下修改。它的其余部分就像一个魅力。
getRootViewContainer(): ComponentRef<any> {
if(this._container) return this._container;
return (this.applicationRef.components[0].hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
}
问题
我正在寻找将 known/defined 组件注入应用程序根目录并将 @Input()
选项投影到该组件的最佳方法。
要求
这对于在应用程序主体中创建 modals/tooltips 之类的东西是必要的,这样 overflow:hidden
/etc 就不会扭曲位置或将其完全切断。
研究
我发现我可以得到 ApplicationRef
,然后笨拙地向上遍历并找到 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
属性中的 none 已实现。为此,我必须手动遍历我的选项并将它们设置为 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);
综上所述,以上所有实际上都有效,但有时感觉有点老套。我还担心像上面这样设置输入属性的生命周期时间,因为它发生在创建之后。
著名参考文献
在 2.3.0 中,引入了 attachView
,它允许您能够将更改检测附加到 ApplicationRef
,但是,您仍然需要手动将元素附加到根容器。这是因为对于 Angular2,其 运行 环境的可能性可能是 web workers、universal、nativescript 等,所以我们需要明确地告诉它 where/how 我们想将其添加到视图中。
下面是一个示例服务,它允许您动态插入组件并自动投影组件的 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;
}
}
getRootViewContainer
需要对 Angular 的较新版本进行如下修改。它的其余部分就像一个魅力。
getRootViewContainer(): ComponentRef<any> {
if(this._container) return this._container;
return (this.applicationRef.components[0].hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
}