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


197

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

Jusqu'au RC4 que j'utilisais ComponentResolver, mais avec RC5 j'obtiens le message suivant:

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.

J'ai trouvé ce document ( Création de composant dynamique synchrone angulaire 2 )

Et je comprends que je peux utiliser soit

  • Type de dynamique ngIfavec ComponentFactoryResolver. Si je passe des composants connus à l'intérieur de @Component({entryComponents: [comp1, comp2], ...})- je peux utiliser.resolveComponentFactory(componentToRender);
  • Véritable compilation d'exécution, avec Compiler...

Mais la question est de savoir comment utiliser cela Compiler? La note ci-dessus dit 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-editorest remplacé par text-editor)

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

Et ainsi de suite (nombre / date / référence différent editorsselon les types de propriétés, ignoré certaines propriétés pour certains utilisateurs ...) . c'est-à-dire qu'il s'agit d'un exemple, la configuration réelle pourrait générer des modèles beaucoup plus différents et complexes.

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


Étant donné que la solution que j'ai trouvée était si agréable, je veux que tout le monde qui trouve cette question jette un œil à ma réponse, qui est très loin en bas pour le moment. :)
Richard Houltz

L'article Voici ce que vous devez savoir sur les composants dynamiques dans Angular a une grande explication des composants dynamiques.
Max Koretskyi

Voici le problème avec chaque réponse et ce qui $compilepourrait réellement faire que ces méthodes ne peuvent pas - je crée une application où je veux simplement compiler le code HTML tel qu'il arrive via une page tierce et des appels ajax. Je ne peux pas supprimer le code HTML de la page et le placer dans mon propre modèle. Soupir
Augie Gardner

@AugieGardner Il y a une raison pour laquelle cela n'est pas possible par conception. Angular n'est pas responsable des mauvaises décisions architecturales ou des systèmes hérités que certaines personnes ont. Si vous souhaitez analyser le code HTML existant, vous êtes libre d'utiliser un autre framework car Angular fonctionne parfaitement avec WebComponents. Définir des limites claires pour guider les hordes de programmeurs inexpérimentés est plus important que d'autoriser des hacks sales pour quelques systèmes hérités.
Phil

Réponses:


163

EDIT - lié à 2.3.0 (2016-12-07)

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

Un sujet similaire est discuté ici Equivalent de $ compile dans Angular 2 . Nous devons utiliser JitCompileret NgModule. En savoir plus sur NgModuleAngular2 ici:

En un mot

Il existe un plunker / exemple fonctionnel (modèle dynamique, type de composant dynamique, module dynamique ,, ... en action)JitCompiler

Le principe est:
1) créer un modèle
2) trouver ComponentFactorydans le cache - aller à 7)
3) - créer Component
4) - créer Module
5) - compiler Module
6) - retourner (et mettre en cache pour une utilisation ultérieure) ComponentFactory
7) utiliser Target et ComponentFactorycréer une instance de dynamiqueComponent

Voici un extrait de code (plus ici ) - Notre générateur personnalisé retourne juste construit / mis en cache ComponentFactoryet l'espace réservé de vue cible consomme pour créer une instance de laDynamicComponent

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

Ça y est - en bref. Pour obtenir plus de détails .. lire ci-dessous

.

TL&DR

Observez un plongeur et revenez pour lire les détails au cas où un extrait nécessiterait plus d'explications

.

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

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

  1. créer un module PartsModule:NgModule (support de petites pièces)
  2. créer un autre module DynamicModule:NgModule, qui contiendra notre composant dynamique (et référencera PartsModuledynamiquement)
  3. créer un modèle dynamique (approche simple)
  4. créer un nouveau Componenttype (uniquement si le modèle a changé)
  5. créer de nouveaux RuntimeModule:NgModule. Ce module contiendra le Componenttype précédemment créé
  6. appeler JitCompiler.compileModuleAndAllComponentsAsync(runtimeModule)pour obtenirComponentFactory
  7. créer une instance de la DynamicComponenttâche - de l'espace réservé View Target etComponentFactory
  8. attribuer @Inputsà nouvelle instance (commutateur de INPUTde TEXTAREAmontage) , à consommer@Outputs

NgModule

Nous avons besoin d'un NgModules.

Alors que je voudrais montrer un exemple très simple, dans ce cas, j'aurais besoin de trois modules (en fait 4 - mais je ne compte pas l'AppModule) . Veuillez prendre ceci plutôt qu'un simple extrait de code 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_DIRECTIVESsont extensibles et sont 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 gestion dynamique des choses. Il contiendra des composants d'hébergement et certains fournisseurs .. qui seront des singletons. Nous les publierons donc de manière standard - avecforRoot()

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 du forRoot()auAppModule

