Como posso usar / criar um modelo dinâmico para compilar o componente dinâmico com o Angular 2.0?

197

Eu quero criar dinamicamente um modelo. Isso deve ser usado para criar um ComponentTypetempo de execução e colocá-lo (até substituí- lo ) em algum lugar dentro do componente de hospedagem.

Até o RC4 eu estava usando ComponentResolver, mas com o RC5 recebo a seguinte mensagem:

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.

Encontrei este documento ( Criação de componente dinâmico síncrono angular 2 )

E entenda que eu posso usar

  • Tipo de dinâmica ngIfcom ComponentFactoryResolver. Se eu passar componentes conhecidos dentro de @Component({entryComponents: [comp1, comp2], ...})- eu posso usar.resolveComponentFactory(componentToRender);
  • Compilação em tempo real, com Compiler...

Mas a questão é como usar isso Compiler? A nota acima diz que eu deveria ligar: Compiler.compileComponentSync/Async- e como?

Por exemplo. Quero criar (com base em algumas condições de configuração) esse tipo de modelo para um tipo de configuração

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

e em outro caso este ( string-editoré substituído por text-editor)

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

E assim por diante (número / data / referência diferente editorspor tipos de propriedade, ignorou algumas propriedades para alguns usuários ...) . isto é, por exemplo, a configuração real pode gerar modelos muito mais diferentes e complexos.

O modelo está mudando, então não posso usar ComponentFactoryResolvere passar os existentes ... Preciso de uma solução com o Compiler.

Radim Köhler
fonte
Desde que a solução que encontrei foi tão boa, quero que todos que encontrarem essa pergunta dêem uma olhada na minha resposta, que está bem no fundo no momento. :)
Richard Houltz
Aqui está o problema com todas as respostas disponíveis e o $compileque realmente pode fazer que esses métodos não possam - estou criando um aplicativo no qual eu apenas quero compilar o HTML conforme ele aparece na página de terceiros e nas chamadas ajax. Não consigo remover o HTML da página e colocá-lo no meu próprio modelo. Sigh
Augie Gardner
@AugieGardner Há uma razão pela qual isso não é possível por design. A Angular não é culpada por más decisões arquiteturais ou sistemas legados que algumas pessoas têm. Se você deseja analisar o código HTML existente, pode usar outra estrutura, pois o Angular funciona perfeitamente com os WebComponents. Definir limites claros para guiar as hordas de programadores inexperientes é mais importante do que permitir hacks sujos para poucos sistemas legados.
Phil

Respostas:

163

EDIT - relacionado a 2.3.0 (07/12/2016)

NOTA: para obter solução para a versão anterior, verifique o histórico desta postagem

Tópico semelhante é discutido aqui Equivalente a $ compile no Angular 2 . Precisamos usar JitCompilere NgModule. Leia mais sobre NgModuleno Angular2 aqui:

Em poucas palavras

um exemplo / exemplo de trabalho ativo (modelo dinâmico, tipo de componente dinâmico, módulo dinâmico JitCompiler, ... em ação)

O principal é:
1) criar o modelo
2) encontrar ComponentFactoryno cache - vá para 7)
3) - criar Component
4) - criar Module
5) - compilar Module
6) - retornar (e armazenar em cache para uso posterior) ComponentFactory
7) usar o Target e ComponentFactorycriar uma instância de dinâmicoComponent

Aqui está um trecho de código (mais sobre isso aqui ) - Nosso Construtor personalizado está retornando apenas construído / armazenado em cache ComponentFactorye a exibição Espaço reservado de destino consome para criar uma instância doDynamicComponent

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

É isso - em poucas palavras. Para obter mais detalhes .. leia abaixo

.

TL&DR

Observe um desentupidor e volte para ler os detalhes, caso algum trecho exija mais explicações

.

Explicação detalhada - Angular2 RC6 ++ e componentes de tempo de execução

