Load HTML page in a service with its own CSS Angular

Tags: ,



I have a Service, PrintService that I have added to my application. The Service extracts elements from a page and renders another window with the contents of the extracted elements.

import {Injectable} from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class PrintService {
  popupPrint(selector: string) {
    const printContents = (document.querySelector(selector) as HTMLTableElement).innerHTML;
    const popupWin = window.open('', '_blank', 'top=0,left=0,height=auto,width=auto');
    popupWin?.document.open();
    popupWin?.document.write(`
      <html>
        <head>
          <title>Print tab</title>
          <style>
            .d-flex  {
              width: 100%;
              display: flex;
              justify-content: space-between;
            }
            
             // ... More CSS

            @media print {
              .d-print-none {
                display: none;
              }
            }
          </style>
        </head>
        <body>
          <section class='d-print-none'>
            <button onclick="window.print();">Print</button>
            <button onclick="window.close();">Cancel</button>
          </section>
            ${printContents}
        </body>
        <script>
        (function() {
           window.print();
        })();
        </script>
      </html>`
    );
  }

  constructor() {
  }
}

This works. Print Service on Stackblitz

My Problem now is this, I need to be remove the css styles from the service above to its own file, How can I be able to achieve this

My initial plan was to move it to a text file and read the text file from angular but I believe there is a better approach

Edit 1

Why do I need to have this on a separate style sheet?

I am building an application on a dark theme using bootstrap css. I need to extract the table and print it on a light theme. I think users would prefer to print a black text on white background.

I have a PrintComponent

@Component({
  selector: 'app-print',
  templateUrl: './print.component.html',
  styleUrls: ['./print.component.less']
})
export class PrintComponent {

  @Input() selector: string;

  constructor(private printService: PrintService) {
  }

  print(): void {
    this.printService.popupPrint(this.selector);
  }

And Html is just a button

<button class="btn btn-secondary btn-sm" (click)='print()' type="button">
  Print <span class="icon-print"></span>
</button>

The Idea is to a simple way to print any item on a page e.g I can have

<app-print selector='#reportTable'>
<table id='reportTable'>
  <!-- Contents of this table will be extracted and displayed for printing -->
</table>

What would I consider a better approach?

  • Currently, my PrintService is a large file. Extracting it to a different file will at least solve this problem.

  • Next If the file can be added to the minification process, that would be great

  • I also hope of a way to ‘lazy load’ this service only when required

  • If possible, can I simply have a link to this stylesheet? something like <link rel="stylesheet" href="some/print/style.css"></link>

Answer

Here’s one way to cover all of your expectations and even a bit more.

Important: some things might have to be done slightly differently, depending on your Angular/TypeScript configuration. This is for Angular 10 with SCSS.

1. Create a separate HTML file for the print window

Create it at e.g. app/print/print-page.html:

<html>
    <head>
        <title>Print tab</title>
        <link rel="stylesheet" href="/print-styles.css" />
    </head>
    <body>
        <section class="d-print-none">
            <button onclick="window.print();">Print</button>
            <button onclick="window.close();">Cancel</button>
        </section>
        {{printContents}}
    </body>
    <script>
        (function () {
            window.print();
        })();
    </script>
</html>

Notice that:

  • we are loading /print-styles.css in the <head> – we will create this file and instruct Angular to properly bundle it later;
  • there’s a token {{printContents}}, which we will use to inject custom HTML into the page.

2. Add TypeScript typings for HTML files

We will want to import this HTML file into our print.service.ts file. To be able to do this, TypeScript needs to understand what kind of data .html files hold. This is done via a typing file (.d.ts). Create a file html.d.ts with this content:

declare module '*.html' {
  const content: string;
  export default content;
}

By default, TypeScript will be able to find type declarations anywhere in your source, so place this file wherever you feel like in your source code directory, e.g. app/print/html.d.ts.

3. Use raw-loader to import HTML files

By default an Angular application knows how to import various script/style files. It does not know how to treat HTML files, however. By using a raw-loader we will instruct Webpack to import the target file as a simple string without any transformations.

First you need to install the raw-loader dependency:

npm i -D raw-loader

Then you can either:

  • configure Angular loaders at the application config level to use raw-loader for all files with names ending in .html (this can be done using a custom Webpack builder from @angular-builders/custom-webpack and is out-of-scope for this question);
  • use the loader in-place, which is fine for a one-off instance like this and is what we’re going to do.

4. Create the print service

Now that TypeScript knows how to interpret the HTML file, we can import it into the print service, thus entirely separating the presentation from the service. In app/print/print.service.ts:

import { Injectable } from '@angular/core';

import printPageHTML from '!!raw-loader!./print-page.html';

@Injectable({
  providedIn: 'root',
})
export class PrintService {
  popupPrint(selector: string) {
    const printContents = document.querySelector(selector).innerHTML;
    const popupWin = window.open('', '_blank', 'top=0,left=0,height=auto,width=auto');
    popupWin.document.open();
    popupWin.document.write(printPageHTML.replace('{{printContents}}', printContents));
  }
}

Notice here:

  • how we import the base HTML for the print window – import printPageHTML from '!!raw-loader!./print-page.html';
  • how we inject whatever HTML we want to print using token replace – printPageHTML.replace('{{printContents}}', printContents).

5. Write styles for the print window

Create a app/print/print-styles.scss file and define your desired styles there. You may import Bootstrap here as well.

.d-flex  {
  width: 100%;
  display: flex;
  justify-content: space-between;
}

// ... More CSS

@media print {
  .d-print-none {
    display: none;
  }
}

6. Bundle the CSS file

We need to instruct Angular to properly bundle print-styles.scss so that:

  • this CSS is not included in the application by default at load time (we want the print window to load it lazily at a later time);
  • the file is minified and included in the build with a predictable name (so that we know how to load it – recall step #1).

In angular.json (a.k.a. the workspace config) modify the architect.build.options.styles path to include the styles file, something like this:

"styles": [
  "src/styles/styles.scss",
  {
    "input": "src/styles/print-styles.scss",
    "inject": false,
    "bundleName": "print-styles"
  },
  ... other styles here
]

Notice inject and bundleName – both are important.

The code

I have created a demo repo here: https://github.com/juona/angular-printing-demo. Unfortunately, I was not able to run this on StackBlitz, so better clone the repo and try it on your machine.

Notes

  • Steps 1-3 are optional, but I thought that it would be a good idea to separate the HTML from the service too.

  • If you wish to test this in development mode, you need to also use the extractCss option. This option is enabled by default only for production builds. To turn it on in dev mode, add "extractCss": true to architect.build.options. If you don’t do this, print-styles.scss will be bundled into a print-styles.js (JavaScript!) file – this is not what we want.

  • This is not a very flexible solution because you have to hard-code the name of the CSS file, you must use the extractCss flag, using JavaScript is inconvenient as you have to write it inside the script tag and so on. But it does seem to achieve exactly what you were looking for.

Alternative approaches

Here are some alternatives to investigate, just in case, as I’m too lazy to do that myself:

  • Angular offers a native solution to open components in separate tabs – I am sure that this could be utilised, especially if more involved print page preparation is required.

  • You may also try to use CSS’s all: unset to unset any styles applied to a component and its children, whilst also hiding any unrelated component while printing is in progress. This would allow you to avoid using new windows/tabs while giving the ability to override global CSS (Bootstrap).



Source: stackoverflow