web-dev-qa-db-fra.com

Comment utiliser / créer un modèle dynamique pour compiler un composant dynamique avec Angular 2.0?

Je veux dynamiquement créer un modèle . Cela devrait être utilisé pour construire un ComponentType à Runtime et le placer (même le remplacer) quelque part à l'intérieur de le composant d'hébergement.

Jusqu'au RC4, j'utilisais ComponentResolver, mais avec le RC5, je reçois le message suivant:

ComponentResolver est obsolète pour la compilation dynamique. Utilisez plutôt ComponentFactoryResolver avec le fournisseur @NgModule/@Component.entryComponents ou ANALYZE_FOR_ENTRY_COMPONENTS. Pour la compilation à l'exécution uniquement , vous pouvez également utiliser Compiler.compileComponentSync/Async.

J'ai trouvé ce document (offical angular2)

Création de composants dynamiques synchrones angulaires 2

Et comprenez que je peux utiliser soit

  • Type de dynamique ngIf avec ComponentFactoryResolver. Si je transmettrai des composants connus à l'hébergement d'un élément de @Component({entryComponents: [comp1, comp2], ...}) - je peux utiliser .resolveComponentFactory(componentToRender);
  • Real Runtime Compilation, avec Compiler...

Mais la question est de savoir comment utiliser cela Compiler? La note ci-dessus indique que je devrais appeler: Compiler.compileComponentSync/Async - alors comment?

Par exemple. Je veux créer (en fonction de certaines conditions de configuration) ce type de modèle pour un type de paramètres

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

et dans un autre cas celui-ci (string-editor est remplacé par text-editor)

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

Et ainsi de suite (numéro/date/référence différent editors selon les types de propriétés, certaines propriétés ont été ignorées pour certains utilisateurs ...) . C'est à dire. Ceci est un exemple, une configuration réelle pourrait générer des modèles beaucoup plus différents et complexes.

Le modèle est en train de changer , je ne peux donc pas utiliser ComponentFactoryResolver et transmettre les modèles existants ... J'ai besoin d'une solution avec le Compiler


AOT et JitCompiler (ancien RuntimeCompiler)

Souhaitez-vous utiliser ces fonctionnalités avec AOT (compilation anticipée)? Obtenez-vous:

Erreur: Une erreur s'est produite lors de la résolution statique des valeurs de symbole. Les appels de fonction ne sont pas pris en charge. Envisagez de remplacer la fonction ou lambda par une référence à une fonction exportée (position 65:17 dans le fichier .ts original), en résolvant le symbole COMPILER_PROVIDERS dans .../node_modules/@angular/compiler/src/compiler.d.ts,

S'il vous plaît, laissez votre commentaire, votez ici:

Est-ce que le code utilisant COMPILER_PROVIDERS est pris en charge par AOT?

175
Radim Köhler

EDIT - lié à 2.3. (2016-12-07)

REMARQUE: pour obtenir la solution de la version précédente, consultez l'historique de ce message

Un sujet similaire est traité ici équivalent de $ compile in Angular 2 . Nous devons utiliser JitCompiler et NgModule. En savoir plus surNgModulein Angular2 ici:

En un mot

Il y a n plunker/exemple de travail (modèle dynamique, type de composant dynamique, module dynamique, JitCompiler, ... en action)

Le principal est:
1) créer un modèle
2) trouver ComponentFactory dans le cache - aller à 7)
3) - créez Component
4) - Créez Module
5) - Compilez Module
6) - renvoie (et cache pour une utilisation ultérieure) ComponentFactory
7) utiliser Target et ComponentFactory pour créer une instance de dynamique Component

Voici un extrait de code (plusieurs - ici ) - Notre générateur personnalisé renvoie le fichier nouvellement construit/mis en cache ComponentFactory et la vue que l’espace réservé cible consomme pour créer une instance de 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;
        //...
    });

C'est ça - en bref, ça. Pour obtenir plus de détails .. lire ci-dessous

.

TL & DR

Observez un plunker et revenez pour lire les détails au cas où un extrait demanderait plus d'explications

.

Explication détaillée - Angular2 RC6 ++ & composants d'exécution

Ci-dessous la description de ce scénario , nous allons

  1. créer un module PartsModule:NgModule (détenteur de petites pièces)
  2. créer un autre module DynamicModule:NgModule, qui contiendra notre composant dynamique (et référencé PartsModule dynamiquement)
  3. créer un modèle dynamique (approche simple)
  4. créer un nouveau Component type (uniquement si le modèle a été modifié)
  5. créer un nouveau RuntimeModule:NgModule. Ce module contiendra le type Component précédemment créé
  6. appelez JitCompiler.compileModuleAndAllComponentsAsync(runtimeModule) pour obtenir ComponentFactory
  7. créer une instance du travail DynamicComponent - de l'espace réservé Vue cible et de ComponentFactory
  8. assigne @Inputs à nouvelle instance (bascule de INPUT à TEXTAREA édition) , consomme @Outputs

