11

I have a recursive tree structure containing nodes which each have an 'htmlStringContent' property. When I display the tree using nested 'node' components and try to present the html content I use:

<div [innerHtml]="node.htmlStringContent"></div>

The HTML displays correctly but for the following elements:

<a (click)="function()">click me</a>

The (click) functions don't work. I know this has previously been posted but with the large amount of updates angular has brought out recently I cant find any solutions. This answer leads me to believe I should be using the ngComponentOutlet directive but I'm not sure how..

How can I get angular to bind this click functionality?

Edit: I have been told to use the ComponentFactoryResolver but can't see how I can use this to display the html correctly. Can anyone provide further help?

Edit2: I am parsing 'htmlStringContent' through a sanitizing pipe before displaying it on [innerHtml]

transform(v: string) : SafeHtml {
  return this._sanitizer.bypassSecurityTrustHtml(v); 
} 

Edit3: Basically this question is asking whether it is as all possible to display HTML from a property on an object in angular 2/ionic 2 while retaining the (click) functionality on it. I am also open to workaround answers.

  • This way click event will not work. You have a create component and then you will be able to generate click event as already shown in one of your links. – micronyks Oct 25 '16 at 14:43
  • @micronyks but i will still have to use [innerHtml] to present the html content? –  Oct 25 '16 at 14:44
  • 1
    I understand that but like angular1, angular2 doesn't have `$compile` service so you can do it using `componentFactoryResolver`. – micronyks Oct 25 '16 at 14:46
  • @micronyks Not entirely sure how to use that.. could you be more elaborate? –  Oct 25 '16 at 15:02
  • With tree structure I'm not sure but when template has angular context you should use CFR only. – micronyks Oct 25 '16 at 15:22
  • @micronyks I cant find any documentation on CFR that would help me with this problem –  Oct 26 '16 at 08:10
  • You can use http://stackoverflow.com/questions/34784778/equivalent-of-compile-in-angular-2/37044960#37044960 – Günter Zöchbauer Oct 26 '16 at 12:27

2 Answers2

4

CFR DEMO : https://plnkr.co/edit/jKEaDz1JVFoAw0YfOXEU?p=preview

@Component({
  selector: 'my-app',
  template: `

     <button (click)="addComponents()">Add HTML (dynamically using CRF)</button>

     <h1>Angular2 AppComponent</h1>
     <hr>

     <div>
     <h5>dynamic html goes here</h5>
      <div class="container">
        <template #subContainer1></template>
      </div>
     </div>


  `,

})
export class App {
    name:string;
    @ViewChild('subContainer1', {read: ViewContainerRef}) subContainer1: ViewContainerRef;

    constructor(
      private compFactoryResolver: ComponentFactoryResolver
      ) {
      this.name = 'Angular2'
    }

    addComponents() {

      let compFactory: ComponentFactory;

      compFactory = this.compFactoryResolver.resolveComponentFactory(Part1Component);
      this.subContainer1.createComponent(compFactory);

    }
  }
micronyks
  • 54,797
  • 15
  • 112
  • 146
  • I see that this dynamically creates components.. but I have already a nested structure of components that display using recursive ngfor, how will this solution present the html string contained in the components properties and present it correctly, inclusive of the click functionality?? –  Nov 01 '16 at 15:37
3

If I understand correctly, you need to use dynamic templates and compile them on runtime. If so, then you need to use the angular compiler:

@Component({
    selector: 'my-app',
    template: `
      <h1>Angular 2 Dynamic Component</h1>
      <template #container></template>
    `
})
export class AppComponent implements AfterContentInit, OnDestroy {
  private dynamicComponentRefs: ComponentRef[] = [];

  @ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef;

  constructor(private resolver: ComponentFactoryResolver,
              private compiler: Compiler) {}

  ngAfterContentInit() {
    let html = `
      <div>Always Visible</div>
      <div [hidden]="clause1">Hidden because of clause1 = true</div>
      <div [hidden]="clause2">Visible because of clause2 = false</div>
      <button type="button" (click)="buttonClicked()">Click me!</button>
      <div *ngIf="clicked">You clicked the button!</div>
    `;

    this.compiler.compileModuleAndAllComponentsAsync(createDynamicComponent(html))
        .then((mwcf: ModuleWithComponentFactories) => {
          let factory: ComponentFactory = mwcf.componentFactories.find(cf => 
            cf.componentType.name === 'DynamicComponent');
            this.dynamicComponentRefs
              .push(this.container.createComponent(factory));
        });
  }

  ngOnDestroy() {
    /* Make sure you destroy all dynamically created components to avoid leaks */
    this.dynamicComponentRefs.forEach(dcr => {
      dcr.destroy();
    });
  }

}

export function createDynamicComponent(html: string): Type<NgModule> {
  @Component({
    template: html,
  })
  class DynamicComponent {
    private clause1: boolean = true;
    private clause2: boolean = false;
    private clicked = false;

    buttonClicked() {
      this.clicked = true;
    }
  }

  @NgModule({
    imports: [CommonModule],
    declarations: [DynamicComponent],
  })
  class DynamicComponentModule {}

  return DynamicComponentModule;
}

Basically you need to dynamically create a component and the module that declares it (e.g. through a function) and pass it the template as an argument. Then you can call compileModuleAndAllComponentsAsync() on the module and get the component factory you need. Then it's a matter of rendering it in the DOM through the ViewContainerRef.createComponent() method.

Here is working plunker: dynamic template component

Keep in mind however that - for the moment - this approach can only be used with JIT compilation. In AOT compilation the angular compiler is not available and trying to use it will throw an error.

Siri0S
  • 678
  • 1
  • 6
  • 10