Abaixo da descrição desse cenário , iremos

  1. criar um módulo PartsModule:NgModule (suporte de pequenos pedaços)
  2. crie outro módulo DynamicModule:NgModule, que conterá nosso componente dinâmico (e fará referência PartsModuledinamicamente)
  3. criar modelo dinâmico (abordagem simples)
  4. crie um novo Componenttipo (somente se o modelo tiver sido alterado)
  5. criar novo RuntimeModule:NgModule. Este módulo conterá o Componenttipo criado anteriormente
  6. ligue JitCompiler.compileModuleAndAllComponentsAsync(runtimeModule)para obterComponentFactory
  7. crie uma Instância do DynamicComponenttrabalho - do espaço reservado Exibir Destino eComponentFactory
  8. atribuir @Inputsa nova instância (alternar de INPUTpara TEXTAREAedição) , consumir@Outputs

NgModule

Nós precisamos de um NgModules.

Embora eu queira mostrar um exemplo muito simples, neste caso, precisaria de três módulos (na verdade 4 - mas não conto o AppModule) . Por favor, tome isso em vez de um trecho simples como base para um gerador de componentes dinâmicos realmente sólido.

Haverá um módulo para todos os componentes pequenos, por exemplo string-editor, text-editor ( date-editor, number-editor...)

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

Onde DYNAMIC_DIRECTIVESsão extensíveis e destinam-se a conter todas as peças pequenas usadas para nosso modelo / modelo dinâmico de componente. Verifique app / parts / parts.module.ts

O segundo será o módulo para o nosso manuseio de material dinâmico. Ele conterá componentes de hospedagem e alguns provedores .. que serão singletons. Para isso, publicaremos de maneira padrão - comforRoot()

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

Verifique o uso do forRoot()noAppModule

Por fim, precisaremos de um módulo adhoc, de tempo de execução ... mas isso será criado posteriormente, como parte do DynamicTypeBuildertrabalho.

O quarto módulo, módulo de aplicativo, é aquele que mantém declara os provedores de compilador:

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

Leia (leia) muito mais sobre o NgModule :

Um construtor de modelos

Em nosso exemplo, processaremos detalhes desse tipo de entidade

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

Para criar um template, neste plunker usamos este construtor simples / ingênuo.

A solução real, um construtor de modelos real, é o local em que seu aplicativo pode fazer muito

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

Um truque aqui é - ele cria um modelo que usa algum conjunto de propriedades conhecidas, por exemplo entity. Essas propriedades devem ser parte do componente dinâmico, que criaremos a seguir.

Para facilitar um pouco, podemos usar uma interface para definir propriedades que nosso construtor de modelos pode usar. Isso será implementado pelo nosso tipo de componente dinâmico.

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

Um ComponentFactoryconstrutor

O mais importante aqui é ter em mente:

nosso tipo de componente, construído com o nosso DynamicTypeBuilder, pode diferir - mas apenas pelo modelo (criado acima) . As propriedades dos componentes (entradas, saídas ou algumas protegidas) ainda são as mesmas. Se precisarmos de propriedades diferentes, definiremos combinações diferentes de Construtor de modelos e tipos

Então, estamos tocando o núcleo da nossa solução. O Construtor 1) criará ComponentType2) criará NgModule3) compilará ComponentFactory4) armazenará em cache para reutilização posterior.

Uma dependência que precisamos receber:

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

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

E aqui está um trecho de como obter 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);
            });
    });
}

Acima, criamos e armazenamos em cache ambos Componente Module. Porque se o modelo (na verdade, a parte dinâmica real de tudo isso) é o mesmo ... podemos reutilizar

E aqui estão dois métodos, que representam a maneira realmente legal de criar classes / tipos decorados em tempo de execução. Não apenas @Componentmas também o@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;
}

Importante:

nossos tipos dinâmicos de componentes diferem, mas apenas por modelo. Então, usamos esse fato para armazená- los em cache . Isso é realmente muito importante. Angular2 também armazenará em cache esses ... pelo tipo . E se recriarmos para o mesmo modelo novas seqüências de caracteres ... começaremos a gerar vazamentos de memória.

