Jak mogę użyć / utworzyć szablon dynamiczny, aby skompilować komponent dynamiczny z Angular 2.0?

197

Chcę dynamicznie utworzyć szablon. Powinno to zostać wykorzystane do zbudowania ComponentTypeśrodowiska wykonawczego i umieszczenia go (a nawet zastąpienia) gdzieś w komponencie hostingowym.

Do czasu używania RC4 ComponentResolver, ale wraz z RC5 pojawia się następujący komunikat:

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.

Znalazłem ten dokument ( Synchroniczne dynamiczne tworzenie komponentów Angular 2 )

I zrozum, że mogę użyć albo

  • Rodzaj dynamiki ngIfz ComponentFactoryResolver. Jeśli przekażę znane elementy wewnątrz @Component({entryComponents: [comp1, comp2], ...})- mogę użyć.resolveComponentFactory(componentToRender);
  • Kompilacja środowiska wykonawczego z Compiler...

Ale pytanie brzmi: jak tego użyć Compiler? Powyższa uwaga mówi, że powinienem zadzwonić: Compiler.compileComponentSync/Async- więc jak?

Na przykład. Chcę utworzyć (w oparciu o niektóre warunki konfiguracji) tego rodzaju szablon dla jednego rodzaju ustawień

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

a w innym przypadku ten ( string-editorjest zastąpiony przez text-editor)

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

I tak dalej (inny numer / data / odniesienie editorswedług typów nieruchomości, pomijając niektóre właściwości dla niektórych użytkowników ...) . tzn. jest to przykład, rzeczywista konfiguracja może wygenerować znacznie więcej różnych i złożonych szablonów.

Szablon się zmienia, więc nie mogę używać ComponentFactoryResolveri przekazywać istniejących ... Potrzebuję rozwiązania z Compiler.

Radim Köhler
źródło
Ponieważ rozwiązanie, które znalazłem, było tak miłe, że każdy, kto znalazł to pytanie, mógł spojrzeć na moją odpowiedź, która jest w tej chwili bardzo głęboko na samym dole. :)
Richard Houltz
Artykuł Oto, co musisz wiedzieć o komponentach dynamicznych w Angular, ma świetne objaśnienie komponentów dynamicznych.
Max Koretskyi
Oto problem z każdą pojedynczą odpowiedzią i co $compilewłaściwie może zrobić to, czego nie potrafią te metody - tworzę aplikację, w której chcę po prostu skompilować kod HTML przychodzący za pośrednictwem strony innej firmy i wywołań ajax. Nie mogę usunąć kodu HTML ze strony i umieścić go we własnym szablonie. Westchnienie
Augie Gardner,
@AugieGardner Istnieje powód, dla którego nie jest to możliwe z założenia. Angular nie ponosi winy za złe decyzje architektoniczne lub starsze systemy, które mają niektórzy ludzie. Jeśli chcesz przeanalizować istniejący kod HTML, możesz użyć innego frameworka, ponieważ Angular doskonale współpracuje z WebComponents. Ustanowienie wyraźnych granic, aby poprowadzić hordy niedoświadczonych programistów, jest ważniejsze niż dopuszczanie brudnych hacków dla kilku starszych systemów.
Phil

Odpowiedzi:

163

EDYCJA - powiązane z 2.3.0 (2016-12-07)

UWAGA: aby uzyskać rozwiązanie dla poprzedniej wersji, sprawdź historię tego postu

Podobny temat omówiono tutaj Odpowiednik kompilacji $ w Angular 2 . Musimy użyć JitCompileri NgModule. Przeczytaj więcej o NgModuleAngular2 tutaj:

W skrócie

Istnieje działający plunker / przykład (szablon dynamiczny, typ komponentu dynamicznego, moduł dynamiczny JitCompiler, ... w akcji)

Podstawowa zasada to:
1) utwórz szablon
2) znajdź ComponentFactoryw pamięci podręcznej - przejdź do 7)
3) - utwórz Component
4) - utwórz Module
5) - skompiluj Module
6) - zwróć (i pamięć podręczną do późniejszego wykorzystania) ComponentFactory
7) użyj obiektu docelowego i ComponentFactoryutwórz instancję dynamicznyComponent

