147

I want to manually compile some HTML containing directives. What is the equivalent of $compile in Angular 2?

For example, in Angular 1, I could dynamically compile a fragment of HTML and append it to the DOM:

var e = angular.element('<div directive></div>');
element.append(e);
$compile(e)($scope);
dreftymac
  • 31,404
  • 26
  • 119
  • 182
Michael Kang
  • 52,003
  • 16
  • 103
  • 135
  • 9
    Most these answers (except 1 now deprecated answer) are NOT the equivalent of angular 1 $compile. $compile takes an HTML string and compiles components and expressions contained there. These answers simply creates pre-defined components (that are not yet instantiated) dynamically and CANNOT take a string argument. This is NOT the same thing. Does anyone know of the real answer to this question? – danday74 Apr 13 '17 at 09:09
  • 1
    https://www.npmjs.com/package/p3x-angular-compile – Patrik Laszlo Apr 21 '17 at 23:02
  • Angular 4 came up with ComponentFactoryResolver which equivalent to $ compile in Angular 1.0 .See my answer https://stackoverflow.com/questions/34784778/equivalent-of-compile-in-angular-2/44713107#44713107 – Jameel Moideen Jun 23 '17 at 04:35
  • 1
    @danday74 - I agree that none of these answers provide the ability to compile arbitrary HTML templates, instead they just select from a set of pre-existing components. I found the real answer here, which works in Angular 8 at least: https://stackoverflow.com/questions/61137899/how-to-compile-runtime-generated-angular8-code. See the one answer, which provides a working StackBlitz which compiles an arbitrary run-time-generated HTML template. – EbenH Jul 20 '20 at 16:46

9 Answers9

136

Angular 2.3.0 (2016-12-07)

To get all the details check:

To see that in action:

The principals:

1) Create Template
2) Create Component
3) Create Module
4) Compile Module
5) Create (and cache) ComponentFactory
6) use Target to create an Instance of it

A quick overview how to create a Component

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

A way how to inject component into 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;
}

A code snippet how to create a ComponentFactory (and cache it)

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

A code snippet how to use the above result

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

The full description with all the details read here, or observe working example

.

.

OBSOLETE - Angular 2.0 RC5 related (RC5 only)

to see previous solutions for previous RC versions, please, search through the history of this post

JeffryHouser
  • 39,401
  • 4
  • 38
  • 59
Radim Köhler
  • 122,561
  • 47
  • 239
  • 335
  • 2
    Thanks a lot, I was looking for a working example of `ComponentFactory` and `ViewContainerRef` to replace the now deprecated DynamicComponentLoader. – Andre Loker May 10 '16 at 08:41
  • This solution should supersede the first one, it's clearly a better one. The ng.ComponentFactory gave a TypeScript error, all I had to do was to import the ComponentFactory object and update the offending line: ```js import {ComponentFactory} from '@angular/core'; ... .then((factory: ComponentFactory) => ``` – metaprogrammer Jun 18 '16 at 18:55
  • Interesting. I have tried implementing this for creating on-the-fly modals, creating a 'confirm you wish you delete this item' kinda of thing. While I can create the modal in the DOM just fine, I'm finding that making that modal appear is more difficult. – freethebees Jun 23 '16 at 13:59
  • need to update plnkr's zone – nograde Sep 13 '16 at 21:37
  • @cwyers - thanks so much sir, thanks. The referenced plunker is updated and using suggested `` – Radim Köhler Sep 14 '16 at 04:51
  • @lqbweb Well, we are using this approach go generate pretty complex and dynamic UI (based on lot of custom settings, security..) all is ok. Have you tested the plunker? It should show that the concept is vital... or? – Radim Köhler Sep 19 '16 at 12:49
  • any possibilities to compile the jquery element with dynamic data specific to grid cell in angular 2.? – Karthick Oct 18 '16 at 16:18
  • After copy-pasting, my app breaks because it can not find the underscore _ at line 43 of file `type.builder.ts`. What is that variable supposed to be? – maxbellec Oct 27 '16 at 09:28
  • (And not a big deal, but in Plunker your DynamicDetail does not use OnInit - no `ngOnInit` method) – maxbellec Oct 27 '16 at 09:29
  • 1
    @maxou That is lo-dash reference in the index.html just add that reference and all will work – Radim Köhler Oct 27 '16 at 09:29
  • @maxou ... correct ;) that statement `implements OnInit` without implementing it won't work locally ;) because TS compiler would blame ;) .. that is a typo *(imperfection)* in the plunker ;) – Radim Köhler Oct 27 '16 at 09:31
  • thanks a lot @RadimKöhler, I needed your solution quite badly, it's perfect! While you're here, how do you suppress the error in the compiler `Cannot find name '_'.`? It does not break the app but is annoying – maxbellec Oct 27 '16 at 09:34
  • @maxou ... sorry not sure about that error. the underscore symbol is simply lodash library.. so once you will add `` all should work – Radim Köhler Oct 27 '16 at 09:57
  • I added it, it does work, the compiler just complains about not finding the name since I guess it does not know about the content of lodash.min.js so it does not know about any variable named _. That error is also displayed in the navigator – maxbellec Oct 27 '16 at 10:19
  • 1
    @maxou I seeeeee. you have to add typings https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/lodash – Radim Köhler Oct 27 '16 at 10:20
  • 67
    Is this really this difficult? I used to be able to just do something like this: `$compile($element.contents())($scope.$new());` and now it's hundreds of lines of code, complete with NgModule creation... This is the kind of thing that makes me want to steer clear of NG2 and move on to something better. – Karvapallo Dec 06 '16 at 19:25
  • 2
    What is advantage of using `JitCompiler` if your example might work with `Compiler` from `@angular/core`? http://plnkr.co/edit/UxgkiT?p=preview – yurzui Dec 08 '16 at 07:07
  • Is it possible to pass some data like **object** or **array** to child component? – amansoni211 Jan 24 '17 at 06:48
  • @amansoni211 Check the plunker... *app/dynamic/detail.view.ts* ... line 65... where we can assign any kind of property to just built component... hope it helps a bit ;) – Radim Köhler Jan 24 '17 at 06:50
  • This does not even work when you use simple expressions like {{1+1}} – danday74 Apr 12 '17 at 15:08
  • This is not the equivalent of $compile, $compile can compile a string, this cannot – danday74 Apr 12 '17 at 17:19
  • https://www.npmjs.com/package/p3x-angular-compile, now you can use JIT + AOT at the same time. – Patrik Laszlo Apr 21 '17 at 23:03
  • 5
    Oh my god, how many lines of code I should write just to compile a small element. I didn't understand well – Mr_Perfect Jun 22 '17 at 11:24
  • JitCompiler and Compiler are different ? – Sitecore Sam Jun 20 '18 at 09:27
  • Have to agree with @Karvapallo – iGanja Jan 18 '20 at 04:21