ComponentFactory usado hospedando o componente

A peça final é um componente que hospeda o destino do nosso componente dinâmico, por exemplo <div #dynamicContentPlaceHolder></div>. Nós obtemos uma referência a ele e usamos ComponentFactorypara criar um componente. Em poucas palavras, e aqui estão todas as peças desse componente (se necessário, abra o êmbolo aqui )

Vamos primeiro resumir as instruções de importação:

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

Acabamos de receber construtores de modelos e componentes. A seguir estão as propriedades necessárias para o nosso exemplo (mais nos comentários)

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

Nesse cenário simples, nosso componente de hospedagem não possui nenhum @Input. Portanto, não precisa reagir às mudanças. Mas, apesar desse fato (e estar pronto para as próximas mudanças) - precisamos introduzir algum sinalizador se o componente já foi (em primeiro lugar) iniciado. E só então podemos começar a mágica.

Finalmente, usaremos nosso construtor de componentes, e ele é apenas compilado / armazenado em cache ComponentFacotry . Nosso espaço reservado alvo será solicitado para instanciar oComponent com essa fábrica.

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

pequena extensão

Além disso, precisamos manter uma referência ao modelo compilado ... para podermos usá- destroy()lo adequadamente , sempre que o alterarmos.

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

feito

É isso mesmo. Não se esqueça de Destruir qualquer coisa que foi construída dinamicamente (ngOnDestroy) . Além disso, certifique-se de armazenar em cache dinâmico typese modulesse a única diferença é o modelo deles.

Confira tudo em ação aqui

para ver versões anteriores (por exemplo, relacionadas ao RC5) desta publicação, verifique o histórico

Radim Köhler
fonte
50
esta parece uma solução tão complicada, a descontinuada era muito simples e clara, existe outra maneira de fazer isso?
tibbus
3
Penso da mesma maneira que o @tibbus: isso ficou muito mais complicado do que costumava ser com o código obsoleto. Obrigado pela sua resposta, no entanto.
Lucio Mollinedo
5
@ribsies obrigado pela sua observação. Deixe-me esclarecer uma coisa. Muitas outras respostas tentam simplificar . Mas estou tentando explicá-lo e mostrá-lo em um cenário fechado ao uso real . Precisávamos armazenar em cache coisas, teríamos que chamar de destruir na recriação, etc. Então, enquanto a mágica da construção dinâmica está realmente type.builder.tscomo você apontou, eu gostaria que qualquer usuário entendesse como colocar tudo isso em contexto ... espero que isso poderia ser útil;)
Radim Köhler
7
@ Radim Köhler - Eu tentei este exemplo. está funcionando sem AOT. Mas quando tentei fazer isso com o AOT, ele mostra o erro "Nenhum metadado do NgModule encontrado para o RuntimeComponentModule". você pode me ajudar a resolver esse erro?
precisa saber é o seguinte
4
A resposta em si é perfeita! Mas para aplicações da vida real não é viável. A equipe angular deve fornecer uma solução para isso na estrutura, pois isso é um requisito comum em aplicativos de negócios. Caso contrário, é necessário perguntar se o Angular 2 é a plataforma certa para aplicativos de negócios.
194 Karl Karl
58

