Як я можу використовувати / створювати динамічний шаблон для компіляції динамічного компонента з кутовим 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 Synchronous Dynamic Component Creation )

І розумій, що я можу використовувати будь-яке

  • Вид динамічного 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 Існує причина, чому це не можливо за допомогою дизайну. Кутовий не винен у поганих архітектурних рішеннях чи спадкових системах, які мають деякі люди. Якщо ви хочете проаналізувати існуючий HTML-код, ви можете використовувати інший фреймворк, оскільки Angular прекрасно працює з WebComponents. Встановлення чітких меж для орієнтування орди недосвідчених програмістів важливіше, ніж дозволити брудні хаки для кількох застарілих систем.
Філ

Відповіді:


163

EDIT - пов’язано з 2.3.0 (2016-12-07)

ПРИМІТКА: щоб отримати рішення для попередньої версії, перевірте історію цієї публікації

Аналогічна тема обговорюється тут Еквівалент компіляції $ у куті 2 . Нам потрібно використовувати JitCompilerі NgModule. Детальніше про NgModuleAngular2 читайте тут:

Коротко

Є робочий планкер / приклад (динамічний шаблон, тип динамічного компонента, динамічний модуль ,, ... в дії)JitCompiler

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

Ось фрагмент коду (докладніше про це тут ) - наш користувальницький конструктор повертає щойно створений / кешований файл, 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

Нам потрібна NgModules.

Хоча я хотів би показати дуже простий приклад, у цьому випадку мені знадобляться три модулі (насправді 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.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, може відрізнятися, але лише за його шаблоном (створеним вище) . Властивості компонентів (входи, виходи або деякі захищені) залишаються однаковими. Якщо нам потрібні різні властивості, нам слід визначити різну комбінацію шаблона та типу 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 дякую за вашу замітку. Дозвольте мені щось уточнити. Багато інших відповідей намагаються зробити це простим . Але я намагаюся пояснити це та показати це у сценарії, закритому для реального використання . Нам потрібно кешувати речі, нам доведеться викликати руйнування при створенні і т. Д. Отже, хоча магія динамічного будівництва справді полягає в type.builder.tsтому, як ви вказали, я б хотів, щоб будь-який користувач зрозумів, як це зробити контекст ... Сподіваюся, що це може бути корисним;)
Радім Келер

7
@Radim Köhler - я спробував цей приклад. працює без AOT. Але коли я намагався запустити це з AOT, він показує помилку "Не знайдено метаданих NgModule для RuntimeComponentModule". Ви можете PLZ допомогти мені вирішити цю помилку.
Труша

4
Сама відповідь ідеальна! Але для реального життя додатки неможливі. Командна команда повинна запропонувати рішення для цього в рамках, оскільки це звичайна вимога в бізнес-програмах. Якщо ні, то слід запитати, чи Angular 2 - це правильна платформа для бізнес-додатків.
Карл

58

EDIT (26.08.2017) : Наведене нижче рішення добре працює з Angular2 та 4. Я оновив його, щоб містити змінну шаблону та обробник кліків, і протестував його з Angular 4.3.
Для Angular4, набагато краще рішення ngComponentOutlet, як описано у відповіді Офіра . Але зараз він ще не підтримує входи та виходи . Якщо [цей PR] ( https://github.com/angular/angular/pull/15362] прийнято, це можливо через екземпляр компонента, повернутий подією create.
Ng-dynamic-компонент може бути найкращим і найпростішим рішення взагалі, але я ще цього не перевіряв.

@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 . У випадку, якщо це має бути корисним випадком (в основному шаблоном повторної генерації), коли умова змінюється ... простий 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/isissue/10131 тощо)
Рене Гамбургер

