29

I had such a large html menu that I decided to binding to be able to make several submenu dropdown and avoid html code duplication. Parent > child (which is parent too) > child...

For the context : In ng2_msList/msList.components.ts, ColumnsManagements.ts is imported as this.ColumnsManagementInstance. The innerHTML menu is displayed properly, in ng2_msList/pages/list.html :

<!-- COLUMNS DROPDOWN BUTTON -->
<ul [innerHTML]="msList.ColumnsManagementInstance.columnMenu" class="dropdown-menu" role="menu"> </ul>

With (in a very simplified version of my code) : (Thanks to this Stack Q)

setHtmlColumnsMenu() {
     var self = this;
     var htmlcolumnsmenu = '';
     [...]
     htmlcolumnsmenu += this.createColumnsList(this.getNoneRelationalColumns(true));

     // which return something like a lot of html content and sometime in it : 
     // <a href="javascript:;" (click)="msList.ColumnsManagementInstance.toogleColumn(column)">
     [...]
     return htmlcolumnsmenu;
}

BUT (click)="msList.ColumnsManagementInstance.toogleColumn(column)" (previously in the html content) is not working anymore. It's writing in the view as simple text in tag (before the innerHtml it was not displayed).

I can't reach a way to make it works again. I test multiple ways to call the function or as I found in web links as on the Ang Doc Section, here for example. These examples call a function which is set in the same file/context very easily (click)="MyAction()" but with my context I can't get a way to call it properly.

The app architecture is maybe be not as Angular2 click call would expect.

Community
  • 1
  • 1
Paul Leclerc
  • 1,117
  • 1
  • 14
  • 18

7 Answers7

36

That's by design. Angular doesn't process HTML added by [innerHTML]="..." (except sanitization) in any way. It just passes it to the browser and that's it.

If you want to add HTML dynamically that contains bindings you need to wrap it in a Angular2 component, then you can add it using for example ViewContainerRef.createComponent()

For a full example see Angular 2 dynamic tabs with user-click chosen components

A less Angulary way would be to inject ElementRef, accessing the added HTML using

elementRef.nativeElement.querySelector('a').addEventListener(...)
Community
  • 1
  • 1
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • 3
    What we need to hear is not always what we want to hear :/ Thanks you, it's certainly what I need, even it means I was wrong ;) Accepted! – Paul Leclerc Jun 07 '16 at 12:15
  • For some reason , your solution doesn't work in my demo https://ng-run.com/edit/N0eb6SerRhXl4v8CrXTa – Royi Namir Jun 05 '19 at 13:44
  • I'm only on my phone. Don't call funtions from bindings. They are executed every time change detection runs because Angular has no other way to check if the result is inchanged. Asdign the result to a foeld and bind to that field instead. Not sure this is related to your problem. – Günter Zöchbauer Jun 05 '19 at 13:50
  • 1
    more detailed in typescript is: ....addEventListener('click', (evt: Event) => this.myMethod(params)); – LeonardoX Feb 21 '20 at 16:16
13

There are two solutions I know to this problem:

1) Using the Renderer2 to add click events on rendering

In this solution the Renderer2 is used to add a click event listener to the element and filter target by its type HTMLAnchorElement.

public ngOnInit() {
  // Solution for catching click events on anchors using Renderer2:
  this.removeEventListener = this.renderer.listen(this.elementRef.nativeElement, 'click', (event) => {
    if (event.target instanceof HTMLAnchorElement) {
      // Your custom anchor click event handler
      this.handleAnchorClick(event);
    }
  });
}

StackBlitz with a demo

2) Using ElementRef and querySelectorAll after view init

In this solution all anchor elements are found from the ElementRef using querySelectorAll and add a custom click handler is added. This has to be done after view initialization so the DOM is rendered and we can query the rendered element.

public ngAfterViewInit() {
  // Solution for catching click events on anchors using querySelectorAll:
  this.anchors = this.elementRef.nativeElement.querySelectorAll('a');
  this.anchors.forEach((anchor: HTMLAnchorElement) => {
    anchor.addEventListener('click', this.handleAnchorClick)
  })
}

StackBlitz with a demo


NOTE Please leave a comment on which solution you prefer and why, I actually have a hard time choosing which one I like more.

Wilt
  • 41,477
  • 12
  • 152
  • 203
12

It may be too late but let me hope that is going to help someone.

Since you want click binding (and probably other bindings) it would be better to skip using [innerHTML]="..." and create an inner component to which you pass the data via @Input() annotation.

Concretely, image you have a component called BaseComponent where you set some HTML code to a variable htmlData :

let htmlData  = '...<button (click)="yourMethodThatWontBeCalled()">Action</button>...'

Then in BaseComponent's html file you bind it like below:

