Как я могу использовать / создать динамический шаблон для компиляции динамического компонента с Angular 2.0?

Я хочу динамически создавать шаблоны. Это должно быть использовано для создания ComponentType во время выполнения и поместите (даже замените) его где-нибудь внутри компонента размещения.

До RC4 я пользовался ComponentResolver, но с RC5 я получаю сообщение:

ComponentResolver не рекомендуется для динамической компиляции. использование ComponentFactoryResolver вместе с @NgModule/@Component.entryComponents или вместо ANALYZE_FOR_ENTRY_COMPONENTS провайдера. Только для времени компиляции, вы также можете использовать Compiler.compileComponentSync/Async,

Я нашел этот (официальный angular2) документ

Angular 2 Синхронное Динамическое Создание Компонентов

И понимаю, что я могу использовать либо

  • Вид динамического ngIf с ComponentFactoryResolver, Если я передам известные компоненты в хостинг один внутри @Component({entryComponents: [comp1, comp2], ...}) - Я могу использовать .resolveComponentFactory(componentToRender);
  • Реальная компиляция во время выполнения, с Compiler...

Но вопрос в том, как это использовать Compiler? Примечание выше говорит, что я должен позвонить: Compiler.compileComponentSync/Async - так как?

Например. Я хочу создать (основываясь на некоторых условиях конфигурации) этот вид шаблона для одного вида настроек

<form>
   <string-editor
     [propertyName]="'code'"
     [entity]="entity"
   ></string-editor>
   <string-editor
     [propertyName]="'description'"
     [entity]="entity"
   ></string-editor>
   ...

а в другом случае этот ( string-editor заменяется на text-editor )

<form>
   <text-editor
     [propertyName]="'code'"
     [entity]="entity"
   ></text-editor>
   ...

И так далее (другой номер / дата / ссылка editors по типам свойств, пропущены некоторые свойства для некоторых пользователей...). Т.е. это пример, реальная конфигурация может генерировать гораздо больше разных и сложных шаблонов.

Шаблон меняется, поэтому я не могу использовать ComponentFactoryResolver и передать существующие... Мне нужно решение с Compiler


AOT и JitCompiler (бывший RuntimeCompiler)

Хотели бы вы использовать эту функцию с AOT (заблаговременная компиляция)? Вы получаете:

Ошибка: обнаружена ошибка при статическом разрешении значений символов. Вызовы функций не поддерживаются. Попробуйте заменить функцию или лямбду ссылкой на экспортированную функцию (позиция 65:17 в исходном файле.ts), разрешив символ COMPILER_PROVIDERS в.../node_modules/@angular/compiler/src/compiler.d.ts,

Пожалуйста, оставьте свой комментарий, проголосуйте здесь:

Может ли / будет / будет ли код с использованием COMPILER_PROVIDERS поддерживаться AOT?

16 ответов

Решение

РЕДАКТИРОВАТЬ - связано с 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) использовать цель и ComponentFactory создать экземпляр динамического Component

Вот фрагмент кода (более подробно здесь) - наш пользовательский Builder возвращает только что построенный / кэшированный ComponentFactory и представление Целевой заполнитель представления потребляют, чтобы создать экземпляр 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++ и компоненты времени выполнения

Ниже описания этого сценария мы будем

  1. создать модуль PartsModule:NgModule (держатель мелких кусочков)
  2. создать еще один модуль DynamicModule:NgModule, который будет содержать наш динамический компонент (и ссылку PartsModule динамически)
  3. создать динамический шаблон (простой подход)
  4. создать новый Component тип (только если шаблон был изменен)
  5. создать новый RuntimeModule:NgModule, Этот модуль будет содержать ранее созданный Component тип
  6. вызов JitCompiler.compileModuleAndAllComponentsAsync(runtimeModule) получить ComponentFactory
  7. создать экземпляр DynamicComponent - работа заполнителя View Target и ComponentFactory
  8. назначать @Inputs на новый экземпляр (переключиться с INPUT в TEXTAREA редактирование), потреблять @Outputs

NgModule

Нам нужен NgModule s.

