Équivalent de $ compile dans Angular 2

134

Je veux compiler manuellement certaines directives contenant du HTML. Quel est l'équivalent de $compiledans Angular 2?

Par exemple, dans Angular 1, je pourrais compiler dynamiquement un fragment de HTML et l'ajouter au DOM:

var e = angular.element('<div directive></div>');
element.append(e);
$compile(e)($scope);
pixelbits
la source
8
La plupart de ces réponses (sauf 1 maintenant obsolète) ne sont PAS l'équivalent de angular 1 $ compile. $ compile prend une chaîne HTML et compile les composants et expressions qui y sont contenus. Ces réponses créent simplement des composants prédéfinis (qui ne sont pas encore instanciés) dynamiquement et NE PEUVENT PAS accepter un argument de chaîne. Ce n'est pas la même chose. Quelqu'un connaît-il la vraie réponse à cette question?
danday74
Angular 4 est venu avec ComponentFactoryResolver qui équivaut à $ compile dans Angular 1.0. Voir ma réponse stackoverflow.com/questions/34784778/...
Code-EZ
1
@ danday74 - Je suis d'accord qu'aucune de ces réponses n'offre la possibilité de compiler des modèles HTML arbitraires, au lieu de cela, ils choisissent simplement parmi un ensemble de composants préexistants. J'ai trouvé la vraie réponse ici, qui fonctionne au moins dans Angular 8: stackoverflow.com/questions/61137899/… . Voir la réponse unique, qui fournit un StackBlitz fonctionnel qui compile un modèle HTML généré au moment de l'exécution arbitraire.
EbenH

Réponses:

132

Angulaire 2.3.0 (07/12/2016)

Pour obtenir tous les détails, vérifiez:

Pour voir cela en action:

Les principaux:

1) Créer un modèle
2) Créer un composant
3) Créer un module
4) Compiler un module
5) Créer (et mettre en cache) ComponentFactory
6) utiliser Target pour en créer une instance

Un aperçu rapide de la création d'un composant

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

Un moyen d'injecter un composant dans NgModule

createComponentModule (componentType: any) {
  @NgModule({
    imports: [
      PartsModule, // there are 'text-editor', 'string-editor'...
    ],
    declarations: [
      componentType
    ],
  })
  class RuntimeComponentModule
  {
  }
  // a module for just this Type
  return RuntimeComponentModule;
}

Un extrait de code comment créer un ComponentFactory (et le mettre en cache)

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

Un extrait de code comment utiliser le résultat ci-dessus

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

La description complète avec tous les détails lisez ici , ou observez l' exemple de travail

.

.

OBSOLETE - lié à Angular 2.0 RC5 (RC5 uniquement)

pour voir les solutions précédentes pour les versions précédentes de RC, s'il vous plaît, recherchez dans l'historique de ce message

Radim Köhler
la source
2
Merci beaucoup, je cherchais un exemple fonctionnel ComponentFactoryet ViewContainerRefpour remplacer le DynamicComponentLoader maintenant obsolète.
Andre Loker
1
@maxou C'est une référence à lo-dash dans l'index.html, ajoutez simplement cette référence et tout fonctionnera
Radim Köhler
62
Est-ce vraiment si difficile? J'étais capable de faire quelque chose comme ça: $compile($element.contents())($scope.$new());et maintenant c'est des centaines de lignes de code, avec la création de NgModule ... C'est le genre de chose qui me donne envie d'éviter NG2 et de passer à quelque chose de mieux.
Karvapallo
2
Quel est l'avantage d'utiliser JitCompilersi votre exemple peut fonctionner avec Compilerde @angular/core? plnkr.co/edit/UxgkiT?p=preview
yurzui
4
Oh mon dieu, combien de lignes de code je devrais écrire juste pour compiler un petit élément. Je n'ai pas bien compris
Mr_Perfect
35

Remarque: comme @BennyBottema le mentionne dans un commentaire, DynamicComponentLoader est désormais obsolète, d'où cette réponse.


Angular2 n'a pas d' équivalent $ compile . Vous pouvez utiliser DynamicComoponentLoaderet pirater avec les classes ES6 pour compiler votre code dynamiquement (voir ce plunk ):

import {Component, DynamicComponentLoader, ElementRef, OnInit} from 'angular2/core'

function compileToComponent(template, directives) {
  @Component({ 
    selector: 'fake', 
    template , directives
  })
  class FakeComponent {};
  return FakeComponent;
}

@Component({
  selector: 'hello',
  template: '<h1>Hello, Angular!</h1>'
})
class Hello {}

