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!

1
  • 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. Commented Apr 24, 2018 at 11:34

4 Answers 4

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();
    }
  }
}
Sign up to request clarification or add additional context in comments.

17 Comments

Thanks for your response. I have tried this method which rendered only HTML view. I want Angular tags in the template to work along.
For tags to work too, you can bootstrap a component on the fly.
in angular 5 for simple html like in your first examle bypassSecurityTrustHtml is redundant
How would one handle multiple (and potentially nested) components within the HTML? Should compiling a parent component cause Angular to compile the nested Components?
@Zlatko your component approach does not work with --prod build. Getting "Runtime compiler is not loaded" error.
|
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);

5 Comments

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. stackblitz.com/edit/angular-1womya
Thank you for optimizing the answer. Update the stackblitz.com/edit/angular-cnnpxh example and add HelloComponent to dynamic-component.
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.
@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'. ("<div [ERROR ->]*ngIf="data">Hello world: {{data.some}} {{getX()}}<hello name="{{data.some}}"></hello></div>"): **
Add CommonModule to imports module please read stackblitz.com/edit/dynamic-raw-template
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.

Comments

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.