Динамическое создание компонентов Angular на лету
В этой статье мы поговорим о создании компонентов динамически, шаг за шагом пройдем этот путь. Помимо простого создания компонентов, мы поговорим о более продвинутых вещах, которые можно сделать в рамках этого процесса.
Создание динамических компонентов
В первую очередь нам нужен сам компонент, который мы будем динамически создавать.
@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 свойств, что так же хорошо влияет на весь проект. Ну и самое главное, это вопросы оптимизации. Благодаря более тонкой работе с компонентами, их динамическому созданию и ленивой загрузке открываются всё большие возможности по оптимизации приложения и улучшению пользовательского опыта при работе с ним. Данная статья не раскрывает собой все вопросы по данной теме, поэтому я советую вам более подробно ознакомиться со всеми материалами, которые существуют по тематике динамических компонентов.
Динамическое создание компонентов мощный инструмент, способный дать вашему проекту новые преимущества. В свою очередь вы как разработчик, изучив досконально этот вопрос, добавите себе в копилку ценные и важные навыки.
Буду рад обратной связи в карму или в комментариях.