15

I wanted to receive an HTML data via service call to server(this is for sure. I cannot keep templates in local) and manipulate them internally on how to show it(either as a modal or full page). This HTML with Angular tags should be looped to a component and work together. At most kind of $compile in Angular JS.

I am developing the solution in Angular 5 and should be compatible with AOT compiler. I had referred several solutions and landed to confusion on the deprecated and updated solutions. Please help me. I believe your updated answer would help many other people as well.. Thank you so much in advance!

Srini
  • 181
  • 1
  • 1
  • 6
  • You mention several solutions you've looked into, it would be good if you describe a few, and what you have tried and exactly what you're confused about. The title of the question is nice and SEO-worthy, but the content is just a bit below par. – Zlatko Apr 24 '18 at 11:34

4 Answers4

25

For rendering HTML on the fly, you need DomSanitizer. E.g. something like this:

<!-- template -->
<div [innerHTML]="htmlData"></div>

// component
import { Component } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  htmlData: any;
  constructor(private sanitizer: DomSanitizer) {}

  ngOnInit() {
    this.htmlData= this.sanitizer.bypassSecurityTrustHtml('<div style="border: 1px solid red;"><h2>Safe Html</h2><span class="user-content">Server prepared this html block.</span></div>');
  }
}

Now, that's the gist of it. You obviously also need a loading mechanic. You might also want to include some data into this block - if it's simple data, it can be on the fly:

this.htmlData = this.sanitizer.bypassSecurityTrustHtml(`<div>${this.someValue}</div>`);

For more complex scenarios you might need to create a dynamic component.

Edit: an example of a component resolved dynamically. With this, you create a component on-the-fly from server-sent html.

@Component({
  selector: 'my-component',
  template: `<h2>Stuff bellow will get dynamically created and injected<h2>
          <div #vc></div>`
})
export class TaggedDescComponent {
  @ViewChild('vc', {read: ViewContainerRef}) vc: ViewContainerRef;

  private cmpRef: ComponentRef<any>;

  constructor(private compiler: Compiler,
              private injector: Injector,
              private moduleRef: NgModuleRef<any>,
              private backendService: backendService,
              ) {}

  ngAfterViewInit() {
    // Here, get your HTML from backend.
    this.backendService.getHTMLFromServer()
        .subscribe(rawHTML => this.createComponentFromRaw(rawHTML));
  }

  // Here we create the component.
  private createComponentFromRaw(template: string) {
    // Let's say your template looks like `<h2><some-component [data]="data"></some-component>`
    // As you see, it has an (existing) angular component `some-component` and it injects it [data]

    // Now we create a new component. It has that template, and we can even give it data.
    const tmpCmp = Component({ template, styles })(class {
      // the class is anonymous. But it's a quite regular angular class. You could add @Inputs,
      // @Outputs, inject stuff etc.
      data: { some: 'data'};
      ngOnInit() { /* do stuff here in the dynamic component */}
    });

    // Now, also create a dynamic module.
    const tmpModule = NgModule({
      imports: [RouterModule],
      declarations: [tmpCmp],
      // providers: [] - e.g. if your dynamic component needs any service, provide it here.
    })(class {});

    // Now compile this module and component, and inject it into that #vc in your current component template.
    this.compiler.compileModuleAndAllComponentsAsync(tmpModule)
      .then((factories) => {
        const f = factories.componentFactories[0];
        this.cmpRef = f.create(this.injector, [], null, this.moduleRef);
        this.cmpRef.instance.name = 'my-dynamic-component';
        this.vc.insert(this.cmpRef.hostView);
      });
  }

  // Cleanup properly. You can add more cleanup-related stuff here.
  ngOnDestroy() {
    if(this.cmpRef) {
      this.cmpRef.destroy();
    }
  }
}
Atiris
  • 2,613
  • 2
  • 28
  • 42