Oto fragment kodu (więcej tutaj ) - nasz niestandardowy konstruktor zwraca właśnie zbudowany / buforowany, ComponentFactorya widok Docelowy symbol zastępczy konsumuje się, aby utworzyć instancję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;
        //...
    });

To jest to - w skrócie. Aby uzyskać więcej informacji .. przeczytaj poniżej

.

TL&DR

Obserwuj plunkera i wróć, by przeczytać szczegóły, na wypadek gdyby jakiś fragment kodu wymagał więcej wyjaśnień

.

Szczegółowe objaśnienie - Angular2 RC6 ++ i komponenty środowiska wykonawczego

Poniżej opis tego scenariusza , będziemy

  1. utwórz moduł PartsModule:NgModule (uchwyt małych elementów)
  2. stwórz kolejny moduł DynamicModule:NgModule, który będzie zawierał nasz komponent dynamiczny (i referencje PartsModuledynamicznie)
  3. utwórz szablon dynamiczny (proste podejście)
  4. utwórz nowy Componenttyp (tylko jeśli szablon się zmienił)
  5. stwórz nowy RuntimeModule:NgModule. Ten moduł będzie zawierał wcześniej utworzony Componenttyp
  6. zadzwoń, JitCompiler.compileModuleAndAllComponentsAsync(runtimeModule)aby uzyskaćComponentFactory
  7. utwórz wystąpienie DynamicComponentzadania zastępczego View Target iComponentFactory
  8. przypisać @Inputsdo nowej instancji (z przełącznikiem INPUTdo TEXTAREAedycji) , zużywają@Outputs

NgModule

Potrzebujemy NgModules.

Chociaż chciałbym pokazać bardzo prosty przykład, w tym przypadku potrzebowałbym trzech modułów (w rzeczywistości 4 - ale nie liczę AppModule) . Proszę, weź to zamiast prostego fragmentu jako podstawy do naprawdę solidnego generatora komponentów dynamicznych.

Nie będzie jeden moduł dla wszystkich małych elementów, na przykład string-editor, text-editor ( date-editor, number-editor...)

@NgModule({
  imports:      [ 
      CommonModule,
      FormsModule
  ],
  declarations: [
      DYNAMIC_DIRECTIVES
  ],
  exports: [
      DYNAMIC_DIRECTIVES,
      CommonModule,
      FormsModule
  ]
})
export class PartsModule { }

Gdzie DYNAMIC_DIRECTIVESsą rozszerzalne i mają pomieścić wszystkie małe części używane w naszym dynamicznym szablonie / typie komponentu. Sprawdź app / parts / parts.module.ts

Drugi będzie modułem do naszej dynamicznej obsługi rzeczy. Będzie zawierał komponenty hostingowe i niektórych dostawców .. które będą singletonami. Dlatego opublikujemy je w standardowy sposób - zforRoot()

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
            ], 
        };
    }
}

Sprawdź użycie forRoot()wAppModule

Na koniec będziemy potrzebować adhoc, modułu wykonawczego .. ale zostanie on utworzony później, jako część DynamicTypeBuilderzadania.

Czwarty moduł, moduł aplikacji, jest tym, który deklaruje dostawców kompilatora:

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

Przeczytaj (czytaj) dużo więcej o NgModule tam:

Szablon budowniczy

W naszym przykładzie przetworzymy szczegóły tego rodzaju bytu

entity = { 
    code: "ABC123",
    description: "A description of this Entity" 
};

Aby stworzyć template, w tym plunkerze używamy tego prostego / naiwnego budowniczego.

Prawdziwe rozwiązanie, prawdziwy konstruktor szablonów, jest miejscem, w którym Twoja aplikacja może wiele zrobić

// 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>";
    }
}

Sztuką jest tutaj - buduje szablon, który wykorzystuje pewien zestaw znanych właściwości, np entity. Taka właściwość (-y) musi być częścią komponentu dynamicznego, który utworzymy następnie.

Aby było trochę łatwiej, możemy użyć interfejsu do zdefiniowania właściwości, których może użyć nasz konstruktor szablonów. Zostanie to zaimplementowane przez nasz dynamiczny typ komponentu.

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