EDIT (26/08/2017) : A solução abaixo funciona bem com Angular2 e 4. Atualizei-a para conter uma variável de modelo e manipulador de cliques e testei-a com Angular 4.3.
Para Angular4, ngComponentOutlet, conforme descrito na resposta de Ophir, é uma solução muito melhor. Mas, no momento, ele ainda não suporta entradas e saídas . Se [this PR] ( https://github.com/angular/angular/pull/15362] for aceito, seria possível através da instância do componente retornada pelo evento create.
Ng-dynamic-component pode ser o melhor e o mais simples solução completamente, mas ainda não testei isso.

A resposta da @Long Field está no local! Aqui está outro exemplo (síncrono):

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

Ao vivo em http://plnkr.co/edit/fdP9Oc .

Rene Hamburger
fonte
3
Eu diria que é um exemplo de como escrever o menos código possível para fazer o mesmo que na minha resposta stackoverflow.com/a/38888009/1679310 . No caso, deve ser um caso útil (principalmente modelo de geração de RE) quando a condição muda ... a simples ngAfterViewInitchamada com a const templatenão funciona. Mas se a sua tarefa consistia em reduzir a abordagem descrita acima detalhado (criar modelo, criar o componente, criar módulo, compilá-lo, criar fábrica .. criar instância) ... você provavelmente fez isso
Radim Köhler
Obrigado pela solução: Porém, estou tendo problemas para carregar o templateUrl e os estilos, recebo o seguinte erro: Nenhuma implementação do ResourceLoader foi fornecida. Não é possível ler o URL localhost: 3000 / app / pages / pages_common.css , alguma idéia do que estou perdendo?
Gerardlamo
Poderia ser possível compilar o modelo html com dados específicos para célula na grade como controle. plnkr.co/edit/vJHUCnsJB7cwNJr2cCwp?p=preview Neste plunker, como posso compilar e mostrar a imagem na última coluna.? Qualquer ajuda.?
Karthick #
1
@monnef, você está certo. Não verifiquei o log do console. Ajustei o código para adicionar o componente no ngOnInit em vez do gancho ngAfterViewInit, pois o primeiro é acionado antes e o segundo após a detecção de alterações. (Ver github.com/angular/angular/issues/10131 e semelhantes tópicos.)
Rene Hamburger
1
puro e simples. Funcionou como esperado ao servir o navegador no desenvolvedor. Mas isso funciona com o AOT? Quando o aplicativo é executado no PROD após a compilação, recebo um "Erro: O compilador de tempo de execução não está carregado" no momento em que a compilação do componente é tentada. (btw, eu estou usando o Ionic 3.5)
mymo 29/07
52

Devo ter chegado tarde à festa, nenhuma das soluções aqui me pareceu útil - muito bagunçada e parecia uma solução alternativa demais.

O que acabei fazendo é usar Angular 4.0.0-beta.6o ngComponentOutlet .

Isso me deu a solução mais curta e mais simples, tudo escrito no arquivo do componente dinâmico.

  • Aqui está um exemplo simples que apenas recebe texto e o coloca em um modelo, mas obviamente você pode alterar de acordo com sua necessidade:
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;
  }
}
  • Breve explicação:
    1. my-component - o componente no qual um componente dinâmico está renderizando
    2. DynamicComponent - o componente a ser construído dinamicamente e é renderizado dentro do meu componente

Não se esqueça de atualizar todas as bibliotecas angulares para ^ Angular 4.0.0

Espero que isso ajude, boa sorte!

ATUALIZAR

Também funciona para o angular 5.

Ophir Stern
fonte
3
Isso funcionou muito bem para mim com o Angular4. O único ajuste que precisei fazer foi poder especificar módulos de importação para o RuntimeComponentModule criado dinamicamente.
Rahul Patel
8
Aqui está um exemplo rápido a partir da Angular rápido: embed.plnkr.co/9L72KpobVvY14uiQjo4p
Rahul Patel
5
Esta solução funciona com "ng build --prod"? Parece que a classe do compilador e o AoT não se encaixam no atm.
Pierre Chavaroche
2
OOphirStern também descobri que essa abordagem funciona bem no Angular 5, mas NÃO com o sinalizador --prod build.
TaeKwonJoe 26/02
2
Eu testei com angular 5 (5.2.8) usando o JitCompilerFactory e usando o sinalizador --prod não funciona! Alguém tem uma solução? (BTW JitCompilerFactory sem o sinalizador --prod funciona perfeitamente)
Frank
20

2019 junho resposta

