Skip to content

Loading components dynamically

@flyze/lib-core-angular 1.0.0-alpha.34· latest

How to render a component that is chosen at runtime and hand it a configuration.

Prerequisite: DynamicComponentModule imported — see getting-started. Mechanics: features/dynamic-component-loader.

Implement DynamicComponent<T> and declare a config property:

export interface TileConfig {
title: string;
value: number;
}
@Component({
selector: 'app-tile',
templateUrl: './tile.component.html'
})
export class TileComponent implements DynamicComponent<TileConfig> {
public config: TileConfig;
}

Do not make config an @Input() — the directive assigns it on the instance, there is no template binding involved.

config is set before the component’s first change detection, so it is already populated in ngOnInit. It is not populated in the constructor.

To react to later config changes, declare setConfig instead of a plain property — the directive prefers it when it exists:

export class TileComponent implements DynamicComponent<TileConfig> {
public config: TileConfig;
setConfig(config: TileConfig) {
this.config = config;
this.recalculate();
}
}

ngOnChanges never fires for a dynamically loaded component. setConfig is the replacement.

<ng-container loadComponent [config]="tile"></ng-container>
public tile: DynamicComponentLoaderConfig = {
component: TileComponent,
config: { title: 'Orders', value: 42 }
};

The component is inserted after the <ng-container>, as a sibling — the container is an anchor, not a wrapper.

Assign a new config object:

// same component, new data — the instance is kept, setConfig()/config is updated
this.tile = { component: TileComponent, config: { title: 'Orders', value: 43 } };
// different component — the old instance is destroyed and a new one created
this.tile = { component: ChartComponent, config: { series: [] } };

The directive compares the component reference to decide which of the two happens, so component state survives a data-only change.

Omitting config entirely leaves the component’s own default in place — it does not reset it to undefined.

For a shared object that every loaded component may need (a gateway, a host API):

<ng-container loadComponent [config]="tile" [environment]="gateway"></ng-container>

Only components that already declare the property receive it:

export class TileComponent implements DynamicComponent<TileConfig> {
public environment: Gateway; // declared -> assigned
// or
environment(gateway: Gateway) { … } // a method -> called
}

A component that declares neither is skipped silently. Rename the target property with [environmentKey]="'host'".

When the component name comes out of stored data, register the class and pass the string:

@Component({ … })
@DynamicComponentDecorator('TileComponent')
export class TileComponent implements DynamicComponent<TileConfig> {}
public tile = { component: 'TileComponent', config: { … } };

Pass the key explicitly, as a literal. It cannot be derived from the class name: minification renames classes, so a production build would register keys nobody can look up.

Two requirements:

  • The key must match the string in the config exactly — it is a plain Map lookup, no normalization.
  • The class’s module must have been loaded before the config is used. Registration happens when the file is evaluated, so a component sitting in a lazy chunk is unknown until that chunk loads. An unknown key logs [loadComponent] Component not found and then throws.

Registering in a barrel file that the app imports eagerly is the simplest way to guarantee both.

<ng-container loadComponent [config]="tile" (componentLoaded)="onLoaded($event)"></ng-container>

$event is the ComponentRef. It fires on every create — including the re-create after a component swap — but before config and environment are applied, so do not read ref.instance.config in the handler. Use it to keep the ref for later imperative access.

The /load-component route of the test app loads the same component twice by string key with different configs.