ComponentFactorybudowniczy

Bardzo ważne jest, aby pamiętać:

nasz typ komponentu, zbudowany z naszym DynamicTypeBuilder, może się różnić - ale tylko jego szablonem (utworzonym powyżej) . Właściwości komponentów (wejścia, wyjścia lub niektóre chronione) są nadal takie same. Jeśli potrzebujemy różnych właściwości, powinniśmy zdefiniować inną kombinację szablonu i konstruktora typów

Dotykamy więc rdzenia naszego rozwiązania. Konstruktor: 1) utworzy ComponentType2) utworzy NgModule3) skompiluje ComponentFactory4) buforuje go w celu późniejszego ponownego użycia.

Zależność, którą musimy otrzymać:

// plunker - app/dynamic/type.builder.ts
import { JitCompiler } from '@angular/compiler';
    
@Injectable()
export class DynamicTypeBuilder {

  // wee need Dynamic component builder
  constructor(
    protected compiler: JitCompiler
  ) {}

A oto fragment, w jaki sposób uzyskać 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);
            });
    });
}

Powyżej tworzymy i buforujemy zarówno Componenti Module. Ponieważ jeśli szablon (w rzeczywistości prawdziwa dynamiczna część tego wszystkiego) jest taki sam ... możemy użyć ponownie

A oto dwie metody, które reprezentują naprawdę fajny sposób tworzenia udekorowanych klas / typów w środowisku wykonawczym. Nie tylko, @Componentale także@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;
}

Ważny:

nasze typy dynamiczne komponentów różnią się, ale tylko według szablonu. Wykorzystujemy ten fakt do buforowania ich. To jest naprawdę bardzo ważne. Angular2 również buforuje je .. według typu . A jeśli odtworzymy dla tych samych ciągów szablonów nowe typy ... zaczniemy generować wycieki pamięci.

ComponentFactory używany przez komponent hostingowy

Ostatnim elementem jest komponent, który hostuje cel dla naszego komponentu dynamicznego, np <div #dynamicContentPlaceHolder></div>. Otrzymujemy odniesienie do niego i używamy go ComponentFactorydo utworzenia komponentu. To w skrócie, a oto wszystkie elementy tego komponentu (w razie potrzeby otwórz tutaj plunker )

Podsumujmy najpierw instrukcje importu:

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

Właśnie otrzymujemy konstruktorów szablonów i komponentów. Dalej są właściwości, które są potrzebne w naszym przykładzie (więcej w komentarzach)

// 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" 
  };

W tym prostym scenariuszu nasz komponent hostingowy nie ma żadnego @Input. Nie musi więc reagować na zmiany. Ale pomimo tego (i aby być gotowym na nadchodzące zmiany) - musimy wprowadzić flagę, jeśli składnik został już (po raz pierwszy) zainicjowany. I dopiero wtedy możemy rozpocząć magię.

Wreszcie użyjemy naszego konstruktora komponentów, który jest po prostu skompilowany / buforowany ComponentFacotry . Nasz obiekt zastępczy Target zostanie poproszony o utworzenie instancji wComponent tej fabryce.

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;
        //...
    });
}

małe rozszerzenie

Musimy również zachować odniesienie do skompilowanego szablonu .., aby móc destroy()go poprawnie , za każdym razem, gdy go zmienimy.

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

Gotowe

To właściwie wszystko. Nie zapomnij zniszczyć czegokolwiek, co zostało zbudowane dynamicznie (ngOnDestroy) . Pamiętaj też o buforowaniu dynamicznym typesi modulesjeśli jedyną różnicą jest ich szablon.

Sprawdź to wszystko tutaj

aby zobaczyć poprzednie wersje tego postu (np. związane z RC5) , sprawdź historię

