2

I've been trying to dynamically create a component and append it to the document tag. I've been having a hard time figuring out how to select the body's ViewContainterRef so I can just append a new component using the ComponentFactoryResolver.

I tried to obtain a reference to the body container using the code below, but it does not work. Does anybody know how to do it? Thanks!

import {
    Component,
    ComponentRef,
    ApplicationRef,
    Injector,
    Input,
    ViewContainerRef,
    ComponentFactoryResolver,
    ViewChild,
    OnInit,
    OnDestroy
} from '@angular/core';

import {
    ModalComponent
} from './modal.component';

@Component({
    selector: 'my-modal'
})
export class MyModalComponent {

    private _bodyRef: ViewContainerRef;


    constructor(private resolver: ComponentFactoryResolver, private app: ApplicationRef) {

        // Does not work!
        this._bodyRef = app['_rootComponents'][0]['_hostElement'].vcRef;

    }


    ngOnInit() {

        // Calls the factory to crate a brand new instance
        let componentFactory = this.resolver.resolveComponentFactory(ModalComponent);
        this._bodyRef.createComponent(componentFactory);


    }
}
Juliano
  • 443
  • 1
  • 5
  • 16
  • You can take a look at my example of similar thing for googlemap infoWindow http://stackoverflow.com/questions/40922224/angular2-component-into-dynamicaly-created-element/40926110#40926110 – yurzui Dec 17 '16 at 04:20

2 Answers2

10

I resolved this with separate service:

import {
  Injectable,
  ComponentFactoryResolver,
  ApplicationRef,
  Injector,
  EmbeddedViewRef,
  ComponentRef
} from '@angular/core';

@Injectable()
export class DOMService {

  constructor(
    private componentFactoryResolver: ComponentFactoryResolver,
    private applicationRef: ApplicationRef,
    private injector: Injector,
  ) {}

  appendComponentToBody(component: any) {
    //create a component reference
    const componentRef = this.componentFactoryResolver.resolveComponentFactory(component)
      .create(this.injector);

    // attach component to the appRef so that so that it will be dirty checked.
    this.applicationRef.attachView(componentRef.hostView);

    // get DOM element from component
    const domElem = (componentRef.hostView as EmbeddedViewRef < any > )
      .rootNodes[0] as HTMLElement;

    document.body.appendChild(domElem);

    return componentRef;
  }

  removeComponentFromBody(componentRef: ComponentRef < any > ) {
    this.applicationRef.detachView(componentRef.hostView);
    componentRef.destroy();
  }
}

Then in your component:

import {
  Component,
  AfterContentInit
} from '@angular/core';
import {
  ComponentToInject
} from 'path/to/component';
import {
  DOMService
} from 'path/to/service';

@Component({
  selector: 'my-component',
  template: '....'
})
export class MyComponent implements AfterContentInit {

  constructor(
    private DOMService: DOMService,
  ) {}

  ngAfterContentInit() {
      // to prevent ExpressionChangedAfterItHasBeenCheckedError
      setTimeout(() => {
          const cmp = this.DOMService.appendComponentToBody(ComponentToInject);

          // if you need to get access to input of injected component. Let's say ComponentToInject has public property title
          const instance = cmp.instance;
          instance.title = 'Some title';

          // if you need to get access to output of injected component. Let's say ComponentToInject assings EventEmitter to onClick property
          instance.onClick.subscribe(() => { // do somethis })
          });
        }

      }
wawka
  • 4,828
  • 3
  • 28
  • 22
  • 2
    ideally, the document should be injected -- `constructor(@Inject(DOCUMENT) private document: Document)` – Irshad Jul 25 '20 at 03:41
6

Angular2 material team are doing something similar to this for the tooltip and other dynamic components that the view might be outside of the current container ref.

This is the class : https://github.com/angular/material2/blob/master/src/lib/core/portal/dom-portal-host.ts What they're doing :

First injecting bunch of useful classes :

constructor(
      private _viewRef: ViewContainerRef,
      private _hostDomElement: Element,
      private _componentFactoryResolver: ComponentFactoryResolver,
      private _appRef: ApplicationRef,
      private _defaultInjector: Injector) {
  }

Then : Creating the component in the current viewContainerRef , which in your case is your modal.

ngOnInit() {

        // Calls the factory to crate a brand new instance
        let componentFactory = this._componentFactoryResolver.resolveComponentFactory(ModalComponent);
        let componentRef = this._viewRef.createComponent(componentFactory);


    }

Then attaching it to the appRef

 (this._appRef as any).attachView(componentRef.hostView);

        this.setDisposeFn(() => {
          (this._appRef as any).detachView(componentRef.hostView);
          componentRef.destroy();
        });

I've never done this , so you might need to give it some effort , but I assume this is the way.

Milad
  • 27,506
  • 11
  • 76
  • 85
  • Although you are pointing in the right direction ... this answer is more explicit http://stackoverflow.com/a/40687392/5583283 – j3ff Apr 05 '17 at 15:48