Angular 2. Динамическое создание компонентов

Продолжается мое знакомство с Angular 2.

Эта заметка — не оригинальное решение, а просто некое memories по найденному на просторах сети.

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

Допустим есть какой-то контейнер и, при определенных условиях, берется некоторая конфигурация, из которой можно взять строку — имя класса компонента, который нужно создать и добавить в этот контейнер.

В результате поисков найдено следующее решение. За основу для демонстрации взят репозиторий для быстрого старта agnular quickstart.

Файлы и классы:

  • app.module.ts — тут собственно главный модуль, в который внесу небольшие изменения
  • app.component.ts — компонент, в шаблон которого я вставлю селектор контейнера
  • container.component.ts — собственно контейнер
  • inner.component.ts — тестовый компонент, селектора которого нет в шаблонах, будет создаваться динамически.

app.module.ts:


import {NgModule}      from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';

import {AppComponent}  from './app.component';
import {Inner} from './inner.component';
import {ContainerComponent} from './container.component';

@NgModule({
    imports:         [BrowserModule],
    declarations:    [AppComponent, Inner, ContainerComponent],
    bootstrap:       [AppComponent],
    entryComponents: [Inner]
})
export class AppModule {
}

В этом классе, кроме добавления классов в раздел declarations, динамически создаваемый компонент добавлен в entryComponents. Для каждого из компонентов в этой секции создаются ComponentFactory и сохраняются в ComponentFactoryResolver.

app.component.ts:


import {Component} from '@angular/core';

@Component({
    selector: 'my-app',
    template: `<h1> Hello {{name}}</h1><div><container-component></container-component></div>`,
})
export class AppComponent {
    name = 'Angular';
}

Тут просто я в шаблон добавил тег компонента-контейнера

inner.component.ts:


import {Component} from '@angular/core';

@Component({
    selector: 'innerOne',
    template: `<div><h1>Inner One</h1></div>`
})
export class Inner {

}

Практически пустой компонент для демонстрации динамического создания.

container.component.ts:


import {Component, ComponentRef, ViewChild, ViewContainerRef, ComponentFactoryResolver, Type, OnInit, OnDestroy} from '@angular/core';

@Component({
    selector: 'container-component',
    template: `<div class="container">
    <div #componentsContainer></div>
</div>`
})
export class ContainerComponent implements OnInit {


    @ViewChild('componentsContainer', {read: ViewContainerRef}) componentsContainer: ViewContainerRef;

    private innerRef: ComponentRef;
    constructor(private resolver: ComponentFactoryResolver){
    }

    ngOnInit(): void {
        let factories = Array.from(this.resolver['_factories'].keys());
        let factoryClass = <Type<any>> factories.find((factory: any) => factory.name === 'Inner');
        let innerComponentFactory = this.resolver.resolveComponentFactory(factoryClass);
        this.innerRef = this.componentsContainer.createComponent(innerComponentFactory);
    }

    ngOnDestroy(): void {
    if(this.innerRef){
        this.innerRef.destroy();
    }
    }

}

Итак, в шаблоне у меня есть элемент с переменной componentsContainer — именно сюда и будут добавляться новые компоненты.

В классе объявлена переменная с ViewChild декоратором и, указан параметр read для того, чтобы вернуть результат не ElementRef типа, а ViewContainerRef. Потому что именно ViewContainerRef имеет нужный мне метод создания компонента.

Через конструктор класса инжектится ComponentFactoryResolver в переменную resolver.
Дальше для примера я использую этап инициализации, но можно все реализовать в другом кастомном методе (например, по подписке на запрос к серверу) главное, чтобы уже была доступна переменная из шаблона для получения ссылки ViewContainerRef.

Ок. Теперь дальше — что же в ngOnInit.

В resolver есть _factories Map с ComponentFactory компонентов из entryComponents. А именно там есть ComponentFactory создаваемого компонента, в данном случае — Inner.

Интересный момент — там есть еще фабрика для AppComponent – это первый основной компонент приложения и для него фабрика создается по-умолчанию и используется при бутстрапе приложения.

Дальше — получаем ключи карты и преобразуем их в массив. Поиск по имени нужного класса создаваемого компонента и приведение к Type<any>.

Type если посмотреть в исходниках это то же самое, что и Function. В typescript класс, в javascript — функция-конструктор.

Type нужен для получения уже конкретной ComponentFactory создаваемого компонента. Через метод resolveComponentFactory ее и получаю.
Все, теперь собственно последнее — создание компонента.

Замечание: необходимо при destroy этапе (ngOnDestroy) удалить и ссылку на созданный компонент.

4 Comments
  1. Barss
    • shrewmus
  2. Paul
    • shrewmus

Leave a Reply

Your email address will not be published. Required fields are marked *