Radim Köhler
źródło
50
wygląda to na tak skomplikowane rozwiązanie, przestarzałe było bardzo proste i jasne, czy jest na to inny sposób?
Tibbus
3
Myślę tak samo jak @tibbus: stało się to o wiele bardziej skomplikowane niż w przypadku przestarzałego kodu. Ale dziękuję za odpowiedź.
Lucio Mollinedo,
5
@ribsies dzięki za notatkę. Pozwól, że coś wyjaśnię. Wiele innych odpowiedzi spróbować zrobić to proste . Ale staram się to wyjaśnić i pokazać w scenariuszu zamkniętym na rzeczywiste użycie . Musielibyśmy buforować rzeczy, musielibyśmy nazwać zniszczeniem przy odtwarzaniu itp. Tak więc, chociaż magia dynamicznego budowania jest naprawdę, type.builder.tsjak wskazałeś, chciałbym, aby każdy użytkownik zrozumiał, jak to wszystko umieścić w kontekst ... Mam nadzieję, że może się przydać;)
Radim Köhler,
7
@Radim Köhler - próbowałem tego przykładu. działa bez AOT. Ale kiedy próbowałem uruchomić to z AOT, pokazuje błąd „Nie znaleziono metadanych NgModule dla RuntimeComponentModule”. czy możesz mi pomóc rozwiązać ten błąd?
Trusha
4
Sama odpowiedź jest idealna! Ale w rzeczywistych zastosowaniach nie jest to możliwe. Zespół Angular powinien dostarczyć rozwiązanie tego problemu w ramach, ponieważ jest to powszechny wymóg w aplikacjach biznesowych. Jeśli nie, należy zapytać, czy Angular 2 jest odpowiednią platformą dla aplikacji biznesowych.
Karl
58

EDYCJA (26/08/2017) : Poniższe rozwiązanie działa dobrze z Angular2 i 4. Zaktualizowałem go, aby zawierał zmienną szablonu i moduł obsługi kliknięć, i przetestowałem go z Angular 4.3.
Dla Angular4 ngComponentOutlet, jak opisano w odpowiedzi Ophira, jest znacznie lepszym rozwiązaniem. Ale w tej chwili nie obsługuje jeszcze wejść i wyjść . Jeśli [ten PR] ( https://github.com/angular/angular/pull/15362] zostanie zaakceptowany, byłoby to możliwe poprzez instancję komponentu zwróconą przez zdarzenie create.
Ng-dynamic-component może być najlepszy i najprostszy rozwiązanie w ogóle, ale jeszcze tego nie testowałem.

Odpowiedź @Long Field jest na miejscu! Oto inny (synchroniczny) przykład:

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

Na żywo pod adresem http://plnkr.co/edit/fdP9Oc .

Rene Hamburger
źródło
3
Powiedziałbym, że jest to przykład, jak napisać jak najmniej kodu, aby zrobić to samo, co w mojej odpowiedzi stackoverflow.com/a/38888009/1679310 . W przypadku, gdy powinien to być użyteczny przypadek (głównie szablon generujący RE), gdy warunki się zmieniają ... proste ngAfterViewInitwywołanie z const templatenie zadziała. Ale jeśli Twoim zadaniem było zredukowanie wyżej opisanego podejścia (utwórz szablon, utwórz komponent, utwórz moduł, skompiluj go, utwórz fabrykę ... utwórz instancję) ... prawdopodobnie to zrobiłeś
Radim Köhler,
Dzięki za rozwiązanie: Mam problem z ładowaniem templateUrl i stylów, ale pojawia się następujący błąd: Nie podano implementacji ResourceLoader. Nie możesz odczytać adresu URL localhost: 3000 / app / pages / pages_common.css , masz pojęcie, czego mi brakuje?
Gerardlamo,
Czy można skompilować szablon HTML z danymi specyficznymi dla komórki w siatce, np. Kontrola. plnkr.co/edit/vJHUCnsJB7cwNJr2cCwp?p=preview W tym narzędziu, jak mogę skompilować i wyświetlić obraz w ostatniej kolumnie. Jakaś pomoc.?
Karthick
1
@monnef, masz rację. Nie sprawdziłem dziennika konsoli. Zmodyfikowałem kod, aby dodać komponent do haka ngOnInit zamiast haka ngAfterViewInit, ponieważ ten pierwszy jest uruchamiany przed, a drugi po wykryciu zmiany. (Zobacz github.com/angular/angular/issues/10131 i podobne wątki.)
Rene Hamburger
1
schludny i prosty. Działa zgodnie z oczekiwaniami podczas wyświetlania w przeglądarce w przeglądarce. Ale czy to działa z AOT? Gdy aplikacja jest uruchamiana w PROD po kompilacji, pojawia się komunikat „Błąd: Kompilator środowiska wykonawczego nie jest ładowany” w momencie próby kompilacji komponentu. (przy okazji, korzystam z Ionic 3.5)
również
52

Musiałem przybyć na imprezę późno, żadne z przedstawionych tu rozwiązań nie wydawało mi się pomocne - zbyt niechlujne i wydawało mi się, że to zbyt duże obejście.

Co skończyło się robi to używając Angular 4.0.0-beta.6„s ngComponentOutlet .

To dało mi najkrótsze, najprostsze rozwiązanie, wszystkie zapisane w pliku komponentu dynamicznego.

  • Oto prosty przykład, który po prostu odbiera tekst i umieszcza go w szablonie, ale oczywiście możesz go zmienić w zależności od potrzeb:
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;
  }
}
  • Krótkie wyjaśnienie:
    1. my-component - komponent, w którym renderowany jest komponent dynamiczny
    2. DynamicComponent - komponent, który ma być budowany dynamicznie i jest renderowany wewnątrz mojego komponentu

