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>
Advertisement
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
toarchitect.build.options
. If you don’t do this,print-styles.scss
will be bundled into aprint-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 thescript
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).