Search code examples
angulariframedata-uri

Dynamically create and embed data URL in Angular


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.