1
акуратний і простий. Працювали, як очікувалося, під час обслуговування над браузером у розробці. Але чи працює це з AOT? Коли програма запускається в PROD після компіляції, я отримую "Помилка: компілятор виконання не завантажується" в момент спроби компіляції компонента. (btw, я використовую
Ionic

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

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

Сподіваюся, це допомагає, удачі!

ОНОВЛЕННЯ

Також працює для кутового 5.


3
Це чудово працювало для мене з Angular4. Єдине коригування, яке мені довелося зробити, - це можливість вказувати модулі імпорту для динамічно створеного RuntimeComponentModule.
Рахул Патель

8
Ось короткий приклад, починаючи з кутового Quickstart: embed.plnkr.co/9L72KpobVvY14uiQjo4p
Rahul Patel

5
Чи працює це рішення з "ng build --prod"? Здається, що клас компілятора та AoT не підходять разом atm.
П’єр Чавароче

2
@OphirStern Я також виявив, що підхід працює добре в Angular 5, але НЕ зі значком --prod build.
TaeKwonJoe

2
Я перевірив його з кутовим 5 (5.2.8) за допомогою JitCompilerFactory, а прапор --prod не працює! Хтось має рішення? (BTW 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
@Snook це може відповісти на ваше запитання stackoverflow.com/questions/47469844/…
Stephen Paul

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

У кутовій 7.x я використовував для цього кутові елементи.

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

і в цьому випадку користувальницька назва тегу я використовував "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, а після цього знову їх розбирати. Не можу знайти кращого рішення.


Інтересуючий підхід, але вам потрібно буде орієнтуватися на es2015 (тому немає підтримки для IE11) у вашому tsconfig.json, інакше це не вдасться оdocument.createElement(tagName);
Снук

Привіт, як ви згадали спосіб обробляти вхідні дані, то чи можна також обробляти виходи дочірніх компонентів?
Мустасан

5

Вирішено це кутова 2 Final версії , просто використовуючи dynamicComponent директиви від нг-гідродинамічного .

Використання:

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

Там, де шаблон є вашим динамічним шаблоном, і контекст може бути встановлений у будь-якій динамічній моделі даних, з якою ви хочете пов’язати ваш шаблон.


На момент написання Angular 5 з AOT це не підтримує, оскільки компілятор JIT не входить у комплект. Без AOT це працює як шарм :)
Річард Хольц

це все ще стосується кутових 7+?
Карлос Е

4

Я хочу додати кілька деталей на додаток до цього дуже чудового поста Радіма.

Я прийняв це рішення і трохи працював над ним, і швидко наштовхнувся на деякі обмеження. Я лише окреслю їх, а потім дам рішення і для цього.

  • Перш за все, мені не вдалося передати динамічну деталь всередині динамічної деталі (в основному, вбудовують динамічні інтерфейси всередині один одного).
  • Наступним питанням було те, що я хотів зробити динамічні деталі всередині однієї з частин, які були доступні в рішенні. Це було неможливо і з початковим рішенням.
  • Нарешті, не вдалося використати URL-адреси шаблонів у таких динамічних частинах, як редактор рядків.

На основі цієї публікації я поставив ще одне запитання про те, як досягти цих обмежень, які можна знайти тут:

рекурсивна компіляція динамічних шаблонів у 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вам доведеться вилучити динамічний дитайл, оскільки вони тепер є частиною

@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

Слідом за чудовою відповіддю Радміна, для кожного, хто використовує версію angular-cli версії 1.0.0-beta.22 і вище, потрібно трохи змінити.

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

Таким чином, вирішення цього питання взагалі не використовувати COMPILER_PROVIDERSі JitCompilerв цьому providersрозділі, але використовувати JitCompilerFactoryзамість цього "@ angular / компілятор", як це всередині класу builder типів:

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

Як бачите, він не є ін'єкційним і, таким чином, не має залежностей від DI. Це рішення також повинно працювати для проектів, що не використовують кутовий кліп.


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файл.
Радім Келер

О, я згоден з вами. У мене є такий сценарій саме тут, саме зараз, коли я намагаюся завантажити основні компоненти програми, не знаючи до компіляції конкретного класу, який потрібно відобразити. Хоча саме цей випадок не потребує створення динамічних компонентів.
zii

1

Це приклад динамічних елементів управління формами, створених із сервера.

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

Цей приклад динамічного управління формами знаходиться в компоненті add (саме тут ви можете отримати Formcontrols з сервера). Якщо ви бачите метод addcomponent, ви можете побачити Forms Controls. У цьому прикладі я не використовую кутовий матеріал, але він працює (я використовую @ 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;
    }
}

Отже, у ваших компонентах текст, рядок, дата, будь-що інше - незалежно від того, яка конфігурація, яку ви передавали в HTML в ng-containerелементі, буде доступною.

Конфігурація,, yourConfigможе бути однаковою і визначає ваші метадані.

Залежно від типу конфігурації або типу введення, директива повинна діяти відповідно, і з підтримуваних типів вона відображатиме відповідний компонент. Якщо ні, то він зафіксує помилку.


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

  }

}

Сподіваюся, це допомагає.

Ура!

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.