Хотя я хотел бы показать очень простой пример, в этом случае мне потребуется три модуля (на самом деле 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) этого поста, проверьте историю

РЕДАКТИРОВАТЬ (26/08/2017): Приведенное ниже решение хорошо работает с Angular2 и 4. Я обновил его, добавив переменную шаблона и обработчик кликов, и протестировал его с Angular 4.3.
Для Angular4, ngComponentOutlet, как описано в ответе Офира, является гораздо лучшим решением. Но сейчас он еще не поддерживает входы и выходы. Если [этот PR] ( https://github.com/angular/angular/pull/15362] будет принят, это будет возможно через экземпляр компонента, возвращаемый событием create.
https://github.com/gund/ng-dynamic-component может быть лучшим и самым простым решением в целом, но я еще не проверял это.

Ответ @Long Field на месте! Вот еще один (синхронный) пример:

import {Compiler, Component, NgModule, OnInit, ViewChild,
  ViewContainerRef} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'

@Component({
  selector: 'my-app',
  template: `<h1>Dynamic template:</h1>
             <div #container></div>`
})
export class App implements OnInit {
  @ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef;

  constructor(private compiler: Compiler) {}

  ngOnInit() {
    this.addComponent(
      `<h4 (click)="increaseCounter()">
        Click to increase: {{counter}}
      `enter code here` </h4>`,
      {
        counter: 1,
        increaseCounter: function () {
          this.counter++;
        }
      }
    );
  }

  private addComponent(template: string, properties?: any = {}) {
    @Component({template})
    class TemplateComponent {}

    @NgModule({declarations: [TemplateComponent]})
    class TemplateModule {}

    const mod = this.compiler.compileModuleAndAllComponentsSync(TemplateModule);
    const factory = mod.componentFactories.find((comp) =>
      comp.componentType === TemplateComponent
    );
    const component = this.container.createComponent(factory);
    Object.assign(component.instance, properties);
    // If properties are changed at a later stage, the change detection
    // may need to be triggered manually:
    // component.changeDetectorRef.detectChanges();
  }
}

@NgModule({
  imports: [ BrowserModule ],
  declarations: [ App ],
  bootstrap: [ App ]
})
export class AppModule {}

Жить по адресу http://plnkr.co/edit/fdP9Oc.

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

То, что я в итоге делал, использует Angular 4.0.0-beta.6 ngComponentOutlet.

Это дало мне самое короткое и простое решение, записанное в файле динамического компонента.

  • Вот простой пример, который просто получает текст и помещает его в шаблон, но, очевидно, вы можете изменить его в соответствии с вашими потребностями:
import {
  Component, OnInit, Input, NgModule, NgModuleFactory, Compiler
} from '@angular/core';

@Component({
  selector: 'my-component',
  template: `<ng-container *ngComponentOutlet="dynamicComponent;
                            ngModuleFactory: dynamicModule;"></ng-container>`,
  styleUrls: ['my.component.css']
})
export class MyComponent implements OnInit {
  dynamicComponent;
  dynamicModule: NgModuleFactory<any>;

  @Input()
  text: string;

  constructor(private compiler: Compiler) {
  }

  ngOnInit() {
    this.dynamicComponent = this.createNewComponent(this.text);
    this.dynamicModule = this.compiler.compileModuleSync(this.createComponentModule(this.dynamicComponent));
  }

  protected createComponentModule (componentType: any) {
    @NgModule({
      imports: [],
      declarations: [
        componentType
      ],
      entryComponents: [componentType]
    })
    class RuntimeComponentModule
    {
    }
    // a module for just this Type
    return RuntimeComponentModule;
  }

  protected createNewComponent (text:string) {
    let template = `dynamically created template with text: ${text}`;

    @Component({
      selector: 'dynamic-component',
      template: template
    })
    class DynamicComponent implements OnInit{
       text: any;

       ngOnInit() {
       this.text = text;
       }
    }
    return DynamicComponent;
  }
}
  • Краткое объяснение:
    1. my-component - компонент, в котором отображается динамический компонент
    2. DynamicComponent - компонент, который будет динамически построен и он рендерится внутри my-component

Не забудьте обновить все угловые библиотеки до ^Angular 4.0.0

Надеюсь это поможет. Удачи!

ОБНОВИТЬ

Также работает для угловых 5.

2019 февраль ответ

Отличные новости! Похоже, что пакет @ angular / cdk теперь имеет первоклассную поддержку порталов!

1) В вашем модуле:

