Equivalent of $compile in Angular 2
Asked Answered
D

9

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);
Doubles answered 14/1, 2016 at 8:28 Comment(4)
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?Boer
npmjs.com/package/p3x-angular-compileOlein
Angular 4 came up with ComponentFactoryResolver which equivalent to $ compile in Angular 1.0 .See my answer #34785278Leucocyte
@Boer - 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: #61138399. See the one answer, which provides a working StackBlitz which compiles an arbitrary run-time-generated HTML template.Catenoid
K
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

Kra answered 5/5, 2016 at 7:31 Comment(25)
Thanks a lot, I was looking for a working example of ComponentFactory and ViewContainerRef to replace the now deprecated DynamicComponentLoader.Touchhole
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<IHaveDynamicData>) => Zyrian
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.Investment
need to update plnkr's zone <script> for rc7 per (#39475202)Samul
@cwyers - thanks so much sir, thanks. The referenced plunker is updated and using suggested <script src="https://unpkg.com/[email protected]/dist/zone.js"></script>Otisotitis
@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?Otisotitis
any possibilities to compile the jquery element with dynamic data specific to grid cell in angular 2.?Alidia
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?Islam
@maxou That is lo-dash reference in the index.html just add that reference and all will workOtisotitis
(And not a big deal, but in Plunker your DynamicDetail does not use OnInit - no ngOnInit method)Islam
@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 ;)Otisotitis
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 annoyingIslam
@maxou ... sorry not sure about that error. the underscore symbol is simply lodash library.. so once you will add <script src="https://cdn.jsdelivr.net/lodash/4.11.1/lodash.min.js"></script> all should workOtisotitis
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 navigatorIslam
@maxou I seeeeee. you have to add typings github.com/DefinitelyTyped/DefinitelyTyped/tree/master/lodashOtisotitis
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.Grabowski
What is advantage of using JitCompiler if your example might work with Compiler from @angular/core? plnkr.co/edit/UxgkiT?p=previewAnastassia
Is it possible to pass some data like object or array to child component?Hawkie
@Hawkie 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 ;)Otisotitis
This does not even work when you use simple expressions like {{1+1}}Boer
This is not the equivalent of $compile, $compile can compile a string, this cannotBoer
npmjs.com/package/p3x-angular-compile, now you can use JIT + AOT at the same time.Olein
Oh my god, how many lines of code I should write just to compile a small element. I didn't understand wellHeid
JitCompiler and Compiler are different ?Illfounded
Have to agree with @GrabowskiLozano
O
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.

Obtest answered 14/1, 2016 at 9:36 Comment(8)
Awesome trick! but in case my dynamic component has some inputs is it possible to bind dynamic data as well ?Vanny
answering to my own question: it is possible through passing data to the compiling function. here the plunk plnkr.co/edit/dK6l7jiWt535jOw1Htct?p=previewVanny
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]Seadog
Since beta-1 the loadIntoLocation function must be called during the OnInit cycle. @alexpods, if you can update your answer it will be great.Seadog
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.Syndesmosis
Since rc1 DynamicComponentLoader has become deprecatedStaurolite
@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 contentsSteib
did any one have answer for this question?Starshaped
L
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

Leucocyte answered 23/6, 2017 at 4:33 Comment(11)
You forgot to mention about entryComponents. Without it your solution won't workAnastassia
ComponentFactoryResolver was in angular2. And i think it is not equivalent of $compileAnastassia
@Anastassia . But it serves the need of $compile right??Leucocyte
@Anastassia 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 worksLeucocyte
$compile takes an HTML string and compiles components and expressions contained there but in your implementation you're creating static componentAnastassia
@Anastassia I am 100% agree on that but we can wrap that component with the html and load dynamicallyLeucocyte
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 $compileAnastassia
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.Fool
Question about this line: (<TileModel>componentRef.instance).data = this.tile; HOW can you specify type <TitleModel>, dynamically?Fatuity
@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.Leucocyte
have you counted the lines of code for each? 1 versus how many? r u kidding? this is better?Lozano
G
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>
Guilder answered 30/10, 2017 at 8:33 Comment(0)
S
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);
  }

}
Saraband answered 25/7, 2016 at 23:34 Comment(1)
The DynamicComponentLoader is no more :'( After that was deprecated, there was ComponentResolver. And now there is the ComponentFactoryResolver (blog.rangle.io/dynamically-creating-components-with-angular-2)Azoic
O
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>
Olein answered 16/1, 2017 at 17:57 Comment(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.Ronn
S
2

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

Stelu answered 28/3, 2020 at 9:40 Comment(1)
it worked using this library! :) thanks for the recommendation.Decor
H
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
            });
        });
    }
}
Headlock answered 6/2, 2020 at 16:4 Comment(0)
G
-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

Gorton answered 14/1, 2016 at 9:31 Comment(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?Doubles
using innerHtml doesn't compile any components inside htmlVarBounden

© 2022 - 2024 — McMap. All rights reserved.