Nie zapomnij zaktualizować wszystkich bibliotek kątowych do ^ Angular 4.0.0

Mam nadzięję, że to pomogło, powodzenia!

AKTUALIZACJA

Działa również dla kątownika 5.

Ophir Stern
źródło
3
Działa to świetnie dla mnie z Angular4. Jedyne, co musiałem zrobić, to móc określić moduły importu dla dynamicznie tworzonego RuntimeComponentModule.
Rahul Patel
8
Oto szybki przykład zaczynający się od Angular Quickstart: embed.plnkr.co/9L72KpobVvY14uiQjo4p
Rahul Patel
5
Czy to rozwiązanie działa z „ng build --prod”? Wygląda na to, że klasa kompilatora i AoT nie pasują do siebie atm.
Pierre Chavaroche
2
@OphirStern Odkryłem również, że podejście działa dobrze w Angular 5, ale NIE z flagą kompilacji --prod.
TaeKwonJoe
2
Przetestowałem to przy użyciu kątowej 5 (5.2.8) przy użyciu JitCompilerFactory i użycie flagi --prod nie działa! Czy ktoś ma rozwiązanie? (BTW JitCompilerFactory bez flagi --prod działa bezbłędnie)
Frank
20

Odpowiedź na czerwiec 2019 r

Dobre wieści! Wygląda na to, że pakiet @ angular / cdk ma teraz doskonałą obsługę portali !

W chwili pisania tego artykułu nie uważałem powyższych oficjalnych dokumentów za szczególnie pomocne (szczególnie w odniesieniu do wysyłania danych i odbierania zdarzeń z komponentów dynamicznych). Podsumowując, będziesz musiał:

Krok 1) Zaktualizuj swój AppModule

Zaimportuj PortalModulez @angular/cdk/portalpakietu i zarejestruj w nim komponenty dynamiczneentryComponents

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

Krok 2. Opcja A: Jeśli NIE musisz przekazywać danych i odbierać zdarzeń z komponentów dynamicznych :

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

Zobacz to w akcji

Krok 2. Opcja B: Jeśli musisz przekazywać dane i odbierać zdarzenia z komponentów dynamicznych :

// 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 = '';
}

Zobacz to w akcji

Stephen Paul
źródło
1
Koleś, właśnie przybiłeś gwoździe. Ten przyciągnie uwagę. Nie mogłem uwierzyć, jak cholernie trudno jest dodać prosty komponent dynamiczny do Angulara, dopóki nie musiałem go zrobić. To jak resetowanie i powrót do czasów sprzed JQuery.
Gi1ber7
2
@ Gi1ber7 Wiem, prawda? Dlaczego zajęło im to tak długo?
Stephen Paul,
1
Dobre podejście, ale czy wiesz, jak przekazać parametry do ChildComponent?
Snook
1
@ Snook to może odpowiedzieć na twoje pytanie stackoverflow.com/questions/47469844/…
Stephen Paul
4
@StephenPaul Czym Portalróżni się to podejście od ngTemplateOutleti ngComponentOutlet? 🤔
Glenn Mohammad,
18