Boas notícias! Parece que o pacote @ angular / cdk agora tem suporte de primeira classe para portais !

Até o momento da redação deste artigo, eu não achava os documentos oficiais acima particularmente úteis (principalmente no que diz respeito ao envio de dados e ao recebimento de eventos dos componentes dinâmicos). Em resumo, você precisará:

Etapa 1) Atualize seu AppModule

Importe PortalModuledo @angular/cdk/portalpacote e registre seu (s) componente (s) dinâmico (s) dentroentryComponents

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

Etapa 2. Opção A: Se você NÃO precisar passar dados e receber eventos de seus componentes dinâmicos :

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

Veja em ação

Etapa 2. Opção B: Se você precisar passar dados e receber eventos de seus componentes dinâmicos :

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

Veja em ação

Stephen Paul
fonte
1
Cara, você acabou de acertar. Este vai chamar a atenção. Eu não podia acreditar no quão difícil é adicionar um componente dinâmico simples no Angular até precisar fazer um. É como redefinir e voltar aos tempos pré-JQuery.
Gi1ber7 6/02/19
2
@ Gi1ber7 eu sei né? Por que eles demoraram tanto tempo?
Stephen Paul
1
Boa abordagem, mas você sabe como passar parâmetros para ChildComponent?
Snook
1
@Snook isso pode responder a sua pergunta stackoverflow.com/questions/47469844/...
Stephen Paul
4
@StephenPaul Como essa Portalabordagem difere ngTemplateOutlete ngComponentOutlet? Gl
Glenn Mohammad
18

Decidi compactar tudo o que aprendi em um arquivo . Há muito o que fazer aqui, especialmente em comparação com antes do RC5. Observe que esse arquivo de origem inclui o AppModule e 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
fonte
10

Eu tenho um exemplo simples para mostrar como fazer componente dinâmico angular 2 rc6.

Digamos, você tem um template html dinâmico = template1 e deseja carregar dinamicamente, primeiro empacote no componente

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

aqui template1 como html, pode estar contém componente ng2

Na rc6, é necessário que o @NgModule envolva esse componente. O @NgModule, assim como o módulo no anglarJS 1, desacopla diferentes partes da aplicação ng2, portanto:

@Component({
  template: template1,

})
class DynamicComponent {

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

(Aqui importe o RouterModule, como no meu exemplo, existem alguns componentes de rota no meu html, como você pode ver mais adiante)

Agora você pode compilar o DynamicModule como: this.compiler.compileModuleAndAllComponentsAsync(DynamicModule).then( factory => factory.componentFactories.find(x => x.componentType === DynamicComponent))

E precisamos colocar acima em app.moudule.ts para carregá-lo, consulte meu app.moudle.ts. Para obter mais detalhes, consulte: https://github.com/Longfld/DynamicalRouter/blob/master/app/MyRouterLink.ts e app.moudle.ts

e veja a demonstração: http://plnkr.co/edit/1fdAYP5PAbiHdJfTKgWo?p=preview

Campo longo
fonte
3
Então, você declarou módulo1, módulo2, módulo3. E se você precisar de outro conteúdo de modelo "dinâmico", precisará criar um formulário de definição (arquivo) moudle4 (module4.ts), certo? Se sim, isso não parece ser dinâmico. É estático, não é? Ou eu sinto falta de alguma coisa?
Radim Köhler
Em cima "template1" é uma string de html, você pode colocar qualquer coisa nele e nós chamamos este modelo dinâmico, pois esta questão está pedindo
Longa Campo
6

No angular 7.x, usei elementos angulares para isso.

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

  2. Crie serviço acessório.

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

Observe que a tag do elemento personalizado deve ser diferente com o seletor de componente angular. em AppUserIconComponent:

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

e, nesse caso, o nome da tag personalizada, usei "user-icon".