Enfin, nous aurons besoin d'un module d'exécution adhoc .. mais qui sera créé plus tard, dans le cadre du DynamicTypeBuildertravail.

Le quatrième module, module d'application, est celui qui continue de déclarer les fournisseurs de compilateur:

...
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 sur NgModule ici:

Un constructeur de modèles

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

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

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

La vraie solution, un vrai constructeur de modèles, 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 ici est - il construit un modèle qui utilise un ensemble de propriétés connues, par exemple entity. Ces propriétés doivent faire partie d'un composant dynamique, que nous créerons ensuite.

Pour le rendre un peu plus facile, nous pouvons utiliser une interface pour définir les propriétés, que notre constructeur de modèles peut utiliser. Ceci sera implémenté par notre type de composant dynamique.

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

Un ComponentFactoryconstructeur

La chose très importante ici est de garder à l'esprit:

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

Nous touchons donc au cœur de notre solution. Le Builder, 1) créera ComponentType2) créera sa NgModule3) compilera ComponentFactory4) le mettra en cache pour une réutilisation ultérieure.

Une dépendance que nous devons 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 à la fois Componentet Module. Parce que si le modèle (en fait la vraie partie dynamique de tout cela) est le même .. nous pouvons réutiliser

Et voici deux méthodes, qui représentent la manière vraiment cool de créer des classes / types décorés en runtime. Non seulement @Componentmais aussi@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 uniquement par modèle. Nous utilisons donc ce fait pour les mettre en cache . C'est vraiment très important. Angular2 mettra également ces derniers en cache par type . Et si nous recréions pour les mêmes chaînes de modèles de nouveaux types ... nous commencerons à générer des fuites de mémoire.

ComponentFactory utilisé par le composant d'hébergement