Postanowiłem zebrać wszystko, czego się nauczyłem, w jeden plik . Jest tu wiele do zrobienia, zwłaszcza w porównaniu do RC5. Zauważ, że ten plik źródłowy zawiera AppModule i 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 {}`
Stephen Paul
źródło
10

Mam prosty przykład, jak pokazać kątowy komponent dynamiczny 2 rc6.

Załóżmy, że masz dynamiczny szablon HTML = szablon1 i chcesz dynamicznie ładować, najpierw zawinąć w komponent

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

tutaj szablon1 jako HTML może zawierać komponent ng2

Od rc6, muszę mieć @NgModule opakowujący ten komponent. @NgModule, podobnie jak moduł w anglarJS 1, oddziela różne części aplikacji ng2, więc:

@Component({
  template: template1,

})
class DynamicComponent {

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

(Tutaj zaimportuj RouterModule, ponieważ w moim przykładzie w html znajduje się kilka składników trasy, co później można zobaczyć)

Teraz możesz skompilować DynamicModule jako: this.compiler.compileModuleAndAllComponentsAsync(DynamicModule).then( factory => factory.componentFactories.find(x => x.componentType === DynamicComponent))

I musimy umieścić powyżej w app.moudule.ts, aby go załadować, zobacz mój app.moudle.ts. Aby uzyskać więcej szczegółowych informacji, sprawdź: https://github.com/Longfld/DynamicalRouter/blob/master/app/MyRouterLink.ts and app.moudle.ts

i zobacz demo: http://plnkr.co/edit/1fdAYP5PAbiHdJfTKgWo?p=preview

Long Field
źródło
3
Więc zadeklarowałeś moduł1, ​​moduł2, moduł3. A jeśli potrzebujesz innej „dynamicznej” zawartości szablonu, musisz utworzyć definicję (plik) z moudle4 (module4.ts), prawda? Jeśli tak, nie wydaje się to być dynamiczne. To jest statyczne, prawda? Czy coś mi umknęło?
Radim Köhler,
W powyższym „szablon1” to ciąg html, możesz w nim umieścić wszystko, a my nazywamy ten dynamiczny szablon, jak zadaje to pytanie
Long Field
6

W wersji kątowej 7.x użyłem do tego elementów kątowych.

  1. Zainstaluj @ angular-elements npm i @ angular / elements -s

  2. Utwórz usługę akcesoriów.

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

Pamiętaj, że niestandardowy znacznik elementu musi być inny w przypadku selektora komponentu kątowego. w AppUserIconComponent:

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

w tym przypadku niestandardową nazwę znacznika użyłem „ikona użytkownika”.

  1. Następnie musisz wywołać rejestr w AppComponent:
@Component({
  selector: 'app-root',
  template: '<router-outlet></router-outlet>'
})
export class AppComponent {
  constructor(   
    dynamicComponents: DynamicComponentsService,
  ) {
    dynamicComponents.register();
  }

}
  1. A teraz w dowolnym miejscu kodu możesz użyć go w następujący sposób:
dynamicComponents.create('user-icon', {user:{...}});

lub tak:

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

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

(w szablonie):

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

Zauważ, że w drugim przypadku musisz przekazać obiekty za pomocą JSON.stringify, a następnie ponownie je przeanalizuj. Nie mogę znaleźć lepszego rozwiązania.

Oleg Pnk
źródło
Interesujące podejście, ale musisz celować w es2015 (więc brak obsługi IE11) w tsconfig.json, w przeciwnym razie nie powiedzie siędocument.createElement(tagName);
Snook
Cześć, jak wspomniałeś o sposobie obsługi danych wejściowych, więc czy wyjścia komponentów podrzędnych mogą być również obsługiwane w ten sposób?
Mustahsan
5

Rozwiązano to w wersji Angular 2 Final, wykorzystując po prostu dyrektywę dynamicComponent od ng-dynamic .

Stosowanie:

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

Gdzie szablon jest Twoim szablonem dynamicznym, a kontekst można ustawić na dowolny dynamiczny model danych, z którym szablon ma się wiązać.

Richard Houltz
źródło
W momencie pisania Angular 5 z AOT nie obsługuje tego, ponieważ kompilator JIT nie jest zawarty w pakiecie. Bez AOT działa jak urok :)
Richard Houltz
czy nadal dotyczy to kątowych 7+?
Carlos E
4

Chciałbym dodać kilka szczegółów do tego bardzo doskonałego postu autorstwa Radima.

Wziąłem to rozwiązanie i pracowałem nad nim przez chwilę i szybko napotkałem pewne ograniczenia. Po prostu opiszę je, a następnie podam rozwiązanie.

  • Przede wszystkim nie byłem w stanie renderować szczegółów dynamicznych wewnątrz szczegółów dynamicznych (zasadniczo zagnieżdżają się w sobie dynamiczne interfejsy użytkownika).
  • Kolejnym problemem było to, że chciałem renderować szczegóły dynamiczne w jednej z części udostępnionych w rozwiązaniu. Nie było to również możliwe w przypadku pierwotnego rozwiązania.
  • Wreszcie nie było możliwe użycie adresów URL szablonów w częściach dynamicznych, takich jak edytor ciągów.

Zadałem kolejne pytanie na podstawie tego postu, w jaki sposób osiągnąć te ograniczenia, które można znaleźć tutaj:

rekurencyjna dynamiczna kompilacja szablonów w angular2

Przedstawię tylko odpowiedzi na te ograniczenia, jeśli napotkasz ten sam problem, co ja, ponieważ dzięki temu rozwiązanie będzie bardziej elastyczne. Byłoby wspaniale, gdyby zaktualizowano również początkowy plunker.

Aby włączyć zagnieżdżanie szczegółów dynamicznych między sobą, musisz dodać DynamicModule.forRoot () w instrukcji importu w 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;
}

Poza tym nie było możliwe użycie <dynamic-detail>w jednej z części edytora tekstu lub edytora tekstu.

Aby to włączyć, musisz zmienić parts.module.tsidynamic.module.ts

Wewnątrz parts.module.tsMusisz dodać DynamicDetailwDYNAMIC_DIRECTIVES

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

Również w dynamic.module.tstrzeba będzie usunąć dynamicDetail, ponieważ są one teraz częścią części

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

Działający zmodyfikowany plunker można znaleźć tutaj: http://plnkr.co/edit/UYnQHF?p=preview (nie rozwiązałem tego problemu, jestem tylko posłańcem :-D)

Wreszcie nie było możliwe użycie szablonów w częściach utworzonych na komponentach dynamicznych. Rozwiązaniem (lub obejściem problemu. Nie jestem pewien, czy jest to błąd kątowy, czy nieprawidłowe użycie frameworka) było utworzenie kompilatora w konstruktorze zamiast wstrzykiwania go.

    private _compiler;

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

Następnie użyj _compilerdo kompilacji, a następnie szablonUrls są również włączone.

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

                this._cacheOfFactories[template] = factory;

                resolve(factory);
            });
    });

Mam nadzieję, że to pomoże komuś innemu!

Z pozdrowieniami Morten

Morten Skjoldager
źródło
4

W następstwie doskonałej odpowiedzi Radmin, potrzebna jest drobna poprawka dla każdego, kto używa wersji kątowej cli w wersji 1.0.0-beta.22 i wyższej.

COMPILER_PROVIDERSnie można już importować (szczegółowe informacje można znaleźć w GitHub angular-cli ).

Zatem obejściem, którego w ogóle nie należy używać COMPILER_PROVIDERSiw JitCompilertej providerssekcji, ale należy użyć JitCompilerFactory„@ angular / compiler” zamiast tego w klasie konstruktora typów:

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

Jak widać, nie można go wstrzykiwać, a zatem nie ma zależności od DI. To rozwiązanie powinno także działać w projektach, które nie używają angular-cli.

Sebastian
źródło
1
Dziękuję za tę sugestię, ale wpadam na „Nie znaleziono metadanych NgModule dla„ DynamicHtmlModule ””. Moja implementacja oparta jest na stackoverflow.com/questions/40060498/…
Cybey
2
Czy ktoś ma działający JitCompiletFactory z próbką AOT? Mam taki sam błąd jak @Cybey
user2771738,
To naprawdę nie wydaje się możliwe. Proszę zobaczyć github.com/angular/angular/issues/11780 , medium.com/@isaacplmann/… i stackoverflow.com/questions/42537138/...
Sebastian
2

Sam próbuję zobaczyć, jak mogę zaktualizować RC4 do RC5, dlatego natknąłem się na ten wpis, a nowe podejście do dynamicznego tworzenia komponentów wciąż jest dla mnie nieco tajemnicą, więc nie sugeruję niczego na temat resolvera fabryki komponentów.

Ale mogę zasugerować nieco jaśniejsze podejście do tworzenia komponentów w tym scenariuszu - wystarczy użyć przełącznika w szablonie, który utworzyłby edytor ciągów lub edytor tekstu zgodnie z pewnymi warunkami, takimi jak ten:

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

A tak przy okazji, „[” w wyrażeniu [prop] ma znaczenie, oznacza to jednokierunkowe wiązanie danych, dlatego możesz, a nawet powinieneś je pominąć, jeśli wiesz, że nie musisz wiązać właściwości ze zmienną.

zii
źródło
1
To byłby sposób, aby przejść .. jeśli switch/ casezawiera kilka decyzji. Ale wyobraź sobie, że wygenerowany szablon może być naprawdę duży ... i różnić się dla każdej jednostki, różnić się pod względem bezpieczeństwa, różnią się statusem jednostki, według każdego typu właściwości (liczba, data, odniesienie ... redaktorzy) ... W takim przypadku rozwiązanie tego w szablonie HTML ngSwitchutworzyłoby duży, bardzo duży htmlplik.
Radim Köhler,
Och, zgadzam się z tobą. Mam tego rodzaju scenariusz właśnie tutaj, właśnie teraz, gdy próbuję załadować główne komponenty aplikacji, nie wiedząc przed kompilacją, jaka konkretna klasa ma zostać wyświetlona. Chociaż ten szczególny przypadek nie wymaga dynamicznego tworzenia komponentów.
zii
1

To jest przykład dynamicznych kontrolek Form generowanych z serwera.

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

Ten przykład przedstawia dynamiczne formanty Form w dodawanym składniku (w tym miejscu można uzyskać Formcontrols z serwera). Jeśli zobaczysz metodę addcomponent, możesz zobaczyć Forms Controls. W tym przykładzie nie używam materiału kątowego, ale działa (używam @ work). Jest to celowane do kątowego 6, ale działa we wszystkich poprzednich wersjach.

Musisz dodać JITComplierFactory dla AngularVersion 5 i nowszych.

Dzięki

Vijay

Vijay Anand Kannan
źródło
0

W tym konkretnym przypadku lepszym rozwiązaniem byłoby użycie dyrektywy do dynamicznego tworzenia komponentu. Przykład:

W kodzie HTML, w którym chcesz utworzyć komponent

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

Podejdę i zaprojektuję dyrektywę w następujący sposób.

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

Tak więc w twoich komponentach będzie dostępny tekst, ciąg, data, cokolwiek - bez względu na konfigurację, którą przekazałeś w HTML w ng-containerelemencie.

Konfiguracja yourConfigmoże być taka sama i definiować metadane.

W zależności od konfiguracji lub typu danych wejściowych dyrektywa powinna działać odpowiednio, a na podstawie obsługiwanych typów będzie renderować odpowiedni komponent. Jeśli nie, zarejestruje błąd.

saidutt
źródło
-1

Opierając się na odpowiedzi Ophira Sterna, oto wariant, który działa z AoT w Angular 4. Jedynym problemem, jaki mam, jest to, że nie mogę wstrzyknąć żadnych usług do komponentu DynamicComponent, ale mogę z tym żyć.

Uwaga: Nie testowałem z 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;

  }

}

Mam nadzieję że to pomoże.

Twoje zdrowie!

Choleryk
źródło