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]);
}
}
Changes to be made:
ngComponentOutlet
belongs to ng-container
so we can modify the code to use it.
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.
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.
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);