Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 2: Inserting capture element dynamically when creating components (dynamically)

My goal is to create a child component and insert into the parent component template. There are examples to do this. However, I create parent component template (DOM Elements) dynamically in the parent component while most of the examples shown statically create the template with the capture element.

Here's the code

app.component

import {Component, ViewChild, ViewContainerRef, ComponentFactoryResolver} from '@angular/core';
import {NewChildComponent} from "./newChild.component";

@Component({
 selector: 'app-main',
 templateUrl: 'app.component.html'
})
export class AppComponent {

 @ViewChild('captureElement', {read: ViewContainerRef})
 captureElement: ViewContainerRef;

 constructor (private componentFactoryResolver: ComponentFactoryResolver) {
 var childComponent = this.componentFactoryResolver.resolveComponentFactory(NewChildComponent); 

 var myArea = document.getElementById('myArea');
 var myRow = document.createElement("div");
 myArea.appendChild(myRow);

 //I want to add the capture element #myCapture as a child of myRow
 //Add something like this <div #captureElement></div> programmatically through JS/TS
 // How can I do this?

 // I then create the component
  this.parent.createComponent(NewChildComponent);

 }

app.component.html

<div id="myArea">
  <!-- Static way of doing it -->
  <!--<div #captureElement></div>-->      
</div>

Instead of statically defining in #captureElement where the child component would be inserted, I would like to create it dynamically in the parent component and make it a child component.

Here are a list of questions I referred before I asked this question

  • Angular2: Insert a dynamic component as child of a container in the DOM
  • How to place a dynamic component in a container
  • Angular 2 dynamic tabs with user-click chosen components

Tried a couple of things

  1. Tried to create a div element with a #captureElement as an attribute programmatically but that doesn't work.
  2. Tried to create a random element programmatically and use ViewContainerRef to find it. That doesn't work either.
like image 384
am3 Avatar asked Dec 01 '22 15:12

am3


1 Answers

We can't create a ViewContainerRef, as a ViewContainerRef only exists within a view.

In 2.3.0, attachView was introduced which allows you to be able to attach change detection to the ApplicationRef. You can create some class that will encapsulate your logic like:

export class HtmlContainer {
   private attached: boolean = false;

   private disposeFn: () => void;

   constructor(
    private hostElement: Element,
    private appRef: ApplicationRef,
    private componentFactoryResolver: ComponentFactoryResolver, 
    private injector: Injector) {
  }

  attach(component: Type<any>) : ComponentRef<any> {
    if(this.attached) {
      throw new Error('component has already been attached')
    }

    this.attached = true;
    const childComponentFactory = this.componentFactoryResolver.resolveComponentFactory(component);

    let componentRef = childComponentFactory.create(this.injector);

    this.appRef.attachView(componentRef.hostView);
    this.disposeFn = () => {
        this.appRef.detachView(componentRef.hostView);
        componentRef.destroy();
    };

    this.hostElement.appendChild((componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0]);

    return componentRef;
  }

  dispose() {
    if(this.attached) {
      this.disposeFn();
    }
  }
}

this class is just helper that

1) resolves your dynamic component

this.componentFactoryResolver.resolveComponentFactory(component);

2) then compiles component by calling compFactory.create

3) after that registers its changeDetector (componentRef.hostView extends ChangeDetectorRef) by calling mentioned above appRef.attachView (otherwise change detection won't work for your component)

4) and finally appends the rootNode of your component to the host element

this.hostElement.appendChild((componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0]);

You can use it as follows:

@Component({
  selector: 'my-app',
  template: `<div id="myArea"></div> `,
  entryComponents: [NewChildComponent]
})
export class AppComponent {
  containers: HtmlContainer[] = [];

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

  ngOnInit() {
    var myArea = document.getElementById('myArea');
    var myRow = document.createElement("div");
    myArea.appendChild(myRow);

    this.addComponentToRow(NewChildComponent, myRow, 'test1');
    this.addComponentToRow(NewChildComponent, myRow, 'test2');
  }

  addComponentToRow(component: Type<any>, row: HTMLElement, param: string) {
    let container = new HtmlContainer(row, this.appRef, this.componentFactoryResolver, this.injector);
    let componentRef = container.attach(component);
    componentRef.instance.param1 = param;

    this.containers.push(container);
  }

  ngOnDestroy() {
    this.containers.forEach(container => container.dispose());
  }
}

Plunker Example

See also

  • Angular2 - Component into dynamicaly created element
  • Angular2 Dynamic Component Injection in Root
  • https://github.com/angular/material2/blob/2.0.0-beta.1/src/lib/core/portal/dom-portal-host.ts#L30-L86 (you can find here fallback for angular < 2.3.0)
like image 130
yurzui Avatar answered Dec 04 '22 03:12

yurzui