Dynamically create and embed data URL in Angular

Issue

In my Angular application, I have the following invariable basic conditions:

  1. The application dynamically loads a ZIP file from an external URL.
  2. Within this ZIP file, there’s a HTML file which is to be extracted. This HTML file may contain images with data:image/jpeg;base64,...-like sources (but no external links).
  3. The HTML file has to be displayed "as is" by the Angular application in a separate browser tab

I managed to implement 1. and 2. seamlessly with JSZip etc. Thus, we can assume, there’s a String variable htmlFileContent now. Displaying the iFrame on specific conditions in an otherwise empty Angular tab is also not the problem, I managed to do this. To achieve the rest of point 3, I see two different approaches:

Using a div:

Use htmlFileContent as the innerHTML of a div element, like so:

<div [innerHtml]="{{ htmlFileContent }}"></div>

This works indeed, but has some umcomely side effects, e. g. the title of the "inner" HTML header is rendered as well in the browser. Hence, I could try and parse htmlFileContent into a DOM and remove the unwanted tree elements. This might be a working solution, but somehow I don’t feel good about it.

Additionally, the HTML contains some in-file anchor links (<a href="#topOfPage"> style) which would also not work any more and need to be corrected.

Using an iFrame:

I know well about the uglyness and deprecation of iFrame usage, nevertheless, in my eyes this seems to be an adequate approach to my problem. So I would use:

<iframe [src]="fileUrl" class="fullscreenIFrame"></iframe>

And here the problem arises: One could set fileUrl="data:text/html;charset=utf-8,"+ htmlFileContent. But then, Angular will (rightly) complain: ERROR Error: NG0904: unsafe value used in a resource URL context (see https://g.co/ng/security#xss) and nothing will be displayed. So, currently I am trying something like this using the DomSanitizer from @angular/platform-browser:

this.filedata = this.sanitizer.bypassSecurityTrustHtml(htmlFileContent);
this.fileUrl = this.sanitizer.bypassSecurityTrustResourceUrl("data:text/html;charset=utf-8,"+ this.filedata) ;

Which will indeed work partly: I get a warning SafeValue must use [property]=binding: rendered in the output and the "inner" HTML is cut where the first src="data:..." image appears.

And here I’m stuck. My iFrame can, of course, have only one src and I can’t concatenate fileUrl (which then would be shortened to the data:text/html;charset=utf-8, content) and fileData there as one is a SafeResourceUrl and one a SafeHtml object.


Here’s a MWE of my problem:

https://stackblitz.com/edit/angular-ivy-uzpcsy?file=src/app/app.component.ts

(Interestingly, the image is rendered here… Anyway, the SafeValue warning persists.)

Do you have any suggestions on how to handle this particular requirement? Might the div-approach still be the better one? Any help is greatly appreciated – many thanks!

Solution

As I didn’t receive or find another possibility, this is the way I implemented it – just for future reference:

import { ActivatedRoute, Router, UrlSegment } from '@angular/router';
import { DomSanitizer, SafeHtml, SafeResourceUrl } from '@angular/platform-browser';

export class MyComponent implements OnInit {
  
  filedata: SafeHtml = 'Loading...';
  currentUrl: string = '';  

  constructor(
    private activatedRoute: ActivatedRoute,
    private sanitizer: DomSanitizer,
  ) {}

  ngOnInit(): void {
    // Use the Angular possibilities to retrieve to complete current URL
    this.activatedRoute.url.subscribe(url => {
      this.currentUrl = '';
      url.forEach(urlElement => {
        this.currentUrl += urlElement + '/';
      });
      this.currentUrl = this.currentUrl.substring(0, this.currentUrl.length - 1);
    });
    this.loadFileData();
  }

  loadFileData(): void {
    // This is the function where the ZIP file is downloaded and the file
    // content is extracted. As it is not part of the question or the
    // answer, I will not post it here.
    // Let's just assume, the file content is now stored in "response".
    this.filedata = 
    this.sanitizer.bypassSecurityTrustHtml(this.correctFileData(response));
  }

  // Receives the file content response as first parameter,
  // removes the unwanted tags and corrects the anchor links.
  private correctFileData(response: string): string {
    let el = document.createElement('html');
    el.innerHTML = response;

    // Remove all "head" tags including their inner elements
    // (Of course, the should be only one head tag, but you never know...)
    let headElements = el.getElementsByTagName('head');

    for (let i = 0; i < headElements.length; i++) {
      el.removeChild(headElements[i]);
    }

    // Correct all anchor links: Prepend the current URL
    // So http://my.address.com/#anchor would become
    // http://my.address.com/myApp/myRoute/mySubRoute#anchor
    // for example
    let links = el.getElementsByTagName('a');
    for (let i = 0; i < links.length; i++) {
      let link = links[i];
      if (link.href.indexOf('#') != -1) {
        console.log(link.href + ' --> ' + this.currentUrl + 
          link.href.substring(link.href.indexOf('#')));
        link.href = this.currentUrl + link.href.substring(link.href.indexOf('#'));
      }
    }

    return el.outerHTML;
  }

And in the component’s HTML, I just use:

<div [innerHTML]="filedata"></div>

Possibly, there are other, better solution, but this one works very well for my use case.

Answered By – ahuemmer

Answer Checked By – Candace Johnson (AngularFixing Volunteer)

Leave a Reply

Your email address will not be published.