Search code examples
angularangular-dynamic-componentsangular-content-projection

Projecting a component inside a dynamic component


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.


Solution

  • 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