РЕДАКТИРОВАТЬ - связано с 2.3.0 (2016-12-07)
ПРИМЕЧАНИЕ: чтобы получить решение для предыдущей версии, проверьте историю этого поста
Подобная тема обсуждается здесь Эквивалент $ compile в Angular 2 . Нам нужно использовать JitCompiler
и NgModule
. Узнайте больше о NgModule
в Angular2 здесь:
В двух словах
Есть рабочий плункер / пример (динамический шаблон, динамический тип компонента, динамический модуль JitCompiler
, ... в действии)
Принцип:
1) создать шаблон
2) найти ComponentFactory
в кеше - перейти к 7)
3) - создать Component
4) - создать Module
5) - скомпилировать Module
6) - вернуть (и кэш для дальнейшего использования) ComponentFactory
7) использовать Target и ComponentFactory
создать экземпляр динамическогоComponent
Вот фрагмент кода (более подробно здесь ) - наш пользовательский Builder возвращает только что построенный / кэшированныйComponentFactory
и заполнитель представления Target, потребляемый для создания экземпляраDynamicComponent
// here we get a TEMPLATE with dynamic content === TODO
var template = this.templateBuilder.prepareTemplate(this.entity, useTextarea);
// here we get Factory (just compiled or from cache)
this.typeBuilder
.createComponentFactory(template)
.then((factory: ComponentFactory<IHaveDynamicData>) =>
{
// Target will instantiate and inject component (we'll keep reference to it)
this.componentRef = this
.dynamicComponentTarget
.createComponent(factory);
// let's inject @Inputs to component instance
let component = this.componentRef.instance;
component.entity = this.entity;
//...
});
Вот и все - в двух словах. Чтобы получить более подробную информацию .. читайте ниже
,
TL & DR
Наблюдайте за поршнем и возвращайтесь, чтобы прочитать подробности, если какой-то фрагмент требует более подробного объяснения.
,
Подробное объяснение - Angular2 RC6 ++ & компоненты среды выполнения
Ниже описания этого сценария мы будем
- создать модуль
PartsModule:NgModule
(держатель мелких кусочков)
- создайте другой модуль
DynamicModule:NgModule
, который будет содержать наш динамический компонент (и ссылаться PartsModule
динамически)
- создать динамический шаблон (простой подход)
- создать новый
Component
тип (только если шаблон изменился)
- создать новый
RuntimeModule:NgModule
. Этот модуль будет содержать ранее созданныйComponent
тип
- вызов
JitCompiler.compileModuleAndAllComponentsAsync(runtimeModule)
чтобы получитьComponentFactory
- создать экземпляр
DynamicComponent
- задания заполнителя View Target иComponentFactory
- назначить
@Inputs
на новый экземпляр (переключатель от INPUT
до TEXTAREA
редактирования) , потребляют@Outputs
NgModule
Нам нужны NgModule
с.
Хотя я хотел бы показать очень простой пример, в этом случае мне потребуется три модуля (на самом деле 4 - но я не считаю AppModule) . Пожалуйста, возьмите это, а не простой фрагмент, как основу для действительно надежного генератора динамических компонентов.
Там будет один модуль для всех небольших компонентов, например string-editor
, text-editor
( date-editor
, number-editor
...)
@NgModule({
imports: [
CommonModule,
FormsModule
],
declarations: [
DYNAMIC_DIRECTIVES
],
exports: [
DYNAMIC_DIRECTIVES,
CommonModule,
FormsModule
]
})
export class PartsModule { }
Где DYNAMIC_DIRECTIVES
расширяемые и предназначены для хранения всех мелких деталей, используемых для нашего шаблона / типа динамического компонента. Проверьте приложение / parts / parts.module.ts
Вторым будет модуль для нашей динамической обработки вещей. Он будет содержать компоненты хостинга и некоторые провайдеры .. которые будут одиночными. Для этого мы опубликуем их стандартным способом - сforRoot()
import { DynamicDetail } from './detail.view';
import { DynamicTypeBuilder } from './type.builder';
import { DynamicTemplateBuilder } from './template.builder';
@NgModule({
imports: [ PartsModule ],
declarations: [ DynamicDetail ],
exports: [ DynamicDetail],
})
export class DynamicModule {
static forRoot()
{
return {
ngModule: DynamicModule,
providers: [ // singletons accross the whole app
DynamicTemplateBuilder,
DynamicTypeBuilder
],
};
}
}
Проверьте использование forRoot()
вAppModule
Наконец, нам понадобится adhoc, модуль времени выполнения ... но он будет создан позже, как часть DynamicTypeBuilder
работы.
Четвертый модуль, прикладной модуль, является тем, кто объявляет поставщиков компиляторов:
...
import { COMPILER_PROVIDERS } from '@angular/compiler';
import { AppComponent } from './app.component';
import { DynamicModule } from './dynamic/dynamic.module';
@NgModule({
imports: [
BrowserModule,
DynamicModule.forRoot() // singletons
],
declarations: [ AppComponent],
providers: [
COMPILER_PROVIDERS // this is an app singleton declaration
],
Читайте (читайте) гораздо больше о NgModule там:
Шаблон строитель
В нашем примере мы обработаем детали этого вида сущности
entity = {
code: "ABC123",
description: "A description of this Entity"
};
Чтобы создать template
, в этом плункере мы используем этот простой / наивный строитель.
Настоящее решение, настоящий конструктор шаблонов - это место, где ваше приложение может многое сделать
// plunker - app/dynamic/template.builder.ts
import {Injectable} from "@angular/core";
@Injectable()
export class DynamicTemplateBuilder {
public prepareTemplate(entity: any, useTextarea: boolean){
let properties = Object.keys(entity);
let template = "<form >";
let editorName = useTextarea
? "text-editor"
: "string-editor";
properties.forEach((propertyName) =>{
template += `
<${editorName}
[propertyName]="'${propertyName}'"
[entity]="entity"
></${editorName}>`;
});
return template + "</form>";
}
}
Хитрость здесь в том, что он создает шаблон, который использует некоторый набор известных свойств, например entity
. Такое свойство (-ies) должно быть частью динамического компонента, который мы создадим следующим.
Чтобы сделать это немного проще, мы можем использовать интерфейс для определения свойств, которые может использовать наш конструктор шаблонов. Это будет реализовано нашим динамическим типом компонента.
export interface IHaveDynamicData {
public entity: any;
...
}
ComponentFactory
строитель
Здесь важно помнить:
наш тип компонента, созданный вместе с нашим DynamicTypeBuilder
, может отличаться - но только его шаблоном (созданным выше) . Свойства компонентов (входы, выходы или некоторые защищенные) остаются неизменными. Если нам нужны разные свойства, мы должны определить разные комбинации Template и Type Builder
Итак, мы касаемся сути нашего решения. Строитель будет 1) создавать ComponentType
2) создавать NgModule
3) компилировать ComponentFactory
4) кэшировать его для последующего повторного использования.
Зависимость, которую мы должны получить:
// plunker - app/dynamic/type.builder.ts
import { JitCompiler } from '@angular/compiler';
@Injectable()
export class DynamicTypeBuilder {
// wee need Dynamic component builder
constructor(
protected compiler: JitCompiler
) {}
И вот фрагмент, как получить ComponentFactory
:
// plunker - app/dynamic/type.builder.ts
// this object is singleton - so we can use this as a cache
private _cacheOfFactories:
{[templateKey: string]: ComponentFactory<IHaveDynamicData>} = {};
public createComponentFactory(template: string)
: Promise<ComponentFactory<IHaveDynamicData>> {
let factory = this._cacheOfFactories[template];
if (factory) {
console.log("Module and Type are returned from cache")
return new Promise((resolve) => {
resolve(factory);
});
}
// unknown template ... let's create a Type for it
let type = this.createNewComponent(template);
let module = this.createComponentModule(type);
return new Promise((resolve) => {
this.compiler
.compileModuleAndAllComponentsAsync(module)
.then((moduleWithFactories) =>
{
factory = _.find(moduleWithFactories.componentFactories
, { componentType: type });
this._cacheOfFactories[template] = factory;
resolve(factory);
});
});
}
Выше мы создаем и кешируем как Component
и Module
. Потому что, если шаблон (фактически настоящая динамическая часть всего этого) один и тот же .. мы можем использовать повторно
И вот два метода, которые представляют действительно крутой способ создания декорированных классов / типов во время выполнения. Не только, @Component
но и@NgModule
protected createNewComponent (tmpl:string) {
@Component({
selector: 'dynamic-component',
template: tmpl,
})
class CustomDynamicComponent implements IHaveDynamicData {
@Input() public entity: any;
};
// a component for this particular template
return CustomDynamicComponent;
}
protected createComponentModule (componentType: any) {
@NgModule({
imports: [
PartsModule, // there are 'text-editor', 'string-editor'...
],
declarations: [
componentType
],
})
class RuntimeComponentModule
{
}
// a module for just this Type
return RuntimeComponentModule;
}
Важный:
динамические типы наших компонентов различаются, но только по шаблонам. Поэтому мы используем этот факт для их кеширования . Это действительно очень важно. Angular2 также будет кешировать это .. по типу . И если мы воссоздаем для того же шаблона строки новых типов ... мы начнем генерировать утечки памяти.
ComponentFactory
используется компонентом хостинга
Последняя часть - это компонент, который содержит цель для нашего динамического компонента, например <div #dynamicContentPlaceHolder></div>
. Мы получаем ссылку на него и используем ComponentFactory
для создания компонента. Это в двух словах, и вот все части этого компонента (если нужно, откройте плункер здесь )
Давайте сначала подведем итоги операторов импорта:
import {Component, ComponentRef,ViewChild,ViewContainerRef} from '@angular/core';
import {AfterViewInit,OnInit,OnDestroy,OnChanges,SimpleChange} from '@angular/core';
import { IHaveDynamicData, DynamicTypeBuilder } from './type.builder';
import { DynamicTemplateBuilder } from './template.builder';
@Component({
selector: 'dynamic-detail',
template: `
<div>
check/uncheck to use INPUT vs TEXTAREA:
<input type="checkbox" #val (click)="refreshContent(val.checked)" /><hr />
<div #dynamicContentPlaceHolder></div> <hr />
entity: <pre>{{entity | json}}</pre>
</div>
`,
})
export class DynamicDetail implements AfterViewInit, OnChanges, OnDestroy, OnInit
{
// wee need Dynamic component builder
constructor(
protected typeBuilder: DynamicTypeBuilder,
protected templateBuilder: DynamicTemplateBuilder
) {}
...
Мы просто получаем, шаблонов и компонентов сборщиков. Далее идут свойства, которые нужны для нашего примера (подробнее в комментариях)
// reference for a <div> with #dynamicContentPlaceHolder
@ViewChild('dynamicContentPlaceHolder', {read: ViewContainerRef})
protected dynamicComponentTarget: ViewContainerRef;
// this will be reference to dynamic content - to be able to destroy it
protected componentRef: ComponentRef<IHaveDynamicData>;
// until ngAfterViewInit, we cannot start (firstly) to process dynamic stuff
protected wasViewInitialized = false;
// example entity ... to be recieved from other app parts
// this is kind of candiate for @Input
protected entity = {
code: "ABC123",
description: "A description of this Entity"
};
В этом простом сценарии у нашего хостинг-компонента их нет @Input
. Так что не надо реагировать на изменения. Но, несмотря на этот факт (и чтобы быть готовым к предстоящим изменениям) - нам нужно ввести некоторый флаг, если компонент уже (во-первых) инициирован. И только тогда мы можем начать магию.
Наконец, мы будем использовать наш компоновщик компонентов, и он только что скомпилирован / кэширован ComponentFacotry
. Нашему целевому заполнителю будет предложено создать экземпляр дляComponent
этой фабрики.
protected refreshContent(useTextarea: boolean = false){
if (this.componentRef) {
this.componentRef.destroy();
}
// here we get a TEMPLATE with dynamic content === TODO
var template = this.templateBuilder.prepareTemplate(this.entity, useTextarea);
// here we get Factory (just compiled or from cache)
this.typeBuilder
.createComponentFactory(template)
.then((factory: ComponentFactory<IHaveDynamicData>) =>
{
// Target will instantiate and inject component (we'll keep reference to it)
this.componentRef = this
.dynamicComponentTarget
.createComponent(factory);
// let's inject @Inputs to component instance
let component = this.componentRef.instance;
component.entity = this.entity;
//...
});
}
небольшое расширение
Кроме того, нам нужно сохранить ссылку на скомпилированный шаблон .. чтобы иметь возможность корректно использовать destroy()
его всякий раз, когда мы его изменим.
// this is the best moment where to start to process dynamic stuff
public ngAfterViewInit(): void
{
this.wasViewInitialized = true;
this.refreshContent();
}
// wasViewInitialized is an IMPORTANT switch
// when this component would have its own changing @Input()
// - then we have to wait till view is intialized - first OnChange is too soon
public ngOnChanges(changes: {[key: string]: SimpleChange}): void
{
if (this.wasViewInitialized) {
return;
}
this.refreshContent();
}
public ngOnDestroy(){
if (this.componentRef) {
this.componentRef.destroy();
this.componentRef = null;
}
}
сделано
Это в значительной степени это. Не забудьте уничтожить все, что было построено динамически (ngOnDestroy) . Также обязательно кешируйте динамически types
и, modules
если единственное отличие, это их шаблон.
Проверьте все это в действии здесь
чтобы увидеть предыдущие версии (например, связанные с RC5) этого поста, проверьте историю