Как реализовать динамические компоненты?

В Angular динамическими называются компоненты, которые создаются и вставляются в DOM во время выполнения (runtime), а не заранее определены в шаблоне. Это позволяет реализовывать такие сценарии, как диалоговые окна, виджеты, табы, динамические формы, шаблоны на основе конфигурации, плагин-системы и т.д.

1. Базовая реализация через ViewContainerRef + ComponentRef

1.1. Подготовка компонента для динамической вставки

@Component({
selector: 'app-dynamic-child',
template: \`<p>Я динамический компонент</p>\`
})
export class DynamicChildComponent {}

1.2. Контейнер в шаблоне хоста

<ng-template #container></ng-template>

1.3. Логика создания компонента

@ViewChild('container', { read: ViewContainerRef, static: true })
container!: ViewContainerRef;
constructor(private cfr: ComponentFactoryResolver) {}
loadComponent() {
this.container.clear();
const factory = this.cfr.resolveComponentFactory(DynamicChildComponent);
const componentRef = this.container.createComponent(factory);
}

2. Использование createComponent без ComponentFactory (Angular 13+)

С версии Angular 13 ComponentFactoryResolver не обязателен.

import { DynamicChildComponent } from './dynamic-child.component';
@ViewChild('container', { read: ViewContainerRef, static: true })
vcRef!: ViewContainerRef;
loadComponent() {
this.vcRef.clear();
const componentRef = this.vcRef.createComponent(DynamicChildComponent);
componentRef.instance.property = 'значение';
}

3. Передача данных в динамический компонент

Если у динамического компонента есть входные свойства (@Input()), их можно задать после создания:

componentRef.instance.title = 'Динамический заголовок';
componentRef.instance.items = \['a', 'b', 'c'\];

4. Получение событий через @Output

componentRef.instance.closed.subscribe(() => {
this.vcRef.clear();
});

Обязательно отписываться или использовать takeUntil внутри компонента.

5. Использование ngComponentOutlet

Более декларативный способ — использовать директиву ngComponentOutlet.

<ng-container \*ngComponentOutlet="currentComponent"></ng-container>

В TS:

currentComponent = DynamicChildComponent;

Дополнительно можно передавать:

  • ngComponentOutletInjector — инжектор;

  • ngComponentOutletContent — контент для <ng-content>;

  • ngComponentOutletNgModuleFactory — модуль.

6. Создание компонентов по конфигурации

Допустим, есть конфигурация:

const config = \[
{ type: ChartComponent, data: { chartId: 1 } },
{ type: TableComponent, data: { columns: 3 } }
\];

Цикл создания:

for (let item of config) {
const ref = this.vcRef.createComponent(item.type);
Object.assign(ref.instance, item.data);
}

7. Модульная загрузка (Lazy Loading) динамических компонентов

С Angular 13+ можно лениво загружать компоненты:

const { LazyComponent } = await import('./lazy.component');
this.vcRef.createComponent(LazyComponent);

Это особенно полезно для снижения размера основного бандла.

8. Встраивание контента через ng-content

Компоненты с ng-content можно наполнять через ngComponentOutletContent.

&lt;ng-container \*ngComponentOutlet="comp; ngComponentOutletContent: \[\[tpl\]\]"&gt;&lt;/ng-container&gt;
&lt;ng-template #tpl&gt;
&lt;p&gt;Вставленный HTML&lt;/p&gt;
&lt;/ng-template&gt;

9. Использование Injector, EnvironmentInjector и ChangeDetectorRef

Если необходимо передать зависимости:

const injector = Injector.create({
providers: \[
{ provide: SomeService, useClass: SomeService, deps: \[\] }
\]
});
this.vcRef.createComponent(DynamicComponent, {
injector
});

Можно использовать EnvironmentInjector из Angular 14+ для создания компонента вне контекста модуля.

10. Хранение ссылок на компоненты

Иногда полезно хранить ComponentRef в массиве, чтобы управлять созданными экземплярами:

components: ComponentRef&lt;any&gt;\[\] = \[\];
const ref = this.vcRef.createComponent(DynamicComponent);
this.components.push(ref);
// Позднее можно вызвать destroy
this.components.forEach(c => c.destroy());

11. Удаление компонента

ref.destroy(); // уничтожить конкретный
this.vcRef.clear(); // удалить всё из контейнера

12. Общий компонент-обёртка для создания по типу

Создание универсального DynamicHostComponent, который принимает тип компонента и входные данные:

@Component({
selector: 'app-dynamic-host',
template: \`&lt;ng-template #host&gt;&lt;/ng-template&gt;\`
})
export class DynamicHostComponent {
@ViewChild('host', { read: ViewContainerRef }) host!: ViewContainerRef;
load&lt;T&gt;(component: Type&lt;T&gt;, data: Partial&lt;T&gt;) {
const ref = this.host.createComponent(component);
Object.assign(ref.instance, data);
}
}

13. Интеграция с диалогами (например, Angular Material)

this.dialog.open(SomeComponent, {
data: { userId: 42 },
width: '600px'
});

Внутри SomeComponent:

constructor(@Inject(MAT_DIALOG_DATA) public data: any) {}

14. Плагины или кастомные виджеты

Можно создать реестр:

const registry = {
chart: ChartComponent,
table: TableComponent
};
const type = registry\[config.type\];
const ref = this.vcRef.createComponent(type);

Такой подход удобен для CMS, форм-конструкторов и административных панелей.

15. Динамические модули

С loadRemoteModule() или NgModuleFactory, можно загружать не только компонент, но и его модуль. Это используется в micro frontend архитектуре или для плагин-систем.

Динамические компоненты в Angular позволяют строить гибкие и расширяемые приложения, управлять отображением компонентов во время выполнения, использовать шаблоны и конфигурации, а также разделять крупные модули на независимые части.