35

Note: As @BennyBottema mentions in a comment, DynamicComponentLoader is now deprecated, hence so is this answer.


Angular2 doesn't have any $compile equivalent. You can use DynamicComoponentLoader and hack with ES6 classes to compile your code dynamically (see this 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'
    );
  }
}

But it will work only until html parser is inside angular2 core.

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
alexpods
  • 47,475
  • 10
  • 100
  • 94
  • Awesome trick! but in case my dynamic component has some inputs is it possible to bind dynamic data as well ? – Eugene Gluhotorenko Mar 06 '16 at 17:38
  • 2
    answering to my own question: it is possible through passing data to the compiling function. here the plunk http://plnkr.co/edit/dK6l7jiWt535jOw1Htct?p=preview – Eugene Gluhotorenko Mar 06 '16 at 19:21
  • This solution is only working with beta-0. From beta 1 to 15 the example code returns an error. Error: There is no component directive at element [object Object] – Nicolas Forney Apr 25 '16 at 12:44
  • Since beta-1 the loadIntoLocation function must be called during the OnInit cycle. @alexpods, if you can update your answer it will be great. – Nicolas Forney Apr 25 '16 at 13:11
  • As of the latest Angular beta, loadIntoLocation() no longer exists. You now want loadNextToLocation() - the signature is different, but actually documented in the DynamicComponentLoader documentation on the Angular 2 site. – James Apr 29 '16 at 06:44
  • 13
    Since rc1 DynamicComponentLoader has become deprecated – Benny Bottema May 27 '16 at 10:29
  • 1
    @BennyBottema since `DynamicComponentLoader` is deprecated, how do we do the same sort of thing in Angular 2? Say I have a modal dialog and I want to dynamically load a new component as it's contents – Luke T O'Brien Nov 16 '16 at 10:23
  • did any one have answer for this question? – Kumaresan Sd May 24 '19 at 09:23
18

Angular Version I have Used - Angular 4.2.0

Angular 4 is came up with ComponentFactoryResolver to load components at runtime. This is a kind of same implementation of $compile in Angular 1.0 which serves your need

In this below example I am loading ImageWidget component dynamically in to a DashboardTileComponent

In order to load a component you need a directive that you can apply to ng-template which will helps to place the dynamic component

WidgetHostDirective

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

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


      }
    }

this directive injects ViewContainerRef to gain access to the view container of the element that will host the dynamically added component.

DashboardTileComponent(Place holder component to render the dynamic component)

This component accepts an input which is coming from a parent components or you can load from your service based on your implementation. This component is doing the major role to resolve the components at runtime. In this method you can also see a method named renderComponent() which ultimately loads the component name from a service and resolve with ComponentFactoryResolver and finally setting data to the dynamic component.

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