@NgModule({
  imports:      [ ..., PortalModule, ... ],
  entryComponents: [ChildComponent]
})
export class AppModule { }

2) В вашем компоненте:

import { Component, OnInit } from '@angular/core';
import { ComponentPortal } from '@angular/cdk/portal';

@Component({
  selector: 'my-app',
  template: `
    <h3>Portal Example</h3>
    <button (click)="onClickAddChild()">Click to add component dynamically</button>
    <ng-template [cdkPortalOutlet]="myPortal"></ng-template>
  `
})
export class AppComponent  {
  myPortal;
  onClickAddChild() {
    this.myPortal = new ComponentPortal(ChildComponent);
  }
}


@Component({
  selector: 'my-child',
  template: `<p>I am a dynamic component!</p>`
})
export class ChildComponent  {
}

Stackblitz

Я решил сжать все, что я узнал, в один файл. Здесь есть что взять, особенно по сравнению с до RC5. Обратите внимание, что этот исходный файл включает в себя AppModule и AppComponent.

import {
  Component, Input, ReflectiveInjector, ViewContainerRef, Compiler, NgModule, ModuleWithComponentFactories,
  OnInit, ViewChild
} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';

@Component({
  selector: 'app-dynamic',
  template: '<h4>Dynamic Components</h4><br>'
})
export class DynamicComponentRenderer implements OnInit {

  factory: ModuleWithComponentFactories<DynamicModule>;

  constructor(private vcRef: ViewContainerRef, private compiler: Compiler) { }

  ngOnInit() {
    if (!this.factory) {
      const dynamicComponents = {
        sayName1: {comp: SayNameComponent, inputs: {name: 'Andrew Wiles'}},
        sayAge1: {comp: SayAgeComponent, inputs: {age: 30}},
        sayName2: {comp: SayNameComponent, inputs: {name: 'Richard Taylor'}},
        sayAge2: {comp: SayAgeComponent, inputs: {age: 25}}};
      this.compiler.compileModuleAndAllComponentsAsync(DynamicModule)
        .then((moduleWithComponentFactories: ModuleWithComponentFactories<DynamicModule>) => {
          this.factory = moduleWithComponentFactories;
          Object.keys(dynamicComponents).forEach(k => {
            this.add(dynamicComponents[k]);
          })
        });
    }
  }

  addNewName(value: string) {
    this.add({comp: SayNameComponent, inputs: {name: value}})
  }

  addNewAge(value: number) {
    this.add({comp: SayAgeComponent, inputs: {age: value}})
  }

  add(comp: any) {
    const compFactory = this.factory.componentFactories.find(x => x.componentType === comp.comp);
    // If we don't want to hold a reference to the component type, we can also say: const compFactory = this.factory.componentFactories.find(x => x.selector === 'my-component-selector');
    const injector = ReflectiveInjector.fromResolvedProviders([], this.vcRef.parentInjector);
    const cmpRef = this.vcRef.createComponent(compFactory, this.vcRef.length, injector, []);
    Object.keys(comp.inputs).forEach(i => cmpRef.instance[i] = comp.inputs[i]);
  }
}

@Component({
  selector: 'app-age',
  template: '<div>My age is {{age}}!</div>'
})
class SayAgeComponent {
  @Input() public age: number;
};

@Component({
  selector: 'app-name',
  template: '<div>My name is {{name}}!</div>'
})
class SayNameComponent {
  @Input() public name: string;
};

@NgModule({
  imports: [BrowserModule],
  declarations: [SayAgeComponent, SayNameComponent]
})
class DynamicModule {}

