题
我正在寻找将已知/已定义组件注入应用程序根目录并将@Input()选项投影到该组件上的最佳方法.
需求
这对于在应用程序主体中创建模态/工具提示等内容是必要的,这样溢出:隐藏/等不会扭曲位置或完全切断它.
研究
我发现我可以得到ApplicationRef,然后hackily向上遍历并找到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); }
但现在我已经创建了组件,但没有完成我的输入属性.为了实现这一点,我必须手动遍历我的选项并在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,childInjector);
所有这一切,以上所有实际上都有效,但有时候感觉有点哈哈.我也关注设置输入属性的生命周期时间,因为它在创建之后发生.
值得注意的参考文献
> https://github.com/angular/angular/issues/9293
> https://github.com/angular/angular/issues/6446
在2.3.0中,引入了attachView,它允许您将更改检测附加到ApplicationRef,但是,您仍然需要手动将元素附加到根容器.这是因为使用Angular2,其运行环境的可能性可能是web worker,universal,nativescript等,所以我们需要明确告诉它我们想要将它添加到视图的位置/方式.
下面是一个示例服务,它允许您动态插入组件并自动投影组件的输入.
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; } }