Zlatko
  • 18,936
  • 14
  • 70
  • 123
  • Thanks for your response. I have tried this method which rendered only HTML view. I want Angular tags in the template to work along. – Srini Dec 30 '17 at 04:30
  • For tags to work too, you can bootstrap a component on the fly. – Zlatko Dec 31 '17 at 11:11
  • 1
    in angular 5 for simple html like in your first examle bypassSecurityTrustHtml is redundant – dimson d Mar 08 '18 at 16:39
  • How would one handle multiple (and potentially nested) components within the HTML? Should compiling a parent component cause Angular to compile the nested Components? – THEK May 01 '18 at 10:24
  • 1
    @Zlatko your component approach does not work with --prod build. Getting "Runtime compiler is not loaded" error. – Manu Sharma May 24 '18 at 06:11
  • @ManuSharma it does for me. What is your specific problem? Did you post a question? – Zlatko May 24 '18 at 06:23
  • @Zlatko i wanted to dynamically populate html similar to the question posted by op. Your solution did work for me but gave error when i did ng build --prod . The approach specified in https://angular.io/guide/dynamic-form worked well for me. Thanks for your post. – Manu Sharma Jun 04 '18 at 00:31
  • Work perfectly, but got trouble while I'm injecting services (or module) and and constructor declaration e.g. ERROR Can't resolve all parameters for formName ... – mduvernon Dec 26 '18 at 20:55
  • Is that possible to pass an event to the parent component? (Can you show how please?) – mduvernon Dec 27 '18 at 16:15
  • @Timan sure. E.g. in that second example, on const tmpCmp, add an observable property, bind it to a click in your dynamic template. Then when you instantiate a component (this.cmpRef = f.create...), subscribe to it: this.cmpRef.instance.clicks$.subscribe(...). – Zlatko Dec 27 '18 at 21:22
  • For a full example, shoot another, specific question, "how to pass an event from a dynamically created component." If I don't see it, somebody will, and likely could help you. – Zlatko Dec 27 '18 at 21:23
  • Thank you very much I have this here https://stackoverflow.com/questions/46132012/using-an-observable-to-detect-a-change-in-a-variable#answer-46132784, – mduvernon Dec 28 '18 at 10:46
  • I Have another question for You Just Here : https://stackoverflow.com/questions/53957518/error-while-reusing-component-that-have-been-loaded-on-the-fly – mduvernon Dec 28 '18 at 11:05
  • Thanks for this very well documented code! But it seems I can't get this code running on Angular 7.2. Trying to reduce potential error sources, I hardcoded the template input string to `
    Hello world
    ` and debugged each step, but the my-dynamic-component does not appear in the DOM. Any ideas what could cause the problem?
    – spiegelm Jan 15 '19 at 16:06
  • I was able to find a solution: https://stackoverflow.com/questions/48028676/compile-dynamic-html-in-angular-4-5-something-similar-to-compile-in-angular-js/52455638#comment95233868_52455638 – spiegelm Jan 15 '19 at 17:13
  • If you use Angular material or another module, how do you import it? I am using MatDialog and put in NgModule's imports array, but get this: Unexpected value 'MatDialog' imported by the module 'class_2'. Please add a @NgModule annotation. – Evan Aug 16 '20 at 17:11
  • @Evan you mean you want it in the dynamic module and component? You either need the MatDialogModule inyour global scope or import it in your dynamic module here. You have a stackblitz of what you would like to do? – Zlatko Aug 17 '20 at 20:07
6

Here's an extended solution with dynamic template and dynamic component class code using eval. See below for a variaton without eval.

Stackblitz Example without using eval.

import { Component, ViewChild, ViewContainerRef, NgModule, Compiler, Injector, NgModuleRef } from '@angular/core';
import {CommonModule} from "@angular/common";
import { RouterModule } from "@angular/router"

@Component({
    selector: 'app-root',
    template: `<div style="text-align:center">
    <h1>
    Welcome to {{ title }}!
    </h1>
</div>
<div #content></div>`
})
export class AppComponent {

    title = 'Angular';

    @ViewChild("content", { read: ViewContainerRef })
    content: ViewContainerRef;

    constructor(private compiler: Compiler,
    private injector: Injector,
    private moduleRef: NgModuleRef<any>, ) {
    }

    // Here we create the component.
    private createComponentFromRaw(klass: string, template: string, styles = null) {
    // Let's say your template looks like `<h2><some-component [data]="data"></some-component>`
    // As you see, it has an (existing) angular component `some-component` and it injects it [data]

    // Now we create a new component. It has that template, and we can even give it data.
    var c = null;
    eval(`c = ${klass}`);
    let tmpCmp = Component({ template, styles })(c);

    // Now, also create a dynamic module.
    const tmpModule = NgModule({
        imports: [CommonModule, RouterModule],
        declarations: [tmpCmp],
        // providers: [] - e.g. if your dynamic component needs any service, provide it here.
    })(class { });

    // Now compile this module and component, and inject it into that #vc in your current component template.
    this.compiler.compileModuleAndAllComponentsAsync(tmpModule)
        .then((factories) => {
        const f = factories.componentFactories[factories.componentFactories.length - 1];
        var cmpRef = f.create(this.injector, [], undefined, this.moduleRef);
        cmpRef.instance.name = 'app-dynamic';
        this.content.insert(cmpRef.hostView);
        });
    }

    ngAfterViewInit() {
    this.createComponentFromRaw(`class _ {
        constructor(){
        this.data = {some: 'data'};
        }
        ngOnInit() { }
        ngAfterViewInit(){}
        clickMe(){ alert("Hello eval");}
        }`, `<button (click)="clickMe()">Click Me</button>`)
    }
}

Here's a small variation without dynamic class code. Binding the dynamic template variables via class did not work out, instead a function was used:

function A() {
    this.data = { some: 'data' };
    this.clickMe = () => { alert("Hello tmpCmp2"); }
}
let tmpCmp2 = Component({ template, styles })(new A().constructor);
Moslem Shahsavan
  • 1,211
  • 17
  • 19
  • 1
    Thanks for answer! Based on this, i was able to get a working example. However imports in the dynamic component and nested components inside the template don't work for me. https://stackblitz.com/edit/angular-1womya – spiegelm Jan 15 '19 at 16:59
  • Thank you for optimizing the answer. Update the stackblitz.com/edit/angular-cnnpxh example and add HelloComponent to dynamic-component. – Moslem Shahsavan Jan 16 '19 at 15:18
  • I'm curious as to why you're using the template literal instead of a factory to create that class? At least in this example it looks completely unnecessary. – Zlatko Jan 16 '19 at 17:24
  • @spiegelm And *ngIf is not work. **ERROR Error: Template parse errors: Can't bind to 'ngIf' since it isn't a known property of 'div'. ("
    ]*ngIf="data">Hello world: {{data.some}} {{getX()}}
    "): **
    – Jai Kumaresh May 20 '19 at 13:19
  • 1
    Add CommonModule to imports module please read https://stackblitz.com/edit/dynamic-raw-template – Moslem Shahsavan May 21 '19 at 05:34
0

Checkout the npm package Ngx-Dynamic-Compiler

This package enables you to use angular directives like *ngIf,*ngFor , Databinding using string interpolation and creates a truly dynamic component at runtime. AOT support has been provided.

0

Check this package one https://www.npmjs.com/package/@codehint-ng/html-compiler Or see the source code to see how to compile the HTML string with paired JS object with data and events.

Vasiliy Mazhekin
  • 688
  • 8
  • 24