  1. Então você deve ligar para registrar no AppComponent:
@Component({
  selector: 'app-root',
  template: '<router-outlet></router-outlet>'
})
export class AppComponent {
  constructor(   
    dynamicComponents: DynamicComponentsService,
  ) {
    dynamicComponents.register();
  }

}
  1. E agora, em qualquer lugar do seu código, você pode usá-lo assim:
dynamicComponents.create('user-icon', {user:{...}});

ou assim:

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

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

(no modelo):

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

Observe que, no segundo caso, você deve passar objetos com JSON.stringify e depois analisá-lo novamente. Não consigo encontrar uma solução melhor.

Oleg Pnk
fonte
Interresting abordagem, mas você vai precisar para atingir es2015 (de modo nenhum suporte para IE11) no seu tsconfig.json, othewise vai falhou emdocument.createElement(tagName);
Snook
Oi, como você mencionou uma maneira de lidar com entradas, as saídas de componentes filhos também podem ser manipuladas dessa maneira?
Mustahsan
5

Resolvido isso na versão final do Angular 2 simplesmente usando a diretiva dynamicComponent da ng-dynamic .

Uso:

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

Onde template é seu modelo dinâmico, o contexto pode ser definido como qualquer modelo de dados dinâmico ao qual você deseja que seu modelo se ligue.

Richard Houltz
fonte
No momento da gravação, o Angular 5 com AOT não suporta isso, pois o compilador JIT não está incluído no pacote. Sem AOT ele funciona como um encanto :)
Richard Houltz
isso ainda se aplica ao angular 7+?
Carlos E
4

Quero acrescentar alguns detalhes sobre este excelente post de Radim.

Peguei essa solução e trabalhei nela um pouco e rapidamente encontrei algumas limitações. Vou apenas descrevê-las e depois dar a solução para isso também.

  • Antes de tudo, não consegui renderizar os detalhes dinâmicos dentro de um detalhe dinâmico (basicamente aninhamos interfaces dinâmicas entre si).
  • O próximo problema era que eu queria renderizar detalhes dinâmicos em uma das partes disponibilizadas na solução. Isso também não foi possível com a solução inicial.
  • Por fim, não foi possível usar URLs de modelo nas partes dinâmicas, como o editor de cadeias.

Fiz outra pergunta com base neste post, sobre como obter essas limitações, que podem ser encontradas aqui:

compilação dinâmica de modelo recursivo em angular2

Vou descrever as respostas para essas limitações, caso você tenha o mesmo problema que eu, pois isso torna a solução muito mais flexível. Seria incrível ter o plunker inicial atualizado com isso também.

Para habilitar o aninhamento de detalhes dinâmicos, você precisará adicionar DynamicModule.forRoot () na instrução de importação no 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;
}

Além disso, não era possível usar <dynamic-detail>dentro de uma das partes o editor de string ou o editor de texto.

Para permitir que você precise alterar parts.module.tsedynamic.module.ts

Dentro parts.module.tsVocê precisará adicionar DynamicDetailoDYNAMIC_DIRECTIVES

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

Além disso, dynamic.module.tsvocê teria que remover o dynamicDetail, pois agora eles fazem parte das partes

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

Um plunker modificado em funcionamento pode ser encontrado aqui: http://plnkr.co/edit/UYnQHF?p=preview (Eu não resolvi esse problema, sou apenas o mensageiro :-D)

Por fim, não foi possível usar templatesurls nas peças criadas nos componentes dinâmicos. Uma solução (ou solução alternativa. Não sei se é um bug angular ou o uso incorreto da estrutura) foi criar um compilador no construtor em vez de injetá-lo.

    private _compiler;

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

Em seguida, use o _compilerpara compilar, e templateUrls também serão ativados.

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

                this._cacheOfFactories[template] = factory;

                resolve(factory);
            });
    });

Espero que isso ajude alguém!

Atenciosamente Morten

Morten Skjoldager
fonte
4

Seguindo a excelente resposta de Radmin, é necessário um pequeno ajuste para todos que estão usando o angular-cli versão 1.0.0-beta.22 e superior.