NgModule

Nous avons besoin d'un NgModules.

Bien que j'aimerais montrer un exemple très simple, dans ce cas, il me faudrait trois modules (en fait 4 - mais je ne compte pas le module d'application) . S'il vous plaît, prenez ceci plutôt qu'un simple extrait comme base pour un générateur de composants dynamiques vraiment solide.

Il y aura un module pour tous les petits composants, par exemple. string-editor, text-editor (date-editor, number-editor...)

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

DYNAMIC_DIRECTIVES sont extensibles et destinés à contenir toutes les petites pièces utilisées pour notre modèle/type de composant dynamique. Vérifiez app/parts/parts.module.ts

Le second sera un module pour notre traitement dynamique des commandes. Il contiendra des composants d'hébergement et quelques fournisseurs .. qui seront des singletons. Pour cela nous les publierons de manière standard - avec forRoot()

import { DynamicDetail }          from './detail.view';
import { DynamicTypeBuilder }     from './type.builder';
import { DynamicTemplateBuilder } from './template.builder';

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

export class DynamicModule {

    static forRoot()
    {
        return {
            ngModule: DynamicModule,
            providers: [ // singletons accross the whole app
              DynamicTemplateBuilder,
              DynamicTypeBuilder
            ], 
        };
    }
}

Vérifiez l'utilisation de forRoot() dans le AppModule

Enfin, nous aurons besoin d’un module d’exécution, ad hoc, mais qui sera créé ultérieurement, dans le cadre du travail DynamicTypeBuilder.

Le quatrième module, le module d'application, est celui qui conserve les fournisseurs de compilateur déclarés:

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

Lisez (lisez) beaucoup plus à propos de NgModule il:

A template constructeur

Dans notre exemple, nous traiterons les détails de ce type d’entité entité

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

Pour créer un template, dans ce plunker , nous utilisons ce générateur simple/naïf.

La vraie solution, un vrai constructeur de templates, est l'endroit où votre application peut faire beaucoup

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

Une astuce consiste à créer un modèle qui utilise un ensemble de propriétés connues, par exemple. entity. Cette propriété (-ies) doit faire partie du composant dynamique, que nous allons créer ensuite.

Pour simplifier un peu les choses, nous pouvons utiliser une interface pour définir les propriétés que notre générateur de modèles peut utiliser. Ceci sera implémenté par notre type de composant dynamique.

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

Un constructeur ComponentFactory

La chose la plus importante ici est de garder à l'esprit:

notre type de composant, construit avec notre DynamicTypeBuilder, peut différer - mais uniquement par son modèle (créé ci-dessus) . Les propriétés des composants (entrées, sorties ou certains protégés) sont toujours les mêmes. Si nous avons besoin de propriétés différentes, nous devrions définir une combinaison différente de Template et Type Builder

Nous touchons donc le cœur de notre solution. Le constructeur va 1) créer ComponentType 2) créer son NgModule 3) compiler ComponentFactory 4) le mettre en cache pour le réutiliser ultérieurement.

Une dépendance à recevoir:

// plunker - app/dynamic/type.builder.ts
import { JitCompiler } from '@angular/compiler';

@Injectable()
export class DynamicTypeBuilder {

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

Et voici un extrait comment obtenir un 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);
            });
    });
}

Ci-dessus, nous créons et mettons en cache les deux Component et Module. Parce que si le template (en fait, la partie dynamique réelle de tout cela) est le même .. nous pouvons le réutiliser

Et voici deux méthodes, qui représentent la manière vraiment cool de créer un déjà décoré classes/types en runtime. Non seulement @Component mais aussi le @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;
}

Important:

nos types de composants dynamiques diffèrent, mais juste par modèle. Nous utilisons donc ce fait pour les mettre en cache . C'est vraiment très important. Angular2 mettra également en cache ces .. par le type . Et si nous recréions pour le même modèle de chaînes de modèles de nouveaux types ... nous commencerions à générer des fuites de mémoire.

ComponentFactory utilisé par le composant d'hébergement

Le dernier élément est un composant qui héberge la cible de notre composant dynamique, par exemple. <div #dynamicContentPlaceHolder></div>. Nous y obtenons une référence et utilisons ComponentFactory pour créer un composant. En résumé, voici toutes les pièces de ce composant (si nécessaire, ouvrez plunker here )

