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

197

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

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

ComponentResolver is deprecated for dynamic compilation.
Use ComponentFactoryResolver together with @NgModule/@Component.entryComponents or ANALYZE_FOR_ENTRY_COMPONENTS provider instead.
For runtime compile only, you can also use Compiler.compileComponentSync/Async.

Я нашел этот документ ( 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.

Радим Келер
источник
Так как решение, которое я нашел, было настолько хорошим, что я хочу, чтобы все, кто нашел этот вопрос, взглянули на мой ответ, который в данный момент находится очень глубоко в самом низу. :)
Ричард Хоулц
Статья Вот, что вам нужно знать о динамических компонентах в Angular, есть отличное объяснение динамических компонентов.
Макс Корецкий
Вот проблема с каждым ответом и что $compile самом деле может сделать то, что эти методы не могут - я создаю приложение, в котором я просто хочу скомпилировать HTML, который поступает через стороннюю страницу и вызовы ajax. Я не могу удалить HTML со страницы и поместить его в свой собственный шаблон. Вздох
Оги Гарднер
@AugieGardner Есть причина, по которой это невозможно по замыслу. Angular не виноват в плохих архитектурных решениях или устаревших системах, которые есть у некоторых людей. Если вы хотите проанализировать существующий HTML-код, вы можете использовать другую платформу, поскольку Angular прекрасно работает с WebComponents. Установка четких границ для руководства полчищами неопытных программистов важнее, чем использование грязных хаков для немногих устаревших систем.
Фил

Ответы:

163

РЕДАКТИРОВАТЬ - связано с 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 ++ & компоненты среды выполнения

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

  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с.

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

Зависимость, которую мы должны получить:

// 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) этого поста, проверьте историю

Радим Келер
источник
50
это выглядит как такое сложное решение, устаревшее было очень простым и понятным, есть ли другой способ сделать это?
tibbus
3
Я думаю так же, как и @tibbus: это стало намного сложнее, чем раньше с устаревшим кодом. Спасибо за ваш ответ, хотя.
Лусио Моллинедо,
5
@ribsies спасибо за вашу заметку. Позвольте мне кое-что уточнить. Многие другие ответы пытаются сделать это простым . Но я пытаюсь объяснить это и показать в сценарии, закрытом для реального использования . Нам нужно было бы кэшировать вещи, мы должны были бы вызывать destroy при воссоздании и т. Д. Итак, хотя магия динамического построения действительно в том, type.builder.tsчто вы указали, я бы хотел, чтобы любой пользователь понимал, как поместить все это в контекст ... Надеюсь, что это может быть полезно;)
Радим Келер
7
@Radim Köhler - я попробовал этот пример. это работает без AOT. Но когда я попытался запустить его с помощью AOT, выдается сообщение об ошибке «Метаданные NgModule не найдены для RuntimeComponentModule». Можете ли вы помочь мне, пожалуйста, решить эту ошибку.
Труша
4
Сам ответ идеален! Но для реальных приложений нереально. Команда Angular должна предоставить решение для этого в рамках, так как это является общим требованием в бизнес-приложениях. Если нет, нужно спросить, является ли Angular 2 подходящей платформой для бизнес-приложений.
Карл
58

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

Рене Гамбургер
источник
3
Я бы сказал, что это пример того, как написать как можно меньше кода, чтобы сделать то же, что и в моем ответе stackoverflow.com/a/38888009/1679310 . В случае, если это должно быть полезно (в основном шаблон, генерирующий RE), когда условие меняется ... простой ngAfterViewInitвызов с const templateне будет работать. Но если ваша задача заключалась в том, чтобы сократить описанный выше детально описанный подход (создать шаблон, создать компонент, создать модуль, скомпилировать его, создать фабрику .. создать экземпляр) ... вы, вероятно, сделали это
Радим Келер,
Спасибо за решение: у меня проблемы с загрузкой templateUrl и стилей, но я получаю следующую ошибку: Реализация ResourceLoader не была предоставлена. Не могу прочитать URL localhost: 3000 / app / pages / pages_common.css , есть идеи, что мне не хватает?
Джерардламо
Можно ли скомпилировать шаблон HTML с данными, специфичными для ячейки в сетке, как элемент управления? plnkr.co/edit/vJHUCnsJB7cwNJr2cCwp?p=preview Как скомпилировать и показать изображение в последнем столбце в этом лонжероне? Любая помощь.?
Картик
1
@monnef, ты прав. Я не проверял журнал консоли. Я изменил код, добавив компонент в ngOnInit, а не в хук ngAfterViewInit, так как первый запускается до, а второй - после обнаружения изменений. (См. Github.com/angular/angular/issues/10131 и аналогичные темы.)
Рене Гамбургер
1
аккуратно и просто. Работал как ожидалось при обслуживании через браузер в dev. Но работает ли это с AOT? Когда приложение запускается в PROD после компиляции, я получаю сообщение «Ошибка: компилятор времени выполнения не загружен» в момент попытки компиляции компонента. (кстати, я использую Ionic 3.5)
mymo
52

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