@Component({
  selector: 'app-root',
  template: `
        <h3>{{message}}</h3>
        <app-dynamic #ad></app-dynamic>
        <br>
        <input #name type="text" placeholder="name">
        <button (click)="ad.addNewName(name.value)">Add Name</button>
        <br>
        <input #age type="number" placeholder="age">
        <button (click)="ad.addNewAge(age.value)">Add Age</button>
    `,
})
export class AppComponent {
  message = 'this is app component';
  @ViewChild(DynamicComponentRenderer) dcr;

}

@NgModule({
  imports: [BrowserModule],
  declarations: [AppComponent, DynamicComponentRenderer],
  bootstrap: [AppComponent]
})
export class AppModule {}`

У меня есть простой пример, чтобы показать, как сделать угловой компонент 2 RC6.

Скажем, у вас есть динамический шаблон HTML = template1 и вы хотите динамическую загрузку, сначала оберните в компонент

@Component({template: template1})
class DynamicComponent {}

здесь template1 как html, может содержать компонент ng2

Начиная с rc6, @NgModule должен обернуть этот компонент. @NgModule, как и модуль в anglarJS 1, он разделяет другую часть приложения ng2, поэтому:

@Component({
  template: template1,

})
class DynamicComponent {

}
@NgModule({
  imports: [BrowserModule,RouterModule],
  declarations: [DynamicComponent]
})
class DynamicModule { }

(Здесь импортируйте RouterModule, так как в моем примере есть некоторые компоненты маршрута в моем html, как вы увидите позже)

Теперь вы можете скомпилировать DynamicModule как: this.compiler.compileModuleAndAllComponentsAsync(DynamicModule).then( factory => factory.componentFactories.find(x => x.componentType === DynamicComponent))

И нам нужно положить выше в app.moudule.ts, чтобы загрузить его, пожалуйста, смотрите мой app.moudle.ts. Для получения дополнительной и полной информации проверьте: https://github.com/Longfld/DynamicalRouter/blob/master/app/MyRouterLink.ts и app.moudle.ts

и посмотрите демо: http://plnkr.co/edit/1fdAYP5PAbiHdJfTKgWo?p=preview

В Angular 7.x я использовал Angular-элементы для этого.

  1. Установите @ angular-elements npm i @ angular / elements -s

  2. Создать вспомогательный сервис.

import { Injectable, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';
import { IStringAnyMap } from 'src/app/core/models';
import { AppUserIconComponent } from 'src/app/shared';

const COMPONENTS = {
  'user-icon': AppUserIconComponent
};

@Injectable({
  providedIn: 'root'
})
export class DynamicComponentsService {
  constructor(private injector: Injector) {

  }

  public register(): void {
    Object.entries(COMPONENTS).forEach(([key, component]: [string, any]) => {
      const CustomElement = createCustomElement(component, { injector: this.injector });
      customElements.define(key, CustomElement);
    });
  }

  public create(tagName: string, data: IStringAnyMap = {}): HTMLElement {
    const customEl = document.createElement(tagName);

    Object.entries(data).forEach(([key, value]: [string, any]) => {
      customEl[key] = value;
    });

    return customEl;
  }
}

Обратите внимание, что ваш пользовательский тег элемента должен отличаться от углового селектора компонента. в AppUserIconComponent:

...
selector: app-user-icon
...

и в этом случае имя пользовательского тега я использовал "значок пользователя".

  1. Затем вы должны позвонить зарегистрироваться в AppComponent:
@Component({
  selector: 'app-root',
  template: '<router-outlet></router-outlet>'
})
export class AppComponent {
  constructor(   
    dynamicComponents: DynamicComponentsService,
  ) {
    dynamicComponents.register();
  }

}
  1. И теперь в любом месте вашего кода вы можете использовать его так:
dynamicComponents.create('user-icon', {user:{...}});

или вот так:

const html = `<div class="wrapper"><user-icon class="user-icon" user='${JSON.stringify(rec.user)}'></user-icon></div>`;

this.content = this.domSanitizer.bypassSecurityTrustHtml(html);

(в шаблоне):

<div class="comment-item d-flex" [innerHTML]="content"></div>

Обратите внимание, что во втором случае вы должны передать объекты с помощью JSON.stringify и после этого снова проанализировать его. Я не могу найти лучшего решения.

В 2021 году в Angular по-прежнему НЕТ ПУТИ для создания компонента с использованием динамического HTML (динамическая загрузка HTML-шаблона) просто для экономии вашего времени.

Даже есть много проголосовавших решений и принятых решений, но все они не будут работать для последних версий в производстве / AOT, по крайней мере, на данный момент.

В основном потому, что Angular не позволяет вам определять компонент с помощью:template: {variable}

Как заявляет команда Angular, они не собираются поддерживать такой подход !! пожалуйста, найдите это для справки https://github.com/angular/angular/issues/15275

Решено это в Angular 2 Final, просто с помощью директивы dynamicComponent из ng-dynamic.

Использование:

<div *dynamicComponent="template; context: {text: text};"></div>

Где шаблон - это ваш динамический шаблон, а контекст может быть установлен для любой динамической модели данных, с которой вы хотите связать свой шаблон.

В ответ на отличный ответ Radmin, для всех, кто использует angular-cli версии 1.0.0-beta.22 и выше, нужен небольшой твик.

COMPILER_PROVIDERSбольше не может быть импортирован (подробности см. в angular-cli GitHub).

Таким образом, обходной путь там не использовать COMPILER_PROVIDERS а также JitCompiler в providers раздел вообще, но используй JitCompilerFactory вместо '@angular/compiler' вместо этого внутри класса построителя типов:

private compiler: Compiler = new JitCompilerFactory([{useDebug: false, useJit: true}]).createCompiler();

Как видите, он не является инъекционным и поэтому не имеет никаких зависимостей от DI. Это решение также должно работать для проектов, не использующих angular-cli.

Я хочу добавить несколько подробностей к этому прекрасному сообщению от Radim.

Я взял это решение, немного поработал над ним и быстро столкнулся с некоторыми ограничениями. Я просто обрисую их в общих чертах, а затем дам решение.

  • Прежде всего, я не смог отобразить динамические детали внутри динамических деталей (в основном, вкладывая динамические интерфейсы друг в друга).
  • Следующая проблема заключалась в том, что я хотел визуализировать динамические детали внутри одной из частей, которые были доступны в решении. Это было невозможно при первоначальном решении.
  • Наконец, было невозможно использовать URL-адреса шаблонов для динамических частей, таких как strin g-editor.

Я сделал еще один вопрос, основанный на этом посте, о том, как добиться этих ограничений, который можно найти здесь:

рекурсивная динамическая компиляция шаблонов в an gular2

Я просто обрисую ответы на эти ограничения, если вы столкнетесь с той же проблемой, что и я, поскольку это сделает решение более гибким. Было бы здорово, если бы первоначальный плункер тоже обновлялся.

Чтобы включить вложение динамической детализации друг в друга, вам нужно добавить DynamicModule.forRoot() в операторе импорта в type.builder.ts

protected createComponentModule (componentType: any) {
    @NgModule({
    imports: [
        PartsModule, 
        DynamicModule.forRoot() //this line here
    ],
    declarations: [
        componentType
    ],
    })
    class RuntimeComponentModule
    {
    }
    // a module for just this Type
    return RuntimeComponentModule;
}

Кроме того, было невозможно использовать <dynamic-detail> внутри одной из частей, являющихся строковым редактором или текстовым редактором.

Чтобы включить это вам нужно изменить parts.module.ts а также dynamic.module.ts

внутри parts.module.ts Вам нужно будет добавить DynamicDetail в DYNAMIC_DIRECTIVES

export const DYNAMIC_DIRECTIVES = [
   forwardRef(() => StringEditor),
   forwardRef(() => TextEditor),
   DynamicDetail
];

Также в dynamic.module.ts вам придется удалить dynamicDetail, так как они теперь являются частью частей

@NgModule({
   imports:      [ PartsModule ],
   exports:      [ PartsModule],
})

Работающий модифицированный поршень можно найти здесь: http://plnkr.co/edit/UYnQHF?p=preview (я не решил эту проблему, я всего лишь мессенджер:-D)

Наконец, было невозможно использовать шаблоны в деталях, созданных на динамических компонентах. Решением (или обходным путем. Я не уверен, является ли это угловой ошибкой или неправильным использованием фреймворка) было создание компилятора в конструкторе вместо его внедрения.

    private _compiler;

    constructor(protected compiler: RuntimeCompiler) {
        const compilerFactory : CompilerFactory =
        platformBrowserDynamic().injector.get(CompilerFactory);
        this._compiler = compilerFactory.createCompiler([]);
    }

Затем используйте _compiler для компиляции, то templateUrls также включены.

return new Promise((resolve) => {
        this._compiler
            .compileModuleAndAllComponentsAsync(module)
            .then((moduleWithFactories) =>
            {
                let _ = window["_"];
                factory = _.find(moduleWithFactories.componentFactories, { componentType: type });

                this._cacheOfFactories[template] = factory;

                resolve(factory);
            });
    });

Надеюсь, это поможет кому-то еще!

С наилучшими пожеланиями Мортен

Это пример динамических элементов управления формы, сгенерированных с сервера.

https://stackblitz.com/edit/angular-t3mmg6

Этот пример динамических элементов управления Form находится в компоненте add (здесь вы можете получить Form controls с сервера). Если вы видите метод addcomponent, вы можете увидеть элементы управления Forms. В этом примере я не использую угловой материал, но он работает (я использую @ work). Это цель для угловых 6, но работает во всех предыдущих версиях.

Нужно добавить JITComplierFactory для AngularVersion 5 и выше.

Спасибо

Виджай

Я сам пытаюсь понять, как я могу обновить RC4 до RC5, и, таким образом, я наткнулся на эту запись, и новый подход к созданию динамических компонентов все еще остается для меня загадкой, поэтому я не буду предлагать что-либо о распознавателе фабрики компонентов.

Но я могу предложить немного более четкий подход к созданию компонентов в этом сценарии - просто используйте переключатель в шаблоне, который бы создал редактор строк или текстовый редактор в соответствии с некоторыми условиями, например так:

<form [ngSwitch]="useTextarea">
    <string-editor *ngSwitchCase="false" propertyName="'code'" 
                 [entity]="entity"></string-editor>
    <text-editor *ngSwitchCase="true" propertyName="'code'" 
                 [entity]="entity"></text-editor>
</form>

И, кстати, "[" в выражении [prop] имеет значение, это указывает на одностороннюю привязку данных, поэтому вы можете и даже должны опустить их в случае, если вы знаете, что вам не нужно связывать свойство с переменной.

Если все, что вам нужно в качестве способа анализа динамической строки и загрузки компонентов с помощью их селекторов, вы также можете найти полезной библиотекой ngx-dynamic-hooks. Изначально я создал это как часть личного проекта, но не видел ничего подобного, поэтому немного отполировал его и опубликовал.

Некоторые лакомые предложения:

  • Вы можете загрузить любые компоненты в динамическую строку с помощью их селектора (или любого другого шаблона по вашему выбору!)
  • Входы и выходы можно использовать как в обычном шаблоне
  • Компоненты могут быть вложены без ограничений
  • Вы можете передавать живые данные из родительского компонента в динамически загружаемые компоненты (и даже использовать их для привязки входов / выходов)
  • Вы можете контролировать, какие компоненты могут загружаться в каждую розетку и даже какие входы / выходы вы можете им предоставить
  • Библиотека использует встроенный DOMSanitizer Angular, чтобы ее можно было безопасно использовать даже с потенциально небезопасным вводом.

Примечательно, что он не полагается на компилятор времени выполнения, как некоторые другие ответы здесь. Из-за этого вы не можете использовать синтаксис шаблона. С другой стороны, это означает, что он работает как в режимах JiT, так и в AoT, а также как в Ivy, так и в старом шаблонизаторе, а также в целом более безопасен.

Посмотрите на это в действии в этом Stackblitz.

Для этого конкретного случая лучше использовать директиву для динамического создания компонента. Пример:

В HTML, где вы хотите создать компонент

<ng-container dynamicComponentDirective [someConfig]="someConfig"></ng-container>

Я бы подошел и разработал директиву следующим образом.

const components: {[type: string]: Type<YourConfig>} = {
    text : TextEditorComponent,
    numeric: NumericComponent,
    string: StringEditorComponent,
    date: DateComponent,
    ........
    .........
};

@Directive({
    selector: '[dynamicComponentDirective]'
})
export class DynamicComponentDirective implements YourConfig, OnChanges, OnInit {
    @Input() yourConfig: Define your config here //;
    component: ComponentRef<YourConfig>;

    constructor(
        private resolver: ComponentFactoryResolver,
        private container: ViewContainerRef
    ) {}

    ngOnChanges() {
        if (this.component) {
            this.component.instance.config = this.config;
            // config is your config, what evermeta data you want to pass to the component created.
        }
    }

    ngOnInit() {
        if (!components[this.config.type]) {
            const supportedTypes = Object.keys(components).join(', ');
            console.error(`Trying to use an unsupported type ${this.config.type} Supported types: ${supportedTypes}`);
        }

        const component = this.resolver.resolveComponentFactory<yourConfig>(components[this.config.type]);
        this.component = this.container.createComponent(component);
        this.component.instance.config = this.config;
    }
}

Таким образом, в ваших компонентах текст, строка, дата, что угодно - независимо от конфигурации, которую вы передавали в HTML в ng-container элемент будет доступен.

Конфиг, yourConfig, могут быть одинаковыми и определять ваши метаданные.

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

Основываясь на ответе Офира Стерна, вот вариант, который работает с AoT в Angular 4. Единственная проблема, с которой я столкнулся, это то, что я не могу внедрить какие-либо сервисы в DynamicComponent, но я могу с этим смириться.

примечание: я не тестировал с Angular 5.

import { Component, OnInit, Input, NgModule, NgModuleFactory, Compiler, EventEmitter, Output } from '@angular/core';
import { JitCompilerFactory } from '@angular/compiler';

export function createJitCompiler() {
  return new JitCompilerFactory([{
    useDebug: false,
    useJit: true
  }]).createCompiler();
}

type Bindings = {
  [key: string]: any;
};

@Component({
  selector: 'app-compile',
  template: `
    <div *ngIf="dynamicComponent && dynamicModule">
      <ng-container *ngComponentOutlet="dynamicComponent; ngModuleFactory: dynamicModule;">
      </ng-container>
    </div>
  `,
  styleUrls: ['./compile.component.scss'],
  providers: [{provide: Compiler, useFactory: createJitCompiler}]
})
export class CompileComponent implements OnInit {

  public dynamicComponent: any;
  public dynamicModule: NgModuleFactory<any>;

  @Input()
  public bindings: Bindings = {};
  @Input()
  public template: string = '';

  constructor(private compiler: Compiler) { }

  public ngOnInit() {

    try {
      this.loadDynamicContent();
    } catch (err) {
      console.log('Error during template parsing: ', err);
    }

  }

  private loadDynamicContent(): void {

    this.dynamicComponent = this.createNewComponent(this.template, this.bindings);
    this.dynamicModule = this.compiler.compileModuleSync(this.createComponentModule(this.dynamicComponent));

  }

  private createComponentModule(componentType: any): any {

    const runtimeComponentModule = NgModule({
      imports: [],
      declarations: [
        componentType
      ],
      entryComponents: [componentType]
    })(class RuntimeComponentModule { });

    return runtimeComponentModule;

  }

  private createNewComponent(template: string, bindings: Bindings): any {

    const dynamicComponent = Component({
      selector: 'app-dynamic-component',
      template: template
    })(class DynamicComponent implements OnInit {

      public bindings: Bindings;

      constructor() { }

      public ngOnInit() {
        this.bindings = bindings;
      }

    });

    return dynamicComponent;

  }

}

Надеюсь это поможет.

Ура!

Другие вопросы по тегам