41

Is there a way to insert dynamically a component as a child (not a sibling) of a DOM tag in Angular 2?

There are plenty of examples around there to insert a dynamic component as a sibling of a given ViewContainerRef's tag, like (as of RC3):

@Component({
  selector: '...',
  template: '<div #placeholder></div>'
})
export class SomeComponent {
  @ViewChild('placeholder', {read: ViewContainerRef}) placeholder;

  constructor(private componentResolver: ComponentResolver) {}

  ngAfterViewInit() {
    this.componentResolver.resolveComponent(MyDynamicComponent).then((factory) => {
        this.componentRef = this.placeholder.createComponent(factory);
    });
  }
}

But this generates a DOM similar to:

<div></div>
<my-dynamic-component></my-dynamic-component>

Expected result:

<div>
    <my-dynamic-component></my-dynamic-component>
</div>

Using the SomeComponent's ViewContainerRef has the same result, it is still inserting the generated component as a sibling, not a child. I would be okay with a solution where the template is empty and dynamic components are inserted in the template (within the component selector tag).

The DOM structure is very important when using libraries like ng2-dragula to drag from a list of dynamic components and benefit from the model updates. The extra div is in the list of draggable elements, but outside the model, breaking the drag & drop logic.

Some say it is not possible (c.f. this comment), but it sounds like a very surprising limitation.

Community
  • 1
  • 1
Antoine OL
  • 1,270
  • 1
  • 12
  • 17

6 Answers6

56

TL;DR: replace <div #placeholder></div> with <div><ng-template #placeholder></ng-template></div> to insert inside the div.

Here is a working stackblitz example (Angular 6), and the relevant code:

@Component({
  selector: 'my-app',
  template: `<div><ng-template #container></ng-template></div>`
})
export class AppComponent implements OnInit {

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

    constructor(private compiler: Compiler) {}

    ngOnInit() {
      this.createComponentFactory(MyDynamicComponent).then(
        (factory: ComponentFactory<MyDynamicComponent>) => this.viewContainer.createComponent(factory),
        (err: any) => console.error(err));
    }

    private createComponentFactory(/*...*/) {/*...*/}

}

It seems <ng-container #placeholder></ng-container> is also working (replace ng-template by ng-container). I like this approach because <ng-container> is clearly addressed to this usecase (a container that don't add a tag) and can be used in other situations like NgIf without wrapping in a real tag.


PS: @GünterZöchbauer directed me to the right discussion in a comment, and I finally answered my own question.


Edit [2018-05-30]: Updated to stackblitz link to have a working, up-to-date example.