...<div [innerHTML]="htmlData"></div>...

Instead of doing this, you create InnerComponent.ts :

@Component({
 selector: 'inner-component',
 templateUrl: './inner-component.html',
 styleUrls: ['./inner-component.scss']
})
export class InnerComponent {
   @Input()
   inputData: any;

   methodThatWillBeCalled(){
    //do you logic on this.inputData
   }
}

InnerComponent's Html file:

...<button (click)="methodThatWillBeCalled()">Action</button>...

Now in BaseComponent's Html file:

...<inner-component [inputData]="PUT_HERE_YOUR_DATA"></inner-component>
hzitoun
  • 5,492
  • 1
  • 36
  • 43
1

I was able to set up click listeners for html that came from an API to innerHtml.

  1. The html from the API needed to be wrapped in a div set to not display.
<div id='wholeHtml' style='display:none;'>
    <h2>Foo Bar Inc.</h2>...
    <button class="buttonToClick">Click me</button>
</div>
  1. The template where the html landed, also needed to be wrapped in targetable div. Note the 'safeHtml' pipe that told Angular that this source was to be trusted (this was an API that I managed).
<div id="parentDiv">
    <div [innerHTML]="apiData | safeHtml"></div>
</div>`
  1. The corresponding component used ElementRef and Renderer2 to copy the hidden div from the api (which was prevented from accepting a click listener), paste it into the parent div, set the newly pasted content to display, and apply click listeners.
import { Component, OnInit, ElementRef, Renderer2 } from '@angular/core';
    ...
  
export class ExampleComponent implements OnInit {
       constructor(private elRef: ElementRef, private renderer: Renderer2) {}
       ngOnInit() {}
       ngAfterViewInit() {
          if(document.getElementById('wholeHtml')){
            let allContent = this.elRef.nativeElement.querySelector('#wholeHtml');
            let parentContainer = this.elRef.nativeElement.querySelector('#parentDiv');
            this.renderer.appendChild(parentContainer, allContent);
            this.renderer.removeStyle(allContent, "display");
            let clickButtons = this.elRef.nativeElement.querySelectorAll('.buttonToClick');
            for(var i = 0; i < clickButtons.length; i++){
               this.renderer.listen(clickButtons[i], 'click', function($event){
                  functionToCall($event.target); 
               });
            };
          }
       }
       ngOnDestroy() {
         this.renderer.destroy();
       }
     }

I was using Angular 6, for what it's worth.

Mr. Stash
  • 2,940
  • 3
  • 10
  • 24
jamesthe500
  • 341
  • 1
  • 7
  • 10
1

in CSS:

.dropdown-menu {
  pointer-events: none;
}

It will solve an issue with innerHTML (click) binding.

Ihor Khomiak
  • 1,151
  • 11
  • 17
0

Using the solution from Ihor:

Wrap the tag using [innerHtlm] in another tag, set pointer-events: none on the child tag and set the click() event on the parent tag:

<div (click)="doSomething()">
  <div style="pointer-events:none" [innerHtlm]="someValue">
</div>
Casper Nybroe
  • 1,179
  • 3
  • 23
  • 47
0

Very hacky, sorry code purists

First of all, if you want to stop angular from sanitizing your innerHTML, using a custom pipe will help here

import {DomSanitizer} from '@angular/platform-browser';
import {PipeTransform, Pipe} from "@angular/core";

    @Pipe({ name: 'rawHtml'})
    export class RawHtmlPipe implements PipeTransform  {
      constructor(private sanitized: DomSanitizer) {}
      transform(value) {
        return this.sanitized.bypassSecurityTrustHtml(value);
      }
    }

You can use this from your code like so (assuming we are binding to an object called comment that has a property commentText)

<p [innerHTML]="comment.commentText | rawHtml"></p>

The next step is adding the click handler

We can't add the click handler into the markup, since the markup is no longer associated with the angular context after rendering.

Instead, add a handler to the parent element of the inner HTML, and use the attributes of the html such as

 <p (click)="commentContentClicked($event, comment)" [innerHTML]="comment.commentText | rawHtml"></p>

From here we can handle the click event, and have access the the bound object also

assuming the inner HTML looks something like:

 <b class='56508ee4-b3fb-40c7-a946-c0eb799b6b75'>Clicked User</b> nice test

and we want to do handle the user clicked event using the id (56508ee4-b3fb-40c7-a946-c0eb799b6b75) as a parameter

commentContentClicked(event, comment){
    console.log('comment', comment);
    console.log('source element', event.srcElement);
  }   

The event.srcElement will show us which element we clicked on in the innerHTML

We then then simply pull the id from the class and continue the workflow

event.srcElement.className
Malcolm Swaine
  • 1,929
  • 24
  • 14