La pièce finale est un composant, qui héberge la cible de notre composant dynamique, par exemple <div #dynamicContentPlaceHolder></div>. Nous obtenons une référence et utilisons ComponentFactorypour créer un composant. C'est en un mot, et voici toutes les pièces de ce composant (si nécessaire, ouvrez le plongeur ici )

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, les créateurs de modèles et de composants. Viennent ensuite les propriétés qui sont nécessaires pour 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'en a pas @Input. Il n'a donc pas à réagir aux changements. Mais malgré cela (et pour être prêt pour les changements à venir) - nous devons introduire un indicateur si le composant a déjà (tout d'abord) été lancé. Et alors seulement, nous pouvons commencer la magie.

Enfin, nous utiliserons notre constructeur de composants, et il vient d'être compilé / mis en cache ComponentFacotry . Notre espace réservé cible sera invité à instancier leComponent avec cette usine.

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 conserver une référence au modèle compilé .. pour pouvoir le correctement destroy(), 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 tout. N'oubliez pas de détruire tout ce qui a été construit dynamiquement (ngOnDestroy) . Assurez-vous également de mettre en cache dynamique typeset modulessi la seule différence est leur modèle.

Vérifiez tout en action ici

pour voir les versions précédentes (par exemple liées à RC5) de ce post, consultez l' historique


50
cela ressemble à une solution si compliquée, la solution obsolète était très simple et claire, existe-t-il une autre façon de procéder?
tibbus

3
Je pense de la même manière que @tibbus: cela est devenu beaucoup plus compliqué qu'auparavant avec le code obsolète. Merci pour votre réponse.
Lucio Mollinedo

5
@ribsies merci pour votre note. Permettez-moi de clarifier quelque chose. Beaucoup d'autres réponses tentent de simplifier les choses . Mais j'essaie de l'expliquer et de le montrer dans un scénario, fermé à une utilisation réelle . Nous aurions besoin de trucs cache, il nous faudrait appeler détruire sur la recréation etc. Ainsi, alors que la magie de la construction dynamique est vraiment type.builder.tscomme vous l' avez dit, je souhaite, que tout utilisateur comprendrait comment placer que tout en contexte ... J'espère que cela pourrait être utile;)
Radim Köhler

7
@Radim Köhler - J'ai essayé cet exemple. cela fonctionne sans AOT. Mais lorsque j'ai essayé de le faire avec AOT, il affiche l'erreur "Aucune métadonnée NgModule trouvée pour RuntimeComponentModule". pouvez-vous m'aider à résoudre cette erreur.
Trusha

4
La réponse elle-même est parfaite! Mais pour des applications réelles non réalisables. L'équipe angulaire doit fournir une solution pour cela dans le cadre, car c'est une exigence courante dans les applications métier. Sinon, il faut se demander si Angular 2 est la bonne plate-forme pour les applications d'entreprise.
Karl

58

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 clics et l'ai testée avec Angular 4.3.
Pour Angular4, ngComponentOutlet comme décrit dans la réponse d'Ophir est une bien meilleure solution. Mais pour le moment, il ne prend pas encore en charge les entrées et sorties . Si [ce PR] ( https://github.com/angular/angular/pull/15362] est accepté, il serait possible via l'instance de composant renvoyée par l'événement create.
Ng-dynamic-component peut être le meilleur et le plus simple solution, 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 {}

En direct sur http://plnkr.co/edit/fdP9Oc .


3
Je dirais que c'est un exemple comment écrire le moins de code possible pour faire la même chose que dans ma réponse stackoverflow.com/a/38888009/1679310 . Dans le cas où cela devrait être utile (principalement un modèle générant RE) lorsque la condition change ... le simple ngAfterViewInitappel avec un const templatene fonctionnera pas. Mais si votre tâche était de réduire l'approche décrite ci-dessus détaillée (créer un modèle, créer un composant, créer un module, le compiler, créer une fabrique .. créer une instance) ... vous l'avez probablement fait
Radim Köhler

Merci pour la solution: j'ai du mal à charger templateUrl et les styles, j'obtiens l'erreur suivante: aucune implémentation de ResourceLoader n'a été fournie. Impossible de lire l'URL localhost: 3000 / app / pages / pages_common.css , une idée de ce qui me manque?
Gerardlamo

Serait-il possible de compiler le modèle html avec des données spécifiques pour la cellule dans la grille comme le contrôle.? plnkr.co/edit/vJHUCnsJB7cwNJr2cCwp?p=preview Dans ce plunker, comment puis-je compiler et afficher l'image dans la dernière colonne.? De l'aide.?
Karthick

1
@monnef, vous avez raison. Je n'ai pas vérifié le journal de la console. J'ai ajusté le code pour ajouter le composant dans le ngOnInit plutôt que le hook ngAfterViewInit, car le premier est déclenché avant et le second après la détection de changement. (Voir github.com/angular/angular/issues/10131 et fils similaires.)
Rene Hamburger

1
soigné et simple. Fonctionné comme prévu lors de la diffusion sur un navigateur en dev. Mais cela fonctionne-t-il avec AOT? Lorsque l'application est exécutée dans PROD après la compilation, j'obtiens une "Erreur: le compilateur d'exécution n'est pas chargé" au moment où la compilation du composant est tentée. (btw, j'utilise Ionic 3.5)
mymo

52

Je dois être arrivé à la fête tard, aucune des solutions ici ne m'a semblé utile - trop compliquée et me semblait être une solution de contournement.

Ce que je fini par faire est à l' aide Angular 4.0.0-beta.6de » ngComponentOutlet .

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

  • Voici un exemple simple qui ne fait que recevoir du texte et le place dans un modèle, mais vous pouvez évidemment changer 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;
  }
}
  • Brève explication:
    1. my-component - le composant dans lequel un composant dynamique est rendu
    2. DynamicComponent - le composant à construire dynamiquement et il est rendu à l'intérieur de mon composant

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

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

METTRE À JOUR

Fonctionne également pour angulaire 5.


3
Cela a très bien fonctionné pour moi avec Angular4. Le seul ajustement que j'ai dû faire était de pouvoir spécifier des modules d'importation pour le RuntimeComponentModule créé dynamiquement.
Rahul Patel

8
Voici un exemple rapide à partir du démarrage rapide angulaire: embed.plnkr.co/9L72KpobVvY14uiQjo4p
Rahul Patel

5
Cette solution fonctionne-t-elle avec "ng build --prod"? Il semble que la classe du compilateur et AoT ne correspondent pas ensemble atm.
Pierre Chavaroche

2
@OphirStern J'ai également découvert que cette approche fonctionne bien dans Angular 5 mais PAS avec l'indicateur de génération --prod.
TaeKwonJoe

2
Je l'ai testé avec angular 5 (5.2.8) en utilisant JitCompilerFactory et en utilisant le drapeau --prod ne fonctionne pas! Quelqu'un a-t-il une solution? (BTW JitCompilerFactory sans le drapeau --prod fonctionne parfaitement)
Frank

20