Antoine OL
  • 1,270
  • 1
  • 12
  • 17
  • The placeholder is not known one, which is dynamic. Could it possible to create the ViewChild dynamically.? – Karthick Oct 21 '16 at 19:32
  • @Karthick sorry for the delay. I believe it is possible to get a completely dynamic ViewChild, but I don't know how to do it. The ViewChild has different ways to identify the component to target though. It could be a separate question to ask :) – Antoine OL Nov 02 '16 at 09:59
  • But position of dynamic component would be
    . Could ng-template disappear?
    – Ben Cheng May 24 '18 at 15:29
  • @BenCheng The ng-template/ng-component actually disappears in the rendered HTML. It is just a kind of tag to point the position where you want to insert your component. You can check the updated example and inspect with the browser dev tools to confirm that. – Antoine OL May 30 '18 at 18:30
  • @Antoine. Actually, It occupied a space for it but not render. – Ben Cheng May 31 '18 at 01:53
  • @BenCheng In the link in my answer (stackblitz), I'm not getting the same output as what you describe. The component template is `

    ` and it renders `

    I am inserted dynamically!

    `. The dynamic component is at the place I would expect it to be, not outside the `

    ` and `` disappeared (replaced by an empty comment I guess). Is there something wrong?

    – Antoine OL Jun 05 '18 at 16:24
  • @Antoine. Yes. The example in stackblitz works. I found that i am using angular 5. I guess this may be the root cause. – Ben Cheng Jun 05 '18 at 17:23
  • @BenCheng This approach works since Angular 2 (or at least 4 with the same syntax), so there might be another reason why you get a different behavior. Maybe that could be a good separate SO question :) – Antoine OL Jun 05 '18 at 22:02
  • Good question and excellent answer. However, is there a way to pass parameters to that dynamic component? Only to initialize and identify the component, the rest can be done via a common service... – Myonara Oct 22 '18 at 17:53
  • 1
    The only approach I am aware of is to get the component instance at the end and manually put the values like `myCompInstance.foo = 'Hello world!'`. Btw I guess Angular is doing something like this (because @Input are not accessible in the constructor, only from NgOnInit hook). I updated the stackblitz example to also set a component property. – Antoine OL Oct 22 '18 at 20:33
  • @Antoine Awesome. I like the simplicity of it. Thank You a lot – Myonara Oct 23 '18 at 17:51
12

I was searching for solution for same problem and approved answer not worked for me. But I have found better and much logical solution how to append dynamically created component as child to current host element.

The idea is to use element reference of newly created component and append it to current element ref by using render service. We can get component object via its injector property.

Here is the code:

@Directive({
    selector: '[mydirective]'
})
export class MyDirectiveDirective {
    constructor(
        private cfResolver: ComponentFactoryResolver,
        public vcRef: ViewContainerRef,
        private renderer: Renderer2
    ) {}

    public appendComponent() {
        const factory = 
        this.cfResolver.resolveComponentFactory(MyDynamicComponent);
        const componentRef = this.vcRef.createComponent(factory);
        this.renderer.appendChild(
           this.vcRef.element.nativeElement,
           componentRef.injector.get(MyDynamicComponent).elRef.nativeElement
        );
    }
}
Magicaner
  • 129
  • 2
  • 3
8

My Solution would be quite similar to @Bohdan Khodakivskyi. But I tried to the Renderer2.

  constructor(
    private el: ElementRef,
    private viewContainerRef: ViewContainerRef,
    private componentFactoryResolver: ComponentFactoryResolver,
    private render: Renderer2
  ) {}

  ngOnInit() {
    const componentFactory = this.componentFactoryResolver.resolveComponentFactory(
      MyDynamicComponent,
    );
    const componentRef = this.viewContainerRef.createComponent(componentFactory);
    this.render.appendChild(this.el.nativeElement, componentRef.location.nativeElement)
  }
Ben Cheng
  • 769
  • 10
  • 25
  • Adjusting DOM structure using renderer might make you run into problems with internal change detection, see [this blog post](https://blog.angularindepth.com/working-with-dom-in-angular-unexpected-consequences-and-optimization-techniques-682ac09f6866). I don't know if it will cause issues in this particular case, though... – Maurits Moeys Aug 12 '18 at 12:21
  • This breaks apart if your host has a *ngIf or other directive that transform it into a template. – Marc J. Schmidt May 17 '19 at 18:17
4

My dirty way, just save the reference of dynamically created component (sibling) and move it using vanilla JS:

  constructor(
    private el: ElementRef,
    private viewContainerRef: ViewContainerRef,
    private componentFactoryResolver: ComponentFactoryResolver,
  ) {}

  ngOnInit() {
    const componentFactory = this.componentFactoryResolver.resolveComponentFactory(
      MyDynamicComponent,
    );
    const componentRef = this.viewContainerRef.createComponent(componentFactory);
    this.el.nativeElement.appendChild(componentRef.location.nativeElement);
  }
2

Easier approache is now available with Portal available in @angular/cdk.

npm i @angular/cdk

app.module.ts:

import { PortalModule } from '@angular/cdk/portal';

@NgModule({
  imports: [PortalModule],
  entryComponents: [MyDynamicComponent]
})

SomeComponent:

@Component({
  selector: 'some-component',
  template: `<ng-template [cdkPortalOutlet]="myPortal"></ng-template>`
})
export class SomeComponent {
  ...
  this.myPortal = new ComponentPortal(MyDynamicComponent);
}
Snook
  • 174
  • 10
2

This answer by @Magicaner helped me but is missing a few details to get it working.

In summary, yes you can add a new child component using Renderer2.appendChild. The directive would look like this:

@Directive({
    selector: '[myDynamicDirective]'
})
export class MyDynamicDirective implements OnInit {
    constructor(
        private cfResolver: ComponentFactoryResolver,
        public vcRef: ViewContainerRef,
        private renderer: Renderer2
    ) { }

    ngOnInit() {
      this.appendComponent();
    }

    public appendComponent() {
        const factory =  this.cfResolver.resolveComponentFactory(MyDynamicComponent);
        const componentRef = this.vcRef.createComponent(factory);
        this.renderer.appendChild(
           this.vcRef.element.nativeElement,
           componentRef.injector.get(MyDynamicComponent).elRef.nativeElement
        );
    }
}

It adds a new instance of MyDynamicComponent as a child. To get the elRef from that dynamic component to send to appendChild you can do this in the component definition:

@Component({
  selector: 'my-dynamic-component',
  template: '...'
})
export class MyDynamicComponent {
  constructor(public elRef: ElementRef) { }
}

And also you need to add the dynamic component to entryComponents in the module, and of course add the directive and dynamic component to the module declarations.

Working stackblitz here

Simon C
  • 109
  • 1
  • 8