This is a service factory to register all the components that you want to resolve dynamically

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

ImageTextWidgetComponent(component we are loading at runtime)

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

Add Finally add this ImageTextWidgetComponent in to your app module as entryComponent

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

Orginal Reference from my blog

Official Documentation

Download Sample Source Code

Jameel Moideen
  • 7,542
  • 12
  • 51
  • 79
  • 1
    You forgot to mention about `entryComponents`. Without it your solution won't work – yurzui Jun 23 '17 at 04:36
  • ComponentFactoryResolver was in angular2. And i think it is not equivalent of $compile – yurzui Jun 23 '17 at 04:39
  • @yurzui . But it serves the need of $compile right?? – Jameel Moideen Jun 23 '17 at 04:41
  • @yurzui I was used the same kind of implementation using $compile. When we remove entry components from the module it will throw an error ImageTextWidgetComponent is not loaded. But application still works – Jameel Moideen Jun 23 '17 at 04:44
  • $compile takes an HTML string and compiles components and expressions contained there but in your implementation you're creating static component – yurzui Jun 23 '17 at 04:51
  • @yurzui I am 100% agree on that but we can wrap that component with the html and load dynamically – Jameel Moideen Jun 23 '17 at 04:53
  • I agree that your answer is good solution for many cases and i use the same thing because it's the right angular way. But again we have to use `JitCompiler` to be able to get the equivalent of $compile – yurzui Jun 23 '17 at 05:02
  • I think this is not an equivalent to `$compile`, because you bind your dynamically created component to `ViewContainerRef`, which you have set up in some other component's template. What I need as a result is a pure HTML element, which I can add anywhere to DOM without knowing it is an Angular component - to make it working with non-Angular libraries, Leaflet in my case. – Michal Moravcik Sep 11 '17 at 13:50
  • Question about this line: (componentRef.instance).data = this.tile; HOW can you specify type , dynamically? – Becario Senior Oct 04 '17 at 13:55
  • 1
    @BecarioSenior if you are not cast to any model class , it will be default dynamic. In this example the type of property data is any , which means you can pass any data to the dynamic component as input. It's give more readability to your code. – Jameel Moideen Oct 04 '17 at 17:22
  • have you counted the lines of code for each? 1 versus how many? r u kidding? this is better? – iGanja Jan 18 '20 at 04:30
9

this npm package made it easier for me: 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
  • 147
  • 1
  • 5
3

In order to dinamically create an instance of a component and attach it to your DOM you can use the following script and should work in Angular RC:

html template:

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

Loader component

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
  • 1,047
  • 11
  • 9
  • 1
    The DynamicComponentLoader is no more :'( After that was deprecated, there was ComponentResolver. And now there is the ComponentFactoryResolver (http://blog.rangle.io/dynamically-creating-components-with-angular-2/) – 11mb Dec 15 '16 at 20:34
3

Angular TypeScript/ES6 (Angular 2+)

Works with AOT + JIT at once together.

I created how to use it here: https://github.com/patrikx3/angular-compile

npm install p3x-angular-compile

Component: Should have a context and some html data...

Html:

<div [p3x-compile]="data" [p3x-compile-context]="ctx">loading ...</div>
Community
  • 1
  • 1
Patrik Laszlo
  • 4,906
  • 8
  • 24
  • 36
  • 1
    It' s not obvious what 'Angular TypeScript' title means. Is the solution useless for ES5 and ES6? It would be helpful to provide the example of programmatic use of this package, a direct counterpart to `$compile(...)($scope)`. There's nothing on it even in repo readme. – Estus Flask May 30 '17 at 10:19
2

You can see the component, that allow to compile simple dynamic Angular components https://www.npmjs.com/package/@codehint-ng/html-compiler

Vasiliy Mazhekin
  • 688
  • 8
  • 24
0

I know this issue is old, but I spent weeks trying to figure out how to make this work with AOT enabled. I was able to compile an object but never able to execute existing components. Well I finally decided to change tact, as I was't looking to compile code so much as execute a custom template. My thought was to add the html which anyone can do and loop though the existing factories. In doing so I can search for the element/attribute/etc. names and execute the component on that HTMLElement. I was able to get it working and figured I should share this to save someone else the immense amount of time I wasted on it.

@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
  • 757
  • 1
  • 8
  • 16
-8

If you want to inject html code use directive

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

If you want to load whole component in some place, use DynamicComponentLoader:

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

Vlado Tesanovic
  • 6,369
  • 2
  • 20
  • 31
  • 2
    I want to inject a fragment of HTML as a string and pass it to a component compiler, and then append that component in my DOM. Can you give an example of how either of your solutions can work? – Michael Kang Jan 14 '16 at 09:35
  • 4
    using innerHtml doesn't compile any components inside htmlVar – Juanín Nov 30 '16 at 18:17