Solvedangular Get a ViewContainerRef for a provider that is passed to bootstrap to load components
✔️Accepted Answer
@scttcper - Ok that actually helped. I was able to abstract out all their specific logic and make this work standalone service. Here is the code for others:
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);
// ApplicationRef's attachView and detachView methods are in Angular ^2.2.1 but not before.
// The `else` clause here can be removed once 2.2.1 is released.
if (appRef['attachView']) {
appRef.attachView(componentRef.hostView);
componentRef.onDestroy(() => {
appRef.detachView(componentRef.hostView);
});
} else {
// When creating a component outside of a ViewContainer, we need to manually register
// its ChangeDetector with the application. This API is unfortunately not published
// in Angular <= 2.2.0. The change detector must also be deregistered when the component
// is destroyed to prevent memory leaks.
let changeDetectorRef = componentRef.changeDetectorRef;
appRef.registerChangeDetector(changeDetectorRef);
componentRef.onDestroy(() => {
appRef.unregisterChangeDetector(changeDetectorRef);
// Normally the ViewContainer will remove the component's nodes from the DOM.
// Without a ViewContainer, we need to manually remove the nodes.
if (componentRootNode.parentNode) {
componentRootNode.parentNode.removeChild(componentRootNode);
}
});
}
location.appendChild(componentRootNode);
return componentRef;
}
}
Other Answers:
+1
@HeavenlyHost I've used this method to add a modal service to my application. It doesn't need to inherit any existing providers other than the ApplicaionRef. Originally I was unable to get the ApplicationRef provider by passing it to the constructor of the service because I'm loading the modal service during bootstrap so I had to provide the injector and then manually inject the ApplicationRef provider when opening the modal. I don't know if this method might allow you to get existing providers but I thought I'd post it and see if it might help you. If this doesn't work you might have to get one of the first child components below the app component and then load to next to that location instead of the root. It would be nice to have a function that instead of "loadNextToLocation" you could "loadWithinLocation" or something like that so you could dynamically load a component as a child of an existing component instead of next to it.
@Injectable()
export class ModalService {
private currentModal: ModalComponent;
constructor(private dynamicComponentLoader: DynamicComponentLoader, private injector: Injector) { }
public openModal(): Promise<ModalComponent> {
var appRef = <ApplicationRef>this.injector.get(ApplicationRef);
let viewRef: ViewContainerRef = appRef['_rootComponents'][0]['_hostElement'].vcRef;
return this.dynamicComponentLoader.loadNextToLocation(ModalComponent, viewRef).then((compRef: ComponentRef<ModalComponent>) => {
this.currentModal = compRef.instance.modalInit(compRef);
return this.currentModal;
});
}
}
I'm submitting a ... (check one with "x")
Current behavior
I can't get (inject)
ViewContainerRef
s for a top-level (application) component.Expected/desired behavior
Should be able to inject
ViewContainerRef
s for a top-level (application) component as it might be needed to dynamically load a component from a service.Reproduction of the problem
http://plnkr.co/edit/eM3ibINNHgGB7Y2GyC4i?p=preview
What is the motivation / use case for changing the behavior?
There is a class of widgets (modals, popovers, tooltips etc.) that:
body
)Taking all the above we need to dynamically create a component (modal window) and provide projectable nodes (modal content). To make projectable nodes "live" we need a
ViewContainerRef
but since we are in a service we don't have access to anyViewContainerRef
- hence the request.@tbosch I was discussing this use-case yesterday with @mhevery and he advised to open an issue / discuss it with you.
Related: #8941