@Component({
  selector: 'my-app',
  template: '<div #container></div>',
})
export class App implements OnInit {
  constructor(
    private loader: DynamicComponentLoader, 
    private elementRef: ElementRef,
  ) {}

  ngOnInit() {} {
    const someDynamicHtml = `<hello></hello><h2>${Date.now()}</h2>`;

    this.loader.loadIntoLocation(
      compileToComponent(someDynamicHtml, [Hello])
      this.elementRef,
      'container'
    );
  }
}

Mais cela ne fonctionnera que jusqu'à ce que l'analyseur HTML soit à l'intérieur du noyau angular2.

alexpods
la source
Super truc! mais dans le cas où mon composant dynamique a des entrées, est-il possible de lier également des données dynamiques?
Eugene Gluhotorenko
2
répondre à ma propre question: c'est possible en passant des données à la fonction de compilation. ici le plunk plnkr.co/edit/dK6l7jiWt535jOw1Htct?p=preview
Eugene Gluhotorenko
Cette solution ne fonctionne qu'avec la version bêta-0. De la version bêta 1 à 15, l'exemple de code renvoie une erreur. Erreur: il n'y a pas de directive de composant à l'élément [objet objet]
Nicolas Forney
13
Depuis rc1 DynamicComponentLoader est devenu obsolète
Benny Bottema
1
@BennyBottema DynamicComponentLoaderétant obsolète, comment faire le même genre de chose dans Angular 2? Disons que j'ai une boîte de dialogue modale et que je veux charger dynamiquement un nouveau composant tel que son contenu
Luke T O'Brien
16

Version angulaire que j'ai utilisée - Angular 4.2.0

Angular 4 est venu avec ComponentFactoryResolver pour charger les composants au moment de l'exécution. C'est une sorte de même implémentation de $ compile dans Angular 1.0 qui répond à vos besoins

Dans cet exemple ci-dessous, je charge le composant ImageWidget dynamiquement dans un DashboardTileComponent

Afin de charger un composant, vous avez besoin d'une directive que vous pouvez appliquer à ng-template qui aidera à placer le composant dynamique

WidgetHostDirective

 import { Directive, ViewContainerRef } from '@angular/core';

    @Directive({
      selector: '[widget-host]',
    })
    export class DashboardTileWidgetHostDirective {
      constructor(public viewContainerRef: ViewContainerRef) { 


      }
    }

cette directive injecte ViewContainerRef pour accéder au conteneur de vue de l'élément qui hébergera le composant ajouté dynamiquement.

