В этой статье мы поговорим о создании компонентов динамически, шаг за шагом пройдем этот путь. Помимо простого создания компонентов, мы поговорим о более продвинутых вещах, которые можно сделать в рамках этого процесса.

Создание динамических компонентов
В первую очередь нам нужен сам компонент, который мы будем динамически создавать.
@Component({ selector: "alert", template: ` <h1>Alert {{type}}</h1> `, }) export class AlertComponent { @Input() type: string = "success"; }
Будем использовать простой компонент alert, который принимает тип оповещения как инпут свойство
Стоит сказать, что динамические компоненты, а вернее те, которые создаются динамически, являются элементами DOM, и, соответственно, в шаблоне необходимо обеспечить место, куда его нужно добавить.
@Component({ selector: 'my-app', template: ` <template #alertContainer></template> `, }) export class App {}
В компоненте my-app создаем шаблон, используя тег ng-template и добавляем к нему переменную через #. Этот template как раз является местом, куда добавится компонент, дальше будем называть это контейнером. В роли контейнера может выступать любой компонент или элемент DOM.
Теперь в компоненте нужно получить ссылку на контейнер. Это можно сделать с помощью ViewChild. Кстати, у меня уже есть статья, которая поможет разобраться в процессах, связанных с взаимодействием с DOM элементами, где как раз подробно рассказывается о декораторе ViewChild. Манипулируй DOM правильно.
@Component({ selector: 'my-app', template: ` <template #alertContainer></template> `, }) export class App { @ViewChild("alertContainer", { read: ViewContainerRef }) container; }
Дефолтное значение, которое возвращается из ViewChild, это экземпляр компонента или элемента DOM, но в нашем случае нужен ViewContainerRef. Название этого класса говорит само за себя, ViewContainerRef содержит в себе ссылку на контейнер, а также методы, которые позволяют создавать компоненты.

Подготовим небольшой интерфейс для создания компонентов. Добавим на страницу две кнопки, а также заинжектим один сервис в наш компонент.
@Component({ selector: 'my-app', template: ` <template #alertContainer></template> <button (click)="createComponent('success')">Create success alert</button> <button (click)="createComponent('danger')">Create danger alert</button> `, }) export class App { @ViewChild("alertContainer", { read: ViewContainerRef }) container; constructor(private resolver: ComponentFactoryResolver) {} }
Сервис ComponentFactoryResolver содержит метод resolveComponentFactory, который принимает в себя компонент, и возвращает ComponentFactory, в котором, в свою очередь присутствует метод create(), именно он и будет использоваться нашим контейнером для создания компонента.

Итак, теперь есть всё необходимое для метода, создающего компоненты.
createComponent(type) { this.container.clear(); const factory: ComponentFactory = this.resolver.resolveComponentFactory(AlertComponent); this.componentRef: ComponentRef = this.container.createComponent(factory); }
Давайте пройдемся по порядку и посмотрим, что тут происходит
this.container.clear();
Каждый раз, когда мы нажмем кнопу для создания компонента, нам необходимо очистить контейнер от предыдущего представления. Иначе это будет подобно методу push() для массивов, что в данной ситуации не является хорошей практикой.
const factory: ComponentFactory = this.resolver.resolveComponentFactory(AlertComponent);
resolveComponentFactory() принимает класс компонента и возвращает ComponentFactory, где есть всё необходимое чтобы его создать.
this.componentRef: ComponentRef = this.container.createComponent(factory);
Мы вызываем метод createComponent() и передаем переменную типа ComponentFactory. Под капотом этот метод вызывает create() и добавляет готовый компонент как дочерний в контейнер.
После всех этих взаимодействий у нас есть ссылка на компонент, и мы можем установить значение его Input свойства
this.componentRef.instance.type = type;
Так же можно и подписаться на Output свойство компонента.
this.componentRef.instance.output.subscribe(event => console.log(event));

