Projecting a component inside a dynamic component

103 Views Asked by At

I can create a component dynamically and project some template inside it just fine with the following code -

@Component({
    selector: 'dynamic',
    template: `
        <p>Dynamic Component</p>
        <ng-content></ng-content>
    `
})
export class DynamicComponent { }

@Component({
    selector: 'tester',
    template: `
        <p>Tester Component</p>
        <ng-container #cnt></ng-container>
        <ng-template #tpl>
            <p>[Projected Content]</p>
        </ng-template>
    `
})
export class TesterComponent implements AfterViewInit {

    @ViewChild('cnt', { read: ViewContainerRef }) cnt: ViewContainerRef
    @ViewChild('tpl') tpl: TemplateRef<any>

    ngAfterViewInit(): void {
        let content = [this.tpl.createEmbeddedView(null).rootNodes];
        this.cnt.createComponent(DynamicComponent, { projectableNodes: content });
    }
}

But I want to project another component (e.g. the following one) inside the dynamic component -

@Component({
    selector: 'content',
    template: `<p>Content Component</p>`
})
export class ContentComponent { }

so that the resulting output becomes the equivalent of -

<p>Tester Component</p>
<dynamic>
    <content></content>
</dynamic>

Cannot figure out how to achieve that. Any help/suggestion would be appreciated. Thanks.

2

There are 2 best solutions below

3
Naren Murali On BEST ANSWER

UPDATE

We can use the createComponent API to dynamically create the component and then using contentRef.location.nativeElement project the content onto the dynamic created component, note the double array syntax, that got this working!

test.ts

import {
  Component,
  ViewChild,
  TemplateRef,
  ViewContainerRef,
  ContentChild,
  createComponent,
  Injector,
  inject,
  ElementRef,
  EnvironmentInjector,
} from '@angular/core';

@Component({
  selector: 'dynamic',
  standalone: true,
  template: `
      <p>Dynamic Component</p>
      <ng-content></ng-content>
  `,
})
export class DynamicComponent {}

@Component({
  selector: 'tester',
  standalone: true,
  template: `
      <p>Tester Component</p>
      <ng-container #cnt></ng-container>
      <ng-template #tpl>
          <p>[Projected Content]</p>
      </ng-template>
  `,
})
export class TesterComponent {
  @ViewChild('cnt', { read: ViewContainerRef }) cnt!: ViewContainerRef;
  @ViewChild('tpl') tpl!: TemplateRef<any>;
  @ContentChild('content') contentChild!: TemplateRef<any>;

  constructor(
    private injector: EnvironmentInjector,
    private elementRef: ElementRef
  ) {}

  ngAfterViewInit(): void {
    const elementInjector = Injector.create({
      providers: [
        {
          provide: 'MyToken',
          useValue: 'Token',
        },
      ],
    });

    const contentRef = createComponent(ContentComponent, {
      environmentInjector: this.injector,
      elementInjector,
    });
    this.cnt.createComponent(DynamicComponent, {
      projectableNodes: [[contentRef.location.nativeElement]] as any,
    });
  }
}

@Component({
  selector: 'app-content',
  standalone: true,
  template: `<p>Content Component</p>`,
})
export class ContentComponent {}

Stackblitz Demo


We can just access the content using ContentChild and then using a or condition, we can either use the content child, or use the view child, whichever is present in the defined order!

ts

import {
  Component,
  ViewChild,
  TemplateRef,
  ViewContainerRef,
  ContentChild,
} from '@angular/core';

@Component({
  selector: 'dynamic',
  standalone: true,
  template: `
      <p>Dynamic Component</p>
      <ng-content></ng-content>
  `,
})
export class DynamicComponent {}

@Component({
  selector: 'tester',
  standalone: true,
  template: `
      <p>Tester Component</p>
      <ng-container #cnt></ng-container>
      <ng-template #tpl>
          <p>[Projected Content]</p>
      </ng-template>
  `,
})
export class TesterComponent {
  @ViewChild('cnt', { read: ViewContainerRef }) cnt!: ViewContainerRef;
  @ViewChild('tpl') tpl!: TemplateRef<any>;
  @ContentChild('content') contentChild!: TemplateRef<any>;

  ngAfterViewInit(): void {
    console.log('content', this.tpl);
    console.log(this.contentChild);
    this.cnt.createComponent(DynamicComponent, {
      projectableNodes: [
        this.contentChild?.createEmbeddedView(null)?.rootNodes ||
          this.tpl?.createEmbeddedView(null)?.rootNodes,
      ],
    });
  }
}

@Component({
  selector: 'app-content',
  standalone: true,
  template: `<p>Content Component</p>`,
})
export class ContentComponent {}

main.ts

import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import 'zone.js';
import { ContentComponent, DynamicComponent, TesterComponent } from './test';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [DynamicComponent, TesterComponent, ContentComponent],
  template: `
    <tester/>
    <tester>
      <ng-template #content>
        <app-content/>
      </ng-template>
    </tester>
  `,
})
export class App {
  name = 'Angular';
}

bootstrapApplication(App);

Stackblitz Demo

1
Paulo Duarte Rettamozo On

There is a feature in @angular/common, NgComponentOutlet, which allows you to bring a component dynamically with @Input() decorator in your component and inject via Template Html

@Component({
  selector: 'ng-component-outlet-complete-example',
  template: ` <ng-template #ahoj>Ahoj</ng-template>
    <ng-template #svet>Svet</ng-template>
    <ng-container
      *ngComponentOutlet="
        CompleteComponent;
        inputs: myInputs;
        injector: myInjector;
        content: myContent
      "
    ></ng-container>`,
})

// Example taken from documentation (ref)


ReferĂȘncias:

https://angular.io/api/common/NgComponentOutlet