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

151

Я хочу динамически создать шаблон. Это необходимо использовать для создания 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?

  • 0
    Так как решение, которое я нашел, было так приятно, я хочу, чтобы все, кто нашел этот вопрос, взглянули на мой ответ, который в данный момент находится очень далеко внизу, в самом низу. :)
  • 0
    В статье Вот, что вам нужно знать о динамических компонентах в Angular, есть отличное объяснение динамических компонентов.
Показать ещё 2 комментария
Теги:
angular
compilation

13 ответов

145
Лучший ответ

EDIT - связанный с 2.3.0 (2016-12-07)

ПРИМЕЧАНИЕ. Чтобы получить решение для предыдущей версии, проверьте историю этого сообщения.

Аналогичная тема обсуждается здесь Эквивалент компиляции $в Angular 2. Нам нужно использовать JitCompiler и NgModule. Подробнее о NgModule в Angular2 здесь:

В двух словах

Существует рабочий плункер/пример (динамический шаблон, динамический тип компонента, динамический модуль, JitCompiler,... в действии)

Принцип:
1) создать шаблон
2) найдите ComponentFactory в кеше - перейдите к 7)
3) - создать Component
4) - создать Module
5) - скомпилировать Module
6) - возврат (и кеш для последующего использования) ComponentFactory
7) используйте Цель и ComponentFactory для создания экземпляра динамического Component

Вот фрагмент кода (более здесь). Наш пользовательский Builder возвращает только встроенный/кэшированный ComponentFactory и объект Target placeholder для создания экземпляра 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 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 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, runtime module.. но это будет создано позже, как часть задания 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 мы используем этот простой/наивный строитель.

Реальное решение, настоящий шаблонный строитель, - это то место, где ваше приложение может многое сделать

// 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) должно быть частью динамического компонента, который мы создадим дальше.

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

export interface IHaveDynamicData { 
    public entity: any;
    ...
}

Конструктор ComponentFactory

Очень важно здесь иметь в виду:

наш тип компонента, построенный с помощью нашего DynamicTypeBuilder, может отличаться, но только по его шаблону (созданному выше). Свойства компонентов (входы, выходы или некоторые защищенные) все те же. Если нам нужны разные свойства, мы должны определить другую комбинацию Template и Type Builder

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

Пусть сначала суммирует операторы импорта:

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. Каждому заполнителю Target будет предложено создать Component с помощью factory.

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 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) этого сообщения, проверьте историю

  • 1
    Я вижу , вы все еще используете directives ключевое слово в @component ( type.builder.ts ). Разве это не амортизируется после RC5? Если да, есть ли решение, которое позволяет избежать этого? Я думаю о том, чтобы поместить @ngmodule в app/parts/*.ts , но пока не удалось ...
  • 0
    Спасибо, я думаю, что решил это, постараюсь опубликовать здесь и плункер ...
Показать ещё 24 комментария
50

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

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

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 {}

Live at http://plnkr.co/edit/fdP9Oc.

  • 3
    Я бы сказал, что это пример того, как написать как можно меньше кода, чтобы сделать то же самое, что и в моем ответе stackoverflow.com/a/38888009/1679310 . В случае, если это должно быть полезно (в основном шаблон, генерирующий RE), когда условие меняется ... простой вызов ngAfterViewInit с const template не будет работать. Но если ваша задача состояла в том, чтобы сократить описанный выше детально описанный подход (создать шаблон, создать компонент, создать модуль, скомпилировать его, создать фабрику .. создать экземпляр) ... вы, вероятно, сделали это
  • 0
    Спасибо за решение: у меня проблемы с загрузкой templateUrl и стилей, но я получаю следующую ошибку: Реализация ResourceLoader не была предоставлена. Не могу прочитать URL localhost: 3000 / app / pages / pages_common.css , есть идеи, что мне не хватает?
Показать ещё 15 комментариев
44

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

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

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

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

ОБНОВИТЬ

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

  • 1
    Спасибо за внимание к этому, может обновить и затем дать трещину - выполнение этого в Angular2 обязательно сломается в Angular4 в любом случае.
  • 3
    Это отлично сработало для меня с Angular4. Единственное, что мне нужно было сделать - это указать модули импорта для динамически создаваемого RuntimeComponentModule.
Показать ещё 15 комментариев
15

Я решил сжать все, что я узнал, в один файл. Там много чего взять, особенно по сравнению с 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 {}'
8

У меня есть простой пример, чтобы показать, как сделать динамический компонент angular 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 { }

(Здесь import 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

  • 2
    Итак, вы объявили module1, module2, module3. И если вам понадобится другой «динамический» контент шаблона, вам нужно будет создать определение (файл) формы moudle4 (module4.ts), верно? Если да, то это не похоже на динамику. Это статично, не так ли? Или я что-то пропустил?
  • 0
    Выше "template1" - это строка html, вы можете поместить в нее все что угодно, и мы называем этот динамический шаблон, так как этот вопрос задает
5

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

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

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

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

  • 0
    На момент написания статьи Angular 5 с AOT не поддерживает это, поскольку JIT-компилятор не входит в комплект. Без AOT работает как шарм :)
4

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

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

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

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

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

  • 1
    Спасибо за это предложение, однако, я сталкиваюсь с "Метаданными NgModule не найдены для 'DynamicHtmlModule'". Моя реализация основана на stackoverflow.com/questions/40060498/…
  • 2
    У кого-нибудь есть рабочий JitCompiletFactory с образцом AOT? У меня та же ошибка, что и у @Cybey
Показать ещё 1 комментарий
4

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

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

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

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

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

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

Чтобы включить динамическую детализацию внутри друг друга, вам нужно добавить DynamicModule.forRoot() в инструкции import в 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)

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

    private _compiler;

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

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

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

                this._cacheOfFactories[template] = factory;

                resolve(factory);
            });
    });

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

С уважением Morten

3

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

  1. Установите @angular-elements npm я @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
...

и в этом случае имя пользовательского тега я использовал "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 и после этого снова проанализировать его. Я не могу найти лучшего решения.

2

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

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

<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] выражении имеет значение, это указывает на одностороннее связывание данных, поэтому вы можете и даже должны опустить их в случае, если вы знаете, что вам не нужно связывать свойство с переменной.

  • 1
    Это было бы способом пойти .. если switch / case содержит несколько решений. Но представьте, что сгенерированный шаблон может быть очень большим ... и отличаться для каждой сущности, различаться по безопасности, различаться по статусу сущности, по каждому типу свойства (число, дата, ссылка ... редакторы) ... В таком случае, решение этого в html-шаблоне с помощью ngSwitch создаст большой, очень-очень большой html файл.
  • 0
    О, я согласен с тобой. У меня есть такой сценарий прямо здесь, прямо сейчас, так как я пытаюсь загрузить основные компоненты приложения, не зная перед компиляцией, какой именно класс должен отображаться. Хотя этот конкретный случай не требует создания динамического компонента.
1

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

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

1) Добавить те компоненты, которые вы хотите, чтобы динамически добавлять в entryComponents массив вашего 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

  • 0
    Чувак, ты только что прибил Этот получит внимание. Я не мог поверить, насколько чертовски сложно добавить простой динамический компонент в Angular, пока мне не понадобилось это сделать. Это похоже на сброс и возврат к временам до JQuery.
  • 1
    @ Gi1ber7 Я знаю, верно? Почему это заняло у них так много времени?
1

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

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

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

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

Спасибо

Виджай

-2

Основываясь на ответе Ophir Stern, вот вариант, который работает с 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;

  }

}

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

Ура!

Ещё вопросы

Сообщество Overcoder
Наверх
Меню