Search code examples
angularangular13angular-dynamic-components

Attach Angular component to document body


I would like to render an Angular component (an overlay) as a direct child of the documents body element. Today this can be done as follows:

constructor(
  private applicationRef: ApplicationRef,
  private componentFactoryResolver: ComponentFactoryResolver,
  @Inject(DOCUMENT) private document: Document,
  private injector: Injector
) {}

public renderOverlayInBody(): void {
  const overlayRef = this.componentFactoryResolver
    .resolveComponentFactory(OverlayComponent)
    .create(this.injector);
  this.applicationRef.attachView(overlayRef.hostView);
  const overlayRoot = (overlayRef.hostView as EmbeddedViewRef<unknown>)
    .rootNodes[0] as HTMLElement;

  // This component can be appended to any place in the DOM,
  // in particular directly to the document body.
  this.document.body.appendChild(overlayRoot);
}

Demo in Stackblitz

Unfortunately, ComponentFactoryResolver has been deprecated in Angular 13 and may be removed in Angular 16. The suggested replacement is ViewContainerRef.createComponent:

constructor(private viewContainerRef: ViewContainerRef) {}

public ngOnInit(): void {
  // This component can only be appended to the current one,
  // in particular not directly to the document body.
  this.viewContainerRef.createComponent(OverlayComponent);
}

While this is much simpler to read, it doesn't allow to render components as direct children of the documents body element. Is there any way to do that, which doesn't rely on currently deprecated code?


Solution

  • The way I finally fixed this problem was to get the ApplicationRef within my service. From there you can take the first component, which is the root component. Once you have this ComponentRef, you can access its injector and get/inject the ViewContainerRef for that component. Having the ViewContainerRef then allows you to call createComponent on it, like so:

    export class MyService
        private static overlayRef: OverlayComponent
    
        constructor(
            private applicationRef: ApplicationRef
        ) {
            if (MyService.overlayRef === undefined) {
                // Get the root view container ref of the application by injecting it into the root component
                const rootViewContainerRef = this.applicationRef.components[0].injector.get(ViewContainerRef);
                // Insert the modal component into the root view container
                const componentRef = rootViewContainerRef.createComponent(OverlayComponent);
                // Get the instance of the modal component
                MyService.overlayRef = componentRef.instance;
            }
        }
    

    The reason you need the static property is that if you're using standalone components and not using this service through a module the service will destroy itself and recreate itself constantly, leading you to have multiple OverlayComponents.

    Also, this code can only insert your OverlayComponent adjacent to your current app root (as a sibling). If your app root isn't in the HTML body, it can't do anything about that (but I'm guessing, since it's an overlay, you don't really care if it's in the body or not since you just want it on top of other page elements (which you'll be able to do using this method).