DashboardTileComponent (composant d'espace réservé pour le rendu du composant dynamique)

Ce composant accepte une entrée provenant d'un composant parent ou vous pouvez charger à partir de votre service en fonction de votre implémentation. Ce composant joue le rôle principal pour résoudre les composants lors de l'exécution. Dans cette méthode, vous pouvez également voir une méthode nommée renderComponent () qui charge finalement le nom du composant à partir d'un service et le résout avec ComponentFactoryResolver et enfin la définition des données sur le composant dynamique.

import { Component, Input, OnInit, AfterViewInit, ViewChild, ComponentFactoryResolver, OnDestroy } from '@angular/core';
import { DashboardTileWidgetHostDirective } from './DashbardWidgetHost.Directive';
import { TileModel } from './Tile.Model';
import { WidgetComponentService } from "./WidgetComponent.Service";


@Component({
    selector: 'dashboard-tile',
    templateUrl: 'app/tile/DashboardTile.Template.html'
})

export class DashboardTileComponent implements OnInit {
    @Input() tile: any;
    @ViewChild(DashboardTileWidgetHostDirective) widgetHost: DashboardTileWidgetHostDirective;
    constructor(private _componentFactoryResolver: ComponentFactoryResolver,private widgetComponentService:WidgetComponentService) {

    }

    ngOnInit() {

    }
    ngAfterViewInit() {
        this.renderComponents();
    }
    renderComponents() {
        let component=this.widgetComponentService.getComponent(this.tile.componentName);
        let componentFactory = this._componentFactoryResolver.resolveComponentFactory(component);
        let viewContainerRef = this.widgetHost.viewContainerRef;
        let componentRef = viewContainerRef.createComponent(componentFactory);
        (<TileModel>componentRef.instance).data = this.tile;

    }
}

DashboardTileComponent.html

 <div class="col-md-2 col-lg-2 col-sm-2 col-default-margin col-default">        
                        <ng-template widget-host></ng-template>

          </div>

WidgetComponentService

Il s'agit d'une fabrique de services pour enregistrer tous les composants que vous souhaitez résoudre dynamiquement

import { Injectable }           from '@angular/core';
import { ImageTextWidgetComponent } from "../templates/ImageTextWidget.Component";
@Injectable()
export class WidgetComponentService {
  getComponent(componentName:string) {
          if(componentName==="ImageTextWidgetComponent"){
              return ImageTextWidgetComponent
          }
  }
}

ImageTextWidgetComponent (composant que nous chargeons au moment de l'exécution)

import { Component, OnInit, Input } from '@angular/core';


@Component({
    selector: 'dashboard-imagetextwidget',
    templateUrl: 'app/templates/ImageTextWidget.html'
})

export class ImageTextWidgetComponent implements OnInit {
     @Input() data: any;
    constructor() { }

    ngOnInit() { }
}

Ajouter Enfin, ajoutez ce ImageTextWidgetComponent à votre module d'application en tant qu'entréeComponent

@NgModule({
    imports: [BrowserModule],
    providers: [WidgetComponentService],
    declarations: [
        MainApplicationComponent,
        DashboardHostComponent,
        DashboardGroupComponent,
        DashboardTileComponent,
        DashboardTileWidgetHostDirective,
        ImageTextWidgetComponent
        ],
    exports: [],
    entryComponents: [ImageTextWidgetComponent],
    bootstrap: [MainApplicationComponent]
})
export class DashboardModule {
    constructor() {

    }
}

TileModel

 export interface TileModel {
      data: any;
    }

Référence originale de mon blog

Documentation officielle

Télécharger un exemple de code source

Code-EZ
la source
1
Vous avez oublié de mentionner entryComponents. Sans cela, votre solution ne fonctionnera pas
yurzui
ComponentFactoryResolver était en angular2. Et je pense que ce n'est pas l'équivalent de $ compile
yurzui
@yurzui. Mais cela sert le besoin de $ compile non ??
Code-EZ
@yurzui J'ai utilisé le même type d'implémentation en utilisant $ compile. Lorsque nous supprimons des composants d'entrée du module, une erreur ImageTextWidgetComponent n'est pas chargée. Mais l'application fonctionne toujours
Code-EZ
1
@BecarioSenior si vous n'êtes pas casté vers une classe de modèle, ce sera dynamique par défaut. Dans cet exemple, le type de données de propriété est any, ce qui signifie que vous pouvez transmettre n'importe quelle donnée au composant dynamique en tant qu'entrée. Cela donne plus de lisibilité à votre code.
Code-EZ
9

ce package npm m'a facilité la tâche: https://www.npmjs.com/package/ngx-dynamic-template

usage:

<ng-template dynamic-template
             [template]="'some value:{{param1}}, and some component <lazy-component></lazy-component>'"
             [context]="{param1:'value1'}"
             [extraModules]="[someDynamicModule]"></ng-template>
aelbatal
la source
3

Afin de créer de manière dynamique une instance d'un composant et de l'attacher à votre DOM, vous pouvez utiliser le script suivant et devrait fonctionner dans Angular RC :

modèle html:

<div>
  <div id="container"></div>
  <button (click)="viewMeteo()">Meteo</button>
  <button (click)="viewStats()">Stats</button>
</div>

Composant de chargeur

import { Component, DynamicComponentLoader, ElementRef, Injector } from '@angular/core';
import { WidgetMeteoComponent } from './widget-meteo';
import { WidgetStatComponent } from './widget-stat';

@Component({
  moduleId: module.id,
  selector: 'widget-loader',
  templateUrl: 'widget-loader.html',
})
export class WidgetLoaderComponent  {

  constructor( elementRef: ElementRef,
               public dcl:DynamicComponentLoader,
               public injector: Injector) { }

  viewMeteo() {
    this.dcl.loadAsRoot(WidgetMeteoComponent, '#container', this.injector);
  }

  viewStats() {
    this.dcl.loadAsRoot(WidgetStatComponent, '#container', this.injector);
  }

}
fabio_biondi
la source
1
Le DynamicComponentLoader n'est plus: '(Après que cela a été obsolète, il y avait ComponentResolver. Et maintenant, il y a le ComponentFactoryResolver ( blog.rangle.io/dynamically-creating-components-with-angular-2 )
11 Mo
3

Angular TypeScript / ES6 (Angular 2+)

Fonctionne avec AOT + JIT à la fois ensemble.

J'ai créé comment l'utiliser ici: https://github.com/patrikx3/angular-compile

npm install p3x-angular-compile

Composant: devrait avoir un contexte et des données html ...

Html:

<div [p3x-compile]="data" [p3x-compile-context]="ctx">loading ...</div>
Patrik Laszlo
la source
1
La signification du titre «Angular TypeScript» n'est pas évidente. La solution est-elle inutile pour ES5 et ES6? Il serait utile de donner l'exemple de l'utilisation programmatique de ce package, une contrepartie directe à $compile(...)($scope). Il n'y a rien dessus, même dans le fichier readme du repo.
Estus Flask
0

Je sais que ce problème est ancien, mais j'ai passé des semaines à essayer de comprendre comment faire fonctionner ce problème avec AOT activé. J'ai pu compiler un objet mais je n'ai jamais pu exécuter des composants existants. Eh bien, j'ai finalement décidé de changer de tact, car je ne cherchais pas tant à compiler du code qu'à exécuter un modèle personnalisé. Ma pensée était d'ajouter le html que tout le monde peut faire et boucler dans les usines existantes. Ce faisant, je peux rechercher l'élément / l'attribut / etc. et exécutez le composant sur ce HTMLElement. J'ai pu le faire fonctionner et j'ai pensé que je devrais partager cela pour sauver à quelqu'un d'autre l'immense temps que j'ai perdu.

@Component({
    selector: "compile",
    template: "",
    inputs: ["html"]
})
export class CompileHtmlComponent implements OnDestroy {
    constructor(
        private content: ViewContainerRef,
        private injector: Injector,
        private ngModRef: NgModuleRef<any>
    ) { }

    ngOnDestroy() {
        this.DestroyComponents();
    }

    private _ComponentRefCollection: any[] = null;
    private _Html: string;

    get Html(): string {
        return this._Html;
    }
    @Input("html") set Html(val: string) {
        // recompile when the html value is set
        this._Html = (val || "") + "";
        this.TemplateHTMLCompile(this._Html);
    }

    private DestroyComponents() { // we need to remove the components we compiled
        if (this._ComponentRefCollection) {
            this._ComponentRefCollection.forEach((c) => {
                c.destroy();
            });
        }
        this._ComponentRefCollection = new Array();
    }

    private TemplateHTMLCompile(html) {
        this.DestroyComponents();
        this.content.element.nativeElement.innerHTML = html;
        var ref = this.content.element.nativeElement;
        var factories = (this.ngModRef.componentFactoryResolver as any)._factories;
        // here we loop though the factories, find the element based on the selector
        factories.forEach((comp: ComponentFactory<unknown>) => {
            var list = ref.querySelectorAll(comp.selector);
            list.forEach((item) => {
                var parent = item.parentNode;
                var next = item.nextSibling;
                var ngContentNodes: any[][] = new Array(); // this is for the viewchild/viewchildren of this object

                comp.ngContentSelectors.forEach((sel) => {
                    var ngContentList: any[] = new Array();

                    if (sel == "*") // all children;
                    {
                        item.childNodes.forEach((c) => {
                            ngContentList.push(c);
                        });
                    }
                    else {
                        var selList = item.querySelectorAll(sel);

                        selList.forEach((l) => {
                            ngContentList.push(l);
                        });
                    }

                    ngContentNodes.push(ngContentList);
                });
                // here is where we compile the factory based on the node we have
                let component = comp.create(this.injector, ngContentNodes, item, this.ngModRef);

                this._ComponentRefCollection.push(component); // save for our destroy call
                // we need to move the newly compiled element, as it was appended to this components html
                if (next) parent.insertBefore(component.location.nativeElement, next);
                else parent.appendChild(component.location.nativeElement);

                component.hostView.detectChanges(); // tell the component to detectchanges
            });
        });
    }
}
Vince
la source
-5

Si vous souhaitez injecter du code html, utilisez la directive

<div [innerHtml]="htmlVar"></div>

Si vous souhaitez charger un composant entier à un endroit, utilisez DynamicComponentLoader:

https://angular.io/docs/ts/latest/api/core/DynamicComponentLoader-class.html

Vlado Tesanovic
la source
2
Je veux injecter un fragment de HTML sous forme de chaîne et le transmettre à un compilateur de composants, puis ajouter ce composant dans mon DOM. Pouvez-vous donner un exemple de la façon dont l'une ou l'autre de vos solutions peut fonctionner?
pixelbits le
3
l'utilisation de innerHtml ne compile aucun composant dans htmlVar
Juanín