Search code examples
angularlazy-loadingasync-pipeng-component-outletdynamic-components

How to use the AsyncPipe with ngComponentOutlet


I want to build a UI dynamically based on a JSON config. Trying to use ngComponentOutlet with the AsyncPipe so I can import(...) the components lazily. My implementation is not working (see example on Stackblitz). There are no errors and the components never initialize.

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [NgComponentOutlet, NgFor, AsyncPipe],
  template: `
    <ng-container *ngFor="let component of schema">
      <ng-content *ngComponentOutlet="load(component.name) | async; inputs: component.inputs"></ng-content>
    </ng-container>
  `,
})
export class App {
  // schema returned from API call
  schema = [
    {
      name: 'Lazy1',
      inputs: {
        title: 'Lazy 1 Component',
      },
    },
    {
      name: 'Lazy2',
      inputs: {
        title: 'Lazy 2 Component',
      },
    },
  ];

  componentManifest: Record<string, { load: () => Promise<unknown> }> = {
    Lazy1: {
      load: () => import('./app/lazy-components/lazy1.component'),
    },
    Lazy2: {
      load: () => import('./app/lazy-components/lazy2.component'),
    },
  };

  async load(component: string) {
    return await this.componentManifest[component]
      .load()
      .then((c) => (c as any)[component]);
  }
}


Solution

  • Changes to be made:

    1. ngComponentOutlet belongs to ng-container so we can modify the code to use it.

    2. The key of componentManifest must contain the component name, because the import has the same name. Same applies for the name property of schema array.

    3. We can preprocess the imports in the constructor and store it in a property component to be processed by the async pipe. Else the load method will be continuously called during every change detection which is a bottleneck.

    Full Code:

    import {
      AsyncPipe,
      CommonModule,
      NgComponentOutlet,
      NgFor,
    } from '@angular/common';
    import { Component } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import { firstValueFrom, from } from 'rxjs';
    import 'zone.js';
    
    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [NgComponentOutlet, NgFor, AsyncPipe, CommonModule],
      template: `
        <ng-container *ngFor="let component of schema">
          <ng-container *ngComponentOutlet="(component['component'] | async); inputs: component.inputs"></ng-container>
        </ng-container>
      `,
    })
    export class App {
      // schema returned from API call
      schema: any = [
        {
          name: 'Lazy1Component',
          inputs: {
            title: 'Lazy 1 Component',
          },
        },
        {
          name: 'Lazy2Component',
          inputs: {
            title: 'Lazy 2 Component',
          },
        },
      ];
    
      componentManifest: Record<string, { load: () => Promise<unknown> }> = {
        Lazy1Component: {
          load: () => import('./app/lazy-components/lazy1.component'),
        },
        Lazy2Component: {
          load: () => import('./app/lazy-components/lazy2.component'),
        },
      };
    
      constructor() {
        if (this.schema?.length) {
          this.schema.forEach((item: any) => {
            item['component'] = this.load(item.name);
          });
        }
      }
    
      load(component: string): any {
        return this.componentManifest[component].load().then((c: any) => {
          return (c as any)[component];
        });
      }
    }
    
    bootstrapApplication(App);
    

    Stackblitz Demo