Résumons d’abord les instructions d’importation:

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

Nous venons de recevoir des générateurs de modèles et de composants. Viennent ensuite les propriétés nécessaires à notre exemple (plus dans les commentaires)

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

Dans ce scénario simple, notre composant d'hébergement n'a pas de @Input. Donc, il ne doit pas réagir aux changements. Mais malgré ce fait (et pour être prêt pour les changements à venir) - nous devons introduire un indicateur si le composant était déjà (premièrement) initié. Et alors seulement nous pourrons commencer la magie.

Enfin, nous allons utiliser notre générateur de composants, et son vient d'être compilé/mis en cache ComponentFacotry. Notre espace réservé cible sera invité à instancier Component avec cette fabrique.

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

petite extension

De plus, nous devons garder une référence au modèle compilé. Pour pouvoir le destroy() le bien, à chaque fois que nous le changerons.

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

terminé

C'est à peu près ça. N'oubliez pas de détruire tout ce qui a été construit dynamiquement (ngOnDestroy) . Assurez-vous également de mettre en cache les types et modules dynamiques si la seule différence est leur modèle.

Vérifiez tout en action ici

pour voir les versions précédentes (exemple: lié à RC5) de ce post, consultez le historique

152
Radim Köhler

EDIT (26/08/2017) : La solution ci-dessous fonctionne bien avec Angular2 et 4. Je l'ai mise à jour pour contenir une variable de modèle et un gestionnaire de clic. et l'a testé avec Angular 4.3.
Pour Angular4, ngComponentOutlet tel que décrit dans réponse d'Ophir est une bien meilleure solution. Mais pour le moment ne supporte pas les entrées et sorties encore. Si [ce PR] ( https://github.com/angular/angular/pull/15362] est accepté, cela serait possible via l'instance de composant renvoyée par l'événement create.
ng-dynamic-composant est peut-être la solution la plus simple et la meilleure, mais je ne l’ai pas encore testée.

La réponse de @Long Field est parfaite! Voici un autre exemple (synchrone):

import {Compiler, Component, NgModule, OnInit, ViewChild,
  ViewContainerRef} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'

@Component({
  selector: 'my-app',
  template: `<h1>Dynamic template:</h1>
             <div #container></div>`
})
export class App implements OnInit {
  @ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef;

  constructor(private compiler: Compiler) {}

  ngOnInit() {
    this.addComponent(
      `<h4 (click)="increaseCounter()">
        Click to increase: {{counter}}
      `enter code here` </h4>`,
      {
        counter: 1,
        increaseCounter: function () {
          this.counter++;
        }
      }
    );
  }

  private addComponent(template: string, properties?: any = {}) {
    @Component({template})
    class TemplateComponent {}

    @NgModule({declarations: [TemplateComponent]})
    class TemplateModule {}

    const mod = this.compiler.compileModuleAndAllComponentsSync(TemplateModule);
    const factory = mod.componentFactories.find((comp) =>
      comp.componentType === TemplateComponent
    );
    const component = this.container.createComponent(factory);
    Object.assign(component.instance, properties);
    // If properties are changed at a later stage, the change detection
    // may need to be triggered manually:
    // component.changeDetectorRef.detectChanges();
  }
}

@NgModule({
  imports: [ BrowserModule ],
  declarations: [ App ],
  bootstrap: [ App ]
})
export class AppModule {}

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

53
Rene Hamburger

Je devais être arrivé à la soirée tardivement, aucune des solutions ici ne me paraissait utile - trop en désordre et semblait trop complexe.

Ce que j'ai fini par faire, c'est d'utiliser Angular 4.0.0-beta.6's ngComponentOutlet .

Cela m'a donné la solution la plus courte et la plus simple, inscrite dans le fichier du composant dynamique.

  • Voici un exemple simple qui reçoit simplement du texte et le place dans un modèle, mais vous pouvez évidemment le modifier en fonction de vos besoins:
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;
  }
}
  • Explication brève:
    1. my-component - le composant dans lequel un composant dynamique est rendu
    2. DynamicComponent - le composant à construire de manière dynamique et restitué dans my-composant

N'oubliez pas de mettre à jour toutes les bibliothèques angulaires vers ^ Angular 4.0.0

En espérant que ça aide, bonne chance!

UPDATE

Fonctionne également pour angular 5.

47
Ophir Stern

