0

i have an issue while click binding on dynamic html.I tried setTimeout function but click event not binding on button.i have also tried template referance on button and get value with @ViewChildren but @ViewChildren showing null value.

Typscript

export class AddSectionComponent implements OnInit {
      sectionList: any = [];
      constructor(private elRef: ElementRef,private _httpService: CommonService ,private sanitized: DomSanitizer) { }
    
      ngOnInit(): void {
        this.getSectionList();
      }
      ngAfterViewInit() {
          let element = this.elRef.nativeElement.querySelector('button');
          if (element) {
            element.addEventListener('click', this.bindMethod.bind(this));
          }
      }
      bindMethod() {
        console.log('clicked');
      }
      sanitizeHtml(value: string): SafeHtml {
        return this.sanitized.bypassSecurityTrustHtml(value)
      }
        getSectionList() {
        //API request
        this._httpService.get('/Section/GetSectionList').subscribe(res => {
          if (res) {
              this.sectionList = res.json();
            //sectionList is returning below HTML   
            //<div class="wrapper">
            //  <button type='button' class='btn btn-primary btn-sm'>Click Me</button>
            //</div>
          }
        })
      }
    }

Template

<ng-container *ngFor="let item of sectionList">
    <div [innerHTML]="sanitizeHtml(item?.sectionBody)">

    </div>
    //innerHTML after rendering showing this 
    //<div class="wrapper">
    //  <button type='button' class='btn btn-primary btn-sm'>Click Me</button>
    //</div>
</ng-container>
Shakir
  • 103
  • 10
  • This is strange requirement, how did you get to this ? the button doesn't exist, you are just assuming that it does, that's why binding it seems tricky, if you know 100% that it does, then why isn't' it part of the component itself ? even if that was conditional. However, it's possible, you just need to keep listening and rebinding the event to the dom each time, that's not very good approach though. – Munzer Aug 18 '22 at 15:39
  • i strongly agreed with you that's not good approach but the main reason i want to load some static Html from my admin side so in this way i can reduce my HTTP calls. – Shakir Aug 18 '22 at 15:46
  • if the button isn't static then based on what do you want to bind its click event ? if it is static then keep everything else part of item.sectionBody, but remove the button, and wrap it in a component
    , or then you would need to figure out a smarter way to bind those events dynamically, I can help you more if you create a stackblitz, even if you want to stick to your current approach, it is possible, I just don't think it is going to be very readable/clean code.
    – Munzer Aug 18 '22 at 15:49
  • i am creating a stackblitz and i will let you know. – Shakir Aug 18 '22 at 16:14
  • https://stackoverflow.com/questions/71479471/execute-angular-innerhtml-as-normal-angular-code/71480844#71480844 – Eliseo Aug 18 '22 at 16:58
  • @Munzer please check it https://stackblitz.com/edit/angular-peqgne?file=src%2Fapp%2Fapp.component.ts%3AL19,src%2Fapp%2Fapp.module.ts,src%2Fapp%2Fapp.component.html – Shakir Aug 18 '22 at 17:34

1 Answers1

1

Short Answer, you are binding functions inside your templates, which means you have a new html content every time change detection runs, and change detection runs everytime a function is called, which means your button keeps on being updated infinitely, that's why it never works, Read more here please.

Now on how to do this, I would listen to ngDoCheck, and check if my button has a listener, if not, I will append the listener. I will also make sure to use on Push change detection, because if not, this will ngDoCheck will be called a lot, and maybe the button will be replaced more often, not quite sure about it. Here is how the code would look like.

html

<!-- no more binding to a function directly --> 
<div #test [innerHTML]='sanitizedHtml'></div>

component

import { HttpClient } from '@angular/common/http';
import { AfterViewChecked, AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, DoCheck, ElementRef, OnDestroy, ViewChild } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  changeDetection:  ChangeDetectionStrategy.OnPush
})
export class AppComponent implements DoCheck {
  name = 'Angular';
  people: any;
  //now we are getting the div itself, notice the #test in the html part
  @ViewChild('test')
  html!: ElementRef<HTMLDivElement>;
  //a property to hold the html content
  sanitizedHtml!: SafeHtml;
  constructor(private _http: HttpClient, private sanitized: DomSanitizer,private change: ChangeDetectorRef ) {}

  ngDoCheck(): void {
    //run with every change detection, check if the div content now has a button and attach the click event
    if (this.html != undefined) {
        let btn = this.html.nativeElement.querySelector('button');
        if (btn && btn.onclick == undefined) {
          btn.onclick = this.bindMethod.bind(this);
        }
      }
  }

  ngOnInit() {
    this.peoples();
  }

  peoples() {
    this._http.get('https://swapi.dev/api/people/1').subscribe((item: any) => {
    const people = `<div class="wrapper">
                      <p>${item['name']}</p>
                      <button type='button' class='btn btn-primary btn-sm'>Click Me</button>
                    </div>`;
    //assign the html content and notify change detection
    this.sanitizedHtml = this.sanitized.bypassSecurityTrustHtml(people);
    this.change.markForCheck();
    });
  }
  bindMethod() {
    console.log('clicked');
  }
}

I don't like the approach because of the need to listen to ngDoCheck, this can run a lot, especially if you don't use onpush change detection. I hope this helped.

Munzer
  • 2,216
  • 2
  • 18
  • 25
  • why we are creating `@ViewChild('test')` because we are not using it anywhere explain it please. – Shakir Aug 19 '22 at 07:41
  • Viewchild is a property decorator, it passes the value to the html Dom, please google it and read more, basically now the property html is a reference to the div, and we are using that reference to select the button – Munzer Aug 19 '22 at 07:44
  • i am used to with `@ViewChild` but my question is that we are not getting any value from `@ViewChild` just declared it. – Shakir Aug 19 '22 at 07:56
  • Yes we are using it for the property 'html', and later you can see that we are accessing that property and selecting the button. – Munzer Aug 19 '22 at 08:32
  • why did not we use `@ViewChid` like this `@ViewChild('test') test:ElementRef;` and then `this.test.nativeElement.querySelector('button');` is there any difference between both ? – Shakir Aug 19 '22 at 08:40
  • 1
    This is exactly what I used, you just changed the name of the property and removed the generic cast of HtmlDivElement. There is no difference. – Munzer Aug 19 '22 at 09:12