1

I am modifying an Angular template that is currently using innerHTML to dynamically build HTML fragments into the DOM. In this case, I am creating a mat-table and dynamically building the header.

Here is the existing template code before my changes. This works as expected calling the buildLabelFromValues() through the [innerHtml] directive:

<div class="mat-table">
    <!--HEADER-->
    <div class="mat-header-row mat-option no-pointer optgroup-header table mat-primary"
      [innerHtml]="'localStr.' + buildLabelFromValues(autoCompleteList[0], true) | stringLocalize | async">
    </div>
    ...
</div>

However, I need to explicitly build the table header in a more "Angular" way (in order to prepare for setting click/change listeners on the header cells). I can't do this through elements created from the innerHtml directive. So, my modified template code is below:

<div class="mat-header-row mat-option no-pointer optgroup-header table mat-primary">
 {{ this.buildHeaderLabelFromValues(autoCompleteList[0]) }}
</div>

The issue I have currently, is that with the modified template code above, the response from buildHeaderLabelFromValues() is an htmlEncoded string of characters. I need the output to be in actual HTML.

enter image description here

Finally, here is the method that builds the header:

buildHeaderLabelFromValues(value: any): string {
    console.log('value:', value);
    let label = '';
    forEach(value, (_value: any, _key: string) => {
      if (!this.hiddenColumns.includes(_key)) {
        if (_value === null) { _value = undefined; } // don't display "null"
        label = `${label}<div title='${cleanHtmlTitle(_value)}' class="table-col mat-header-cell">${this.formatHeaderText(_key.toString())}</div>`;
      }
    });
    return label;
}

I suppose the issue could be that because the method above is of type: string, the html carets "<" and ">" are being encoded as &lt; and &gt; as part of the interpolated values, something the [innerHTML] directive does not do. I just need to understand how to resolve it so that I can move on with the refactor of this template.

Scott B
  • 38,833
  • 65
  • 160
  • 266

2 Answers2

2

The problem you are facing is a very common one considering that in a lot of scenarios, we intend to somehow inject our own HTML into the template, and it can be easily done via innerHTML attribute like you have demonstrated. Also, as you have correctly mentioned, this injected HTML is just plain HTML and wont support Angular magic since It's injected after the Angular Code Compilation.

So what's the solution? The one that you propose simply fails because it is string interpolation (with double curly braces) and renders on the document whatever you mention. Dom Sanitizer won't help since that too is injecting HTML after compilation so no angular magic.

One sensible solution I can think of is to create an angular component for you table header, provide it inputs that you are using to construct custom HTML and then pass all the input to Table Header Component (Via @Input()) and let it use it's template HTML depending on conditions arrived upon from inputs.

@Component({
   selector:    'my-th',
   template: '<th [title]="Inp1" [ngClass]="{'some-class':true}"></th>',
})
export class TableHeadComponent {
    @Input() inp1;
    @Output() outpt;
    class: string = 'xyz';
}

And the parent component could then use it like

<my-th [inp1]="inp" (outpt)="handle()"></my-th>
luiscla27
  • 4,956
  • 37
  • 49
Madhav Sarpal
  • 256
  • 1
  • 7
  • I like your thinking on this. I was leaning towards building this out as an Directive but wanted to stub it out first in the template itself. But I see that approach has reached its limits, so I will begin refactoring based on either Directive or Component. – Scott B Sep 23 '22 at 18:19
  • One problem with this approach is that the `` tag throws off the mat-table structural styling so the headers no longer stretch to fit the columns in the body section. – Scott B Sep 27 '22 at 13:01
  • Edit on above comment. I just added the `mat-table-row` classes to my custom tag rather than including the DIV I was using before. – Scott B Sep 27 '22 at 15:23
0

You need to sanitize using your innerHTML content by using DomSanitizer:

DomSanitizer helps preventing Cross Site Scripting Security bugs (XSS) by sanitizing values to be safe to use in the different DOM contexts.

import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
constructor(private sanitizer: DomSanitizer) {}

getSafeHtml(): SafeHtml {
    return this.sanitizer.bypassSecurityTrustHtml(
        this.buildHeaderLabelFromValues(autoCompleteList[0])
    );
}
<div class="mat-table">
    <!--HEADER-->
    <div class="mat-hea... bunch of css classes" [innerHTML]="getSafeHtml()">
    </div>
    ...
</div>

There's some issues regarding to DomSanitizer, please take a look to this other question which addresses some of them.

Also, for text interpolation "magic" I would suggest to just use an already built one, like TranslateDefaultParser from package @ngx-translate/core, here's an example:

import { TranslateDefaultParser } from '@ngx-translate/core';
public parser = new TranslateDefaultParser();
getSafeHtml(): SafeHtml {
    const baseString = '<div>{{label}}</div><div>{{value}}</div>';
    return this.sanitizer.bypassSecurityTrustHtml(
        this.parser.interpolate(baseString, {
            label: 'Hero Name',
            value: 'John Connor'
        })
    );
}

The example will output the following working html:

<div>Hero Name</div><div>John Connor</div>
luiscla27
  • 4,956
  • 37
  • 49
  • 1
    @ScottB I removed them by mistake, I see that you already restored it. My bad!! so sorry. – luiscla27 Sep 23 '22 at 18:39
  • 1
    Actually, is a pretty important part of your question the one I deleted. hehe. By just reading it, discards my answer as I didn't bind any event... Maybe I deleted it unconsciously to make my answer look better. lol – luiscla27 Sep 23 '22 at 18:43
  • Do you think my answer might be useful to answer your question?? otherwise I think I'll delete it. – luiscla27 Sep 23 '22 at 18:46
  • I'm taking a different approach but your answer may be helpful to others if they decide to use your approach vs using a companion component or directive. – Scott B Sep 27 '22 at 13:02