То , что я в конечном итоге делает это с помощью Angular 4.0.0-beta.6«s 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.

Офир Стерн
источник
3
Это отлично сработало для меня с Angular4. Единственное, что мне нужно было сделать - это указать модули импорта для динамически создаваемого RuntimeComponentModule.
Рахул Патель
8
Вот быстрый пример, начиная с Angular Quickstart
Рахул Патель
5
Это решение работает с "ng build --prod"? Кажется, что класс компилятора и AoT не подходят друг другу.
Пьер Шаварош
2
@OphirStern Я также обнаружил, что подход хорошо работает в Angular 5, но НЕ с флагом сборки --prod.
TaeKwonJoe
2
Я протестировал его с углом 5 (5.2.8) с использованием JitCompilerFactory и использование флага --prod не работает! у кого-нибудь есть решение? (Кстати, JitCompilerFactory без флага --prod работает безупречно)
Фрэнк,
20

2019 июнь ответ

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

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

Шаг 1) Обновите свой AppModule

Импортируйте PortalModuleиз @angular/cdk/portalпакета и зарегистрируйте ваши динамические компоненты внутриentryComponents

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

Шаг 2. Вариант A: если вам НЕ нужно передавать данные и получать события от ваших динамических компонентов :

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

@Component({
  selector: 'app-child',
  template: `<p>I am a child.</p>`
})
export class MyDynamicComponent{
}

Увидеть это в действии

Шаг 2. Вариант B: если вам действительно необходимо передавать данные и получать события от ваших динамических компонентов :

// A bit of boilerplate here. Recommend putting this function in a utils 
// file in order to keep your component code a little cleaner.
function createDomPortalHost(elRef: ElementRef, injector: Injector) {
  return new DomPortalHost(
    elRef.nativeElement,
    injector.get(ComponentFactoryResolver),
    injector.get(ApplicationRef),
    injector
  );
}

@Component({
  selector: 'my-app',
  template: `
    <button (click)="onClickAddChild()">Click to add random child component</button>
    <div #portalHost></div>
  `
})
export class AppComponent {

  portalHost: DomPortalHost;
  @ViewChild('portalHost') elRef: ElementRef;

  constructor(readonly injector: Injector) {
  }

  ngOnInit() {
    this.portalHost = createDomPortalHost(this.elRef, this.injector);
  }

  onClickAddChild() {
    const myPortal = new ComponentPortal(MyDynamicComponent);
    const componentRef = this.portalHost.attach(myPortal);
    setTimeout(() => componentRef.instance.myInput 
      = '> This is data passed from AppComponent <', 1000);
    // ... if we had an output called 'myOutput' in a child component, 
    // this is how we would receive events...
    // this.componentRef.instance.myOutput.subscribe(() => ...);
  }
}

@Component({
  selector: 'app-child',
  template: `<p>I am a child. <strong>{{myInput}}</strong></p>`
})
export class MyDynamicComponent {
  @Input() myInput = '';
}

Увидеть это в действии

Стивен Пол
источник
1
Чувак, ты просто прибил Этот получит внимание. Я не мог поверить, насколько чертовски сложно добавить простой динамический компонент в Angular, пока мне не понадобилось это сделать. Это похоже на перезагрузку и возврат к временам до JQuery.
Gi1ber7
2
@ Gi1ber7 Я знаю, верно? Почему это заняло у них так много времени?
Стивен Пол
1
Хороший подход, но знаете ли вы, как передавать параметры в ChildComponent?
Снук
1
@ Это может ответить на ваш вопрос stackoverflow.com/questions/47469844/…
Стивен Пол
4
@StephenPaul Чем этот Portalподход отличается от ngTemplateOutletи ngComponentOutlet? 🤔
Гленн Мухаммед
18

Я решил сжать все, что я узнал, в один файл . Здесь есть что взять, особенно по сравнению с до 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 {}`
Стивен Пол
источник
10

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

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

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

Олег Пнк
источник
Interresting подход, но вам нужно будет предназначаться es2015 (поэтому никакой поддержки IE11) в вашем tsconfig.json, не othewise он потерпел неудачу вdocument.createElement(tagName);
Снук
Привет, как вы упомянули способ обработки входных данных, можно ли обрабатывать выходные данные дочерних компонентов так же, как это?
Мустахсан
5

Решил это в Angular 2 Final, просто используя директиву dynamicComponent из ng-dynamic .

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

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

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

Ричард Хоулц
источник
На момент написания статьи Angular 5 с AOT не поддерживает это, так как JIT-компилятор не входит в комплект. Без AOT это работает как очарование :)
Ричард Хоульц
это все еще относится к угловому 7+?
Карлос Э
4

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

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

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

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

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

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

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

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

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

Мортен Скьольдагер
источник
4

Следуя отличному ответу 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.

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

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

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

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

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

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

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

Спасибо

Виджай

Виджай Ананд Каннан
источник
0

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

В 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;
    }
}

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

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

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

saidutt
источник
-1

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

  }

}

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

Ура!

вспыльчивый
источник