Réponse de juin 2019

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

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

Étape 1) Mettez à jour votre AppModule

Importez à PortalModulepartir du @angular/cdk/portalpackage et enregistrez vos composants dynamiques à l'intérieurentryComponents

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

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

Voyez-le en action


1
Mec, tu viens de clouer. Celui-ci attirera l'attention. Je ne pouvais pas croire à quel point il était difficile d'ajouter un simple composant dynamique dans Angular jusqu'à ce que j'en ait besoin. C'est comme faire une réinitialisation et revenir à l'époque antérieure à JQuery.
Gi1ber7

2
@ Gi1ber7 je sais bien? Pourquoi cela leur a-t-il pris autant de temps?
Stephen Paul

1
Belle approche, mais savez-vous comment passer des paramètres à ChildComponent?
Snook

1
@Snook cela peut répondre à votre question stackoverflow.com/questions/47469844/…
Stephen Paul

4
@StephenPaul En quoi cette Portalapproche diffère-t-elle de ngTemplateOutletet ngComponentOutlet? 🤔
Glenn Mohammad

18

J'ai décidé de compacter tout ce que j'ai appris dans un seul fichier . Il y a beaucoup à retenir 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 {}`

10

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

Dites, vous avez un template html dynamique = template1 et souhaitez charger dynamiquement, envelopper d'abord

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

ici template1 en html, peut contenir un composant ng2

Depuis rc6, @NgModule doit envelopper ce composant. @NgModule, tout comme le module dans anglarJS 1, il dissocie 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 des composants de route dans mon html comme vous pouvez le voir plus tard)

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

Et nous devons mettre ci-dessus dans app.moudule.ts pour le charger, s'il vous plaît voir mon app.moudle.ts. Pour plus de détails, 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


3
Vous avez donc déclaré module1, module2, module3. Et si vous aviez besoin d'un autre contenu de modèle "dynamique", vous auriez besoin de créer une définition (fichier) sous forme de moudle4 (module4.ts), non? Si oui, cela ne semble pas être dynamique. C'est statique, n'est-ce pas? Ou est-ce que je manque quelque chose?
Radim Köhler

Ci-dessus, "template1" est une chaîne de code html, vous pouvez y mettre n'importe quoi et nous appelons ce modèle dynamique, comme cette question le demande
Long Field

6

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

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

  2. Créez 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ée 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 Register dans AppComponent:
@Component({
  selector: 'app-root',
  template: '<router-outlet></router-outlet>'
})
export class AppComponent {
  constructor(   
    dynamicComponents: DynamicComponentsService,
  ) {
    dynamicComponents.register();
  }

}
  1. Et maintenant, à n'importe quel endroit de votre code, vous pouvez l'utiliser comme ceci:
dynamicComponents.create('user-icon', {user:{...}});

ou comme ça:

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 deuxième cas, vous devez passer des objets avec JSON.stringify et ensuite les analyser à nouveau. Je ne peux pas trouver de meilleure solution.


Approche intéressante, mais vous devrez cibler es2015 (donc pas de support pour IE11) dans votre tsconfig.json, sinon cela échouera àdocument.createElement(tagName);
Snook

Salut, comme vous avez mentionné un moyen de gérer les entrées, les sorties des composants enfants peuvent-elles également être gérées comme ça?
Mustahsan

5

Résolution de ce problème dans la version finale d'Angular 2 simplement en utilisant la directive dynamicComponent de ng-dynamic .

Usage:

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

Où modèle est votre modèle dynamique et le contexte peut être défini sur n'importe quel modèle de données dynamique auquel vous souhaitez que votre modèle se lie.


Au moment de la rédaction, Angular 5 avec AOT ne prend pas en charge cela car le compilateur JIT n'est pas inclus dans le bundle. Sans AOT, cela fonctionne comme un charme :)
Richard Houltz

cela s'applique-t-il toujours à angular 7+?
Carlos E

4

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

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

  • Tout d'abord, je n'ai pas pu rendre les détails dynamiques à l'intérieur d'un détail dynamique (imbriquer essentiellement des interfaces utilisateur dynamiques les unes dans les autres).
  • Le problème suivant était que je voulais rendre un détail dynamique à l'intérieur de l'une des parties qui était mise à disposition dans la solution. Ce n'était pas possible non plus avec la solution initiale.
  • 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 fait une autre question basée sur ce post, sur la façon d'atteindre ces limitations, qui peut être trouvée ici:

compilation récursive de modèles dynamiques dans angular2

Je vais simplement décrire les réponses à ces limitations, si vous rencontrez le même problème que moi, car cela rend la solution beaucoup plus flexible. Ce serait génial d'avoir également mis à jour le plongeur initial avec cela.

Pour activer l'imbrication des détails dynamiques les uns dans les autres, vous devrez ajouter DynamicModule.forRoot () dans l'instruction d'importation dans le 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;
}

En outre, il n'était pas possible d'utiliser <dynamic-detail>à l'intérieur de l'une des parties étant l'éditeur de chaîne ou l'éditeur de texte.

Pour activer cela, vous devrez changer parts.module.tsetdynamic.module.ts

À l'intérieur, parts.module.tsvous devrez ajouter DynamicDetailleDYNAMIC_DIRECTIVES

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

Aussi dans le, dynamic.module.tsvous devrez supprimer le dynamicDetail car ils font maintenant partie des parties

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

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

Enfin il n'a pas été possible d'utiliser des templateurs dans les pièces créées sur les composants dynamiques. Une solution (ou solution de contournement. Je ne sais pas si c'est un bogue angulaire ou 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([]);
    }

Utilisez ensuite le _compilerpour compiler, les templateUrls sont également activés.

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

Pour donner suite à l'excellente réponse de Radmin, un petit ajustement est nécessaire pour tous ceux qui utilisent angular-cli version 1.0.0-beta.22 et plus.

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

Ainsi, la solution de contournement consiste à ne pas utiliser du tout COMPILER_PROVIDERSet JitCompilerdans la providerssection, mais à utiliser à la place JitCompilerFactoryde '@ angular / compiler' comme ceci à l'intérieur de la classe de générateur de type:

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

Comme vous pouvez le voir, il n'est pas injectable et n'a donc pas de dépendances avec la DI. Cette solution devrait également fonctionner pour les projets n'utilisant pas angular-cli.


1
Merci pour cette suggestion, cependant, je rencontre "Aucune métadonnée NgModule trouvée pour 'DynamicHtmlModule'". Mon implémentation est basée sur stackoverflow.com/questions/40060498/…
Cybey

2
Quelqu'un at-il travaillé JitCompiletFactory avec un échantillon AOT? J'ai la même erreur que @Cybey
user2771738


2

J'essaie moi-même de voir comment puis-je mettre à jour RC4 vers RC5 et donc je suis tombé sur cette entrée et la nouvelle approche de la création de composants dynamiques me tient toujours un peu de mystère, donc je ne suggérerai rien sur le résolveur d'usine de composants.

Mais, ce que je peux suggérer est une approche un peu plus claire de la création de composants dans ce scénario - utilisez simplement un commutateur dans le modèle qui créerait un éditeur de chaîne ou un éditeur de texte selon 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 en passant, "[" dans l'expression [prop] a un sens, cela indique une liaison de données à sens unique, donc vous pouvez et même devez les omettre si vous savez que vous n'avez pas besoin de lier la propriété à la variable.


1
Ce serait une voie à suivre .. si le switch/ casecontient peu de décisions. Mais imaginez que le modèle généré puisse être très volumineux ... et différer pour chaque entité, différer par la sécurité, différer par le statut de l'entité, par chaque type de propriété (numéro, date, référence ... éditeurs) ... Dans ce cas, résoudre ce problème dans un modèle html ngSwitchcréerait un htmlfichier volumineux, très très volumineux .
Radim Köhler

Oh je suis d'accord avec toi. J'ai ce genre de scénario ici, en ce moment alors que j'essaie de charger les principaux composants de l'application sans savoir avant la compilation une classe particulière à afficher. Bien que ce cas particulier ne nécessite pas de création de composant dynamique.
zii

1

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

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

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

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

Merci

Vijay


0

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

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

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

J'aborderais et concevrais 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, texte, chaîne, date, peu importe - quelle que soit la configuration que vous avez passée dans le code HTML de l' ng-containerélément serait disponible.

La configuration,, yourConfigpeut être la même et définir vos métadonnées.

Selon votre configuration ou type d'entrée, la directive devrait agir en conséquence et à partir des types pris en charge, elle rendrait le composant approprié. Sinon, il enregistrera une erreur.


-1

S'appuyant sur la réponse d'Ophir Stern, voici une variante qui fonctionne avec AoT dans Angular 4. Le seul problème que j'ai est que je ne peux pas injecter de services dans DynamicComponent, mais je peux vivre avec ça.

remarque: je n'ai pas testé avec 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;

  }

}

J'espère que cela t'aides.

À votre santé!

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.