Чтобы избежать утечек памяти, стоит уничтожить компонент после использования
ngOnDestroy() { this.componentRef.destroy(); }
Метод для создания компонента готов, все операции изучены и понятны, и, в конце концов, остается только добавить динамический компонент в массив entryComponents в модуле.
@NgModule({ entryComponents: [ AlertComponent ], bootstrap: [ App ] }) export class AppModule {}
Ленивая загрузка компонентов
Ангуляр обеспечивает ряд возможностей по оптимизации работы вашего проекта. Одна из таких особенностей – это ленивая загрузка модулей, что позволяет подгружать различные модули с определенным функционалом по мере необходимости, а не сразу при старте приложения. Это позволяет сократить размер изначально загружаемого кода, уменьшить нагрузку на клиенте и т.д. Об этой возможности Ангуляр можно найти довольно много материалов и ознакомиться с ними подробнее.
Однако мы можем пойти дальше, и загружать лениво не только целые модули, но и отдельные компоненты на странице.
Давайте посмотрим, как это можно сделать, используя уже изученные методы из первой части статьи.
Создадим компонент BarComponent
@Component({ template: ` <h1 (click)="titleChanges.emit('changed')">{{ title }}</h1> ` }) export class BarComponent implements OnInit { title = 'Default'; titleChanges = new EventEmitter();
Тут присутствует свойство title, а также свойство titleChanges, на которое можно подписаться и принимать исходящие из компонента события.
Теперь давайте посмотрим, как лениво загрузить этот компонент, динамически его создать, и получить доступ к его свойствам.
@Component({ template: ` <button (click)="loadBar()">Load BarComponent</button> <ng-template #vcr></ng-template> ` }) export class MyComponent { @ViewChild('vcr', { read: ViewContainerRef }) vcr: ViewContainerRef; barRef: ComponentRef<BarComponent>; constructor(private resolver: ComponentFactoryResolver) {} async loadBar() { if (!this.barRef) { const { BarComponent } = await import(`./bar/bar.component`); const factory = this.resolver.resolveComponentFactory(BarComponent); this.barRef = this.vcr.createComponent(factory); this.barRef.instance.title = 'Changed'; // Don't forget to unsubscribe this.barRef.instance.titleChanges.subscribe(console.log); } } }
Сперва, как и в первой части статьи, получим ссылку на контейнер через ViewChild. После этого нужно загрузить сам компонент BarComponent. Синтаксис этой загрузки очень похож на синтаксис, используемый при ленивой загрузке модулей.
const { BarComponent } = await import(`./bar/bar.component`);
Затем инжеткируем сервис ComponentFactoryResolver, чтобы пользоваться его методами для создания компонента.
Дальнейшая последовательность действий аналогична динамическому созданию компонента.
После выполнения всех действий, у нас есть доступ к компоненту и его свойствам. Тут нужно обратить внимание, что мы не используем декораторы Input и Output, потому что мы общаемся с компонентом не через шаблон, а напрямую взаимодействуем с экземпляром компонента прямо в коде.
Однако если посмотреть на созданный компонент, можно сделать вывод что он довольно бесполезный и пригодится только чтобы отображать данные из входных свойств. Для полноты картины, было бы здорово обеспечить возможность использования остальные возможности Ангуляра в нем, например: другие компоненты, директивы, пайпы. Для решения этой проблемы мы создадим BarModule, и добавим туда все необходимое. (В последней версии Ангуляр, а на данный момент — это 16 версия, появилась возможность использовать standalone компоненты и напрямую подключать в него все используемые компоненты, модули, директивы и т.д.)
@NgModule({ imports: [ReactiveFormsModule], declarations: [FooComponent] }) class BarModule { }
Самое интересное, что благодаря механизму tree-shaking, если единственное место, где мы будем использовать модуль ReactiveFormsModule — это наш компонент BarComponent, то исходный код реактивных форм загрузится только при загрузке BarComponent. Это открывает новые возможности в вопросах оптимизации приложения.
Еще одно...
Вышеописанный синтаксис отлично подойдет для использования в проектах, где версия Ангуляр 12 и ниже. Однако начиная с 13 версии класс ComponentFactoryResolver был отмечен как deprecated, и это повлияло на процесс создания динамических компонентов, так как теперь из последовательности действий ушел один шаг, а именно создание фабрики компонента.
const factory = this.resolver.resolveComponentFactory(BarComponent);
Теперь же для создания компонента можно напрямую обращаться к методу createComponent и передавать класс создаваемого компонента в качестве параметра, на выходе получится тот же результат.
Итоги
Динамическое создание компонентов открывает большие возможности по взаимодействию с ними и их управлению. Помимо всего этого в шаблонах компонента остается гораздо меньше кода с описанием всех input и output свойств, что так же хорошо влияет на весь проект. Ну и самое главное, это вопросы оптимизации. Благодаря более тонкой работе с компонентами, их динамическому созданию и ленивой загрузке открываются всё большие возможности по оптимизации приложения и улучшению пользовательского опыта при работе с ним. Данная статья не раскрывает собой все вопросы по данной теме, поэтому я советую вам более подробно ознакомиться со всеми материалами, которые существуют по тематике динамических компонентов.
Динамическое создание компонентов мощный инструмент, способный дать вашему проекту новые преимущества. В свою очередь вы как разработчик, изучив досконально этот вопрос, добавите себе в копилку ценные и важные навыки.
Буду рад обратной связи в карму или в комментариях.