COMPILER_PROVIDERSnão pode mais ser importado (para obter detalhes, consulte angular-cli GitHub ).

Portanto, a solução alternativa é não usar COMPILER_PROVIDERSe JitCompilerna providersseção, mas usar JitCompilerFactory'@ angular / compiler' como este dentro da classe do construtor de tipos:

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

Como você pode ver, não é injetável e, portanto, não tem dependências com o DI. Essa solução também deve funcionar para projetos que não usam angular-cli.

Sebastian
fonte
1
Obrigado por esta sugestão, no entanto, estou executando "Nenhum metadado NgModule encontrado para 'DynamicHtmlModule'". Minha implementação é baseada em stackoverflow.com/questions/40060498/…
Cybey 15/02
2
Alguém tem JitCompiletFactory trabalhando com amostra AOT? Eu tenho o mesmo erro que @Cybey
user2771738 2/17/17
2

Eu mesmo estou tentando ver como atualizar o RC4 para o RC5 e, portanto, me deparei com essa entrada e uma nova abordagem para a criação dinâmica de componentes ainda me traz um pouco de mistério, então não vou sugerir nada sobre o resolvedor de fábrica de componentes.

Mas, o que posso sugerir é uma abordagem um pouco mais clara da criação de componentes nesse cenário - basta usar a opção no modelo que criaria o editor de strings ou o editor de texto de acordo com alguma condição, como esta:

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

E, a propósito, "[" na expressão [prop] tem um significado, isso indica uma ligação de dados unidirecional; portanto, você pode e deve omitir esses, caso saiba que não precisa vincular propriedade à variável.

zii
fonte
1
Esse seria um caminho a percorrer .. se o switch/ casecontiver poucas decisões. Mas imagine que o modelo gerado possa ser realmente grande ... e diferir para cada entidade, diferir por segurança, diferir por status da entidade, por cada tipo de propriedade (número, data, referência ... editores) ... Nesse caso, resolver isso no modelo html com ngSwitchcriaria um htmlarquivo muito, muito muito grande .
Radim Köhler
Oh, eu concordo com você. Eu tenho esse tipo de cenário aqui, agora, enquanto estou tentando carregar os principais componentes do aplicativo sem saber antes da compilação que determinada classe será exibida. Embora esse caso em particular não precise da criação de componentes dinâmicos.
zii 11/08/16
1

Este é o exemplo dos controles dinâmicos de formulário gerados no servidor.

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

Este exemplo é dinâmico. Os controles de formulário estão no componente add (é aqui que você pode obter os controles de formulário do servidor). Se você vir o método addcomponent, poderá ver os controles de formulários. Neste exemplo, não estou usando material angular, mas funciona (estou usando @ work). Este é o alvo para o angular 6, mas funciona em todas as versões anteriores.

É necessário adicionar JITComplierFactory para AngularVersion 5 e superior.

obrigado

Vijay

Vijay Anand Kannan
fonte
0

Nesse caso específico, parece que usar uma diretiva para criar dinamicamente o componente seria uma opção melhor. Exemplo:

No HTML em que você deseja criar o componente

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

Eu abordaria e projetaria a diretiva da seguinte maneira.

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

Portanto, em seus componentes, texto, string, data, o que for - qualquer que seja a configuração que você passou no HTML no ng-containerelemento, estaria disponível.

A configuração,, yourConfigpode ser a mesma e definir seus metadados.

Dependendo da configuração ou do tipo de entrada, a diretiva deve agir de acordo e dos tipos suportados, renderizará o componente apropriado. Caso contrário, ele registrará um erro.

saidutt
fonte
-1

Com base na resposta de Ophir Stern, aqui está uma variante que funciona com o AoT no Angular 4. O único problema que tenho é que não posso injetar nenhum serviço no DynamicComponent, mas posso conviver com isso.

nota: eu não testei com o 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;

  }

}

Espero que isto ajude.

Felicidades!

Spitfire
fonte