J'ai décidé de compacter tout ce que j'ai appris dans un seul fichier. Il y a beaucoup à prendre ici surtout par rapport à avant RC5. Notez que ce fichier source inclut AppModule et 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 {}`
16
Stephen Paul

J'ai un exemple simple pour montrer comment faire un composant dynamique angulaire 2 rc6.

Disons que vous avez un template html dynamique = template1 et que vous souhaitez un chargement dynamique, tout d’abord intégré dans le composant

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

ici template1 en html, peut contenir un composant ng2

De rc6, vous devez avoir @NgModule pour envelopper ce composant. @NgModule, tout comme le module d'anglarJS 1, il découple différentes parties de l'application ng2, donc:

@Component({
  template: template1,

})
class DynamicComponent {

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

(Ici, importez RouterModule comme dans mon exemple, il y a quelques composants de route dans mon HTML comme vous pourrez le voir plus tard)

Vous pouvez maintenant compiler DynamicModule en tant que: this.compiler.compileModuleAndAllComponentsAsync(DynamicModule).then( factory => factory.componentFactories.find(x => x.componentType === DynamicComponent))

Et nous avons besoin de mettre ci-dessus dans app.moudule.ts pour le charger, s'il vous plaît voir mon app.moudle.ts. Pour plus d'informations, consultez: https://github.com/Longfld/DynamicalRouter/blob/master/app/MyRouterLink.ts et app.moudle.ts

et voir la démo: http://plnkr.co/edit/1fdAYP5PAbiHdJfTKgWo?p=preview

9
Long Field

Réponse de juin 2019

Bonne nouvelle! Il semble que le package @ angular/cdk dispose désormais d'un support de première classe pour portails !

Au moment de la rédaction de ce document, je n'ai pas trouvé les documents officiels ci-dessus particulièrement utiles (notamment en ce qui concerne l'envoi de données et la réception d'événements à partir des composants dynamiques). En résumé, vous devrez:

Étape 1) Mettez à jour votre AppModule

Importez PortalModule à partir du package cdk et enregistrez votre ou vos composants dynamiques à l'intérieur de entryComponents

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

Étape 2. Option A: Si vous n'avez PAS besoin de transmettre des données à et de recevoir des événements de vos composants dynamiques :

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

Voir en action

Étape 2. Option B: Si vous DEVEZ transmettre des données et recevoir des événements de vos composants dynamiques :

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

Voir en action

9
Stephen Paul

Résolu ceci dans Angular 2 Version finale simplement en utilisant la directive dynamicComponent de ng-dynamic .

Usage:

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

Où template est votre modèle dynamique et le contexte peut être défini sur tout modèle de données dynamique auquel vous souhaitez associer votre modèle.

5
Richard Houltz

Pour faire suite à l'excellente réponse de Radmin, un petit Tweak est nécessaire pour tous ceux qui utilisent angular-cli version 1.0.0-beta.22 ou supérieure.

COMPILER_PROVIDERS ne peut plus être importé (pour plus de détails, voir angular-cli GitHub ).

La solution de contournement consiste donc à ne pas utiliser COMPILER_PROVIDERS et JitCompiler dans la section providers, mais à utiliser plutôt JitCompilerFactory à partir de '@ angular/compiler' dans la classe de constructeur de types:

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

Comme vous pouvez le constater, il n’est pas injectable et n’a donc aucune dépendance avec l’ID. Cette solution devrait également fonctionner pour des projets n'utilisant pas angular-cli.

4
Sebastian

Je souhaite ajouter quelques détails en plus de cet excellent article de Radim.

J'ai pris cette solution, y ai travaillé un peu et j'ai rapidement rencontré quelques limitations. Je vais simplement les décrire, puis donner la solution à cela également.

  • Tout d'abord, je ne pouvais pas rendre le détail dynamique à l'intérieur d'un détail dynamique (imbriquer des interfaces utilisateur dynamiques les unes dans les autres).
  • Le problème suivant était que je souhaitais rendre un détail dynamique dans l'une des pièces mises à disposition dans la solution. Ce n'était pas possible avec la solution initiale non plus.
  • Enfin, il n'était pas possible d'utiliser des URL de modèle sur les parties dynamiques comme l'éditeur de chaînes.

J'ai créé une autre question basée sur ce post, sur la manière d'atteindre ces limitations, que vous pouvez trouver ici:

compilation de modèles dynamiques récursifs dans angular2

Je vais juste exposer les réponses à ces limitations, si vous rencontrez le même problème que moi, car cela rend la solution plus flexible. Il serait génial de mettre à jour le premier plunker avec cela également.

Pour permettre l'imbrication de détails dynamiques imbriqués l'un dans l'autre, vous devez ajouter DynamicModule.forRoot () dans l'instruction d'importation du type 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;
}

De plus, il n’était pas possible d’utiliser <dynamic-detail> dans l’une des parties sous forme d’éditeur de chaîne ou de texte.

Pour l'activer, vous devrez changer parts.module.ts et dynamic.module.ts

Dans parts.module.ts Vous devrez ajouter DynamicDetail dans le DYNAMIC_DIRECTIVES

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

De plus, dans le dynamic.module.ts, vous devrez supprimer dynamicDetail car ils font maintenant partie des pièces.

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

Un plunker modifié peut être trouvé ici: http://plnkr.co/edit/UYnQHF?p=preview (Je n'ai pas résolu ce problème, je ne suis que le messager :-D)

Enfin, il n’a pas été possible d’utiliser des modèles dans les pièces créées sur les composants dynamiques. Une solution (ou une solution de contournement. Je ne sais pas s'il s'agit d'un bogue angulaire ou d'une mauvaise utilisation du framework) était de créer un compilateur dans le constructeur au lieu de l'injecter.

    private _compiler;

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

Ensuite, utilisez le _compiler pour compiler, puis templateUrls est également activé.

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

                this._cacheOfFactories[template] = factory;

                resolve(factory);
            });
    });

J'espère que ceci aide quelqu'un d'autre!

Cordialement Morten

4

Dans angular 7.x, j'ai utilisé des éléments angulaires pour cela.

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

  2. Créer un service accessoire.

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

Notez que votre balise d'élément personnalisé doit être différente avec le sélecteur de composant angulaire. dans AppUserIconComponent:

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

et dans ce cas, le nom de la balise personnalisée j'ai utilisé "user-icon".

  1. Ensuite, vous devez appeler enregistrer dans AppComponent:
@Component({
  selector: 'app-root',
  template: '<router-outlet></router-outlet>'
})
export class AppComponent {
  constructor(   
    dynamicComponents: DynamicComponentsService,
  ) {
    dynamicComponents.register();
  }

}
  1. Et maintenant, n'importe où dans votre code, vous pouvez l'utiliser comme ceci:
dynamicComponents.create('user-icon', {user:{...}});

ou comme ceci:

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

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

(dans le modèle):

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

Notez que dans le second cas, vous devez transmettre des objets avec JSON.stringify, puis les analyser à nouveau. Je ne peux pas trouver de meilleure solution.

4
Oleg Pnk

J'essaie moi-même de voir comment mettre à jour RC4 vers RC5. Je suis donc tombé par hasard sur cette entrée. Une nouvelle approche de la création de composants dynamiques reste encore un peu mystérieuse. Je ne suggère donc rien sur le résolveur d'usine de composants.

Mais ce que je peux suggérer, c’est une approche un peu plus claire de la création de composants sur ce scénario. Il suffit d’utiliser un commutateur dans un modèle qui créerait un éditeur de chaîne ou un éditeur de texte en fonction de certaines conditions, comme ceci:

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

Et au fait, "[" dans [prop] expression a une signification, cela indique une liaison de données, vous pouvez donc et même les omettre si vous savez que vous n'avez pas besoin de lier une propriété à une variable.

2
zii

Voici l'exemple des contrôles de formulaire dynamiques générés à partir du serveur.

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

Cet exemple est dynamique Les contrôles de formulaire sont dans le composant add (c'est ici que vous pouvez obtenir les contrôles de formulaire du serveur). Si vous voyez la méthode addcomponent, vous pouvez voir les contrôles de formulaire. Dans cet exemple, je n'utilise pas de matériau angulaire, mais cela fonctionne (j'utilise @ work). Ceci est ciblé sur 6 angulaire, mais fonctionne dans toutes les versions précédentes.

Besoin d'ajouter JITComplierFactory pour AngularVersion 5 et versions supérieures.

Merci

Vijay

1

Pour ce cas particulier, utiliser une directive pour créer dynamiquement le composant serait une meilleure option. Exemple:

Dans le HTML où vous voulez créer le composant

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

Je voudrais aborder et concevoir la directive de la manière suivante.

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

Ainsi, dans vos composants, le texte, la chaîne, la date, peu importe, quelle que soit la configuration que vous avez transmise dans le code HTML dans l'élément ng-container serait disponible.

La configuration, yourConfigname__, peut être identique et définir vos métadonnées.

En fonction de votre configuration ou de votre type d’entrée, la directive doit agir en conséquence et, à partir des types pris en charge, elle rendra le composant approprié. Sinon, cela enregistrera une erreur.

0
saidutt