How to export HTML table to PDF in Angular
Hello,
In this post, I will explain how to convert an HTML table to PDF. This applies to other HTML widgets but I will be focusing on table
which has a large dataset and vertical scrolling to not clutter the page.
Prerequisites
We are going to use 2 libraries to achieve this.
I am using the following versions for this example taken from my package.json.
{ "html2canvas": "^1.4.1", "jspdf": "^2.5.1" }
Disclaimer
html2canvas:
As the name suggests, html2canvas
processes and converts a given HTML DOM node into an image using canvas
under the hood while keeping the styling intact.
This library is considered "experimental" (as of writing) by its author and should not be used for production.
Please take a look at the Features list and verify whether this library supports all features that are required by your web app.
jsPDF:
It is used to generate a PDF file based on the image (created from the html2canvas
library).
Learn more on their GitHub here.
Theory
The idea is very simple:
We use html2canvas to generate an
canvas
object with the table.We then use the
canvas#toDataURL
function (MDN) to generate a data URL.We initialize
jsPDF
, useaddImage
function to set the image via data URL...
Profit?
Implementation
- Let's create a new Angular project.
$ ng new testpdf
- Install both the libraries from NPM
$ npm install jspdf html2canvas
- HTML code for Angular component
<!-- app.component.html -->
<!-- Export button -->
<div class="my-3">
<button class="btn btn-primary" (click)="export()">Export to PDF</button>
</div>
<!-- Table -->
<h3>Preview:</h3>
<div class="table-container">
<table id="data-table" class="table table-bordered">
<thead>
<tr>
<th>ID</th>
<th>First name</th>
<th>Last name</th>
</tr>
</thead>
<tbody>
<tr>
<td>3</td>
<td>Cartman</td>
<td>Whateveryournameis</td>
</tr>
<tr>
<td>10</td>
<td>Cartman</td>
<td>Titi</td>
</tr>
<tr>
<td>11</td>
<td>Toto</td>
<td>Lara</td>
</tr>
<tr>
<td>22</td>
<td>Luke</td>
<td>Yoda</td>
</tr>
<tr>
<td>26</td>
<td>Foo</td>
<td>Moliku</td>
</tr>
<tr>
<td>31</td>
<td>Luke</td>
<td>Someone Last Name</td>
</tr>
<tr>
<td>32</td>
<td>Batman</td>
<td>Lara</td>
</tr>
<tr>
<td>37</td>
<td>Zed</td>
<td>Kyle</td>
</tr>
<tr>
<td>39</td>
<td>Louis</td>
<td>Whateveryournameis</td>
</tr>
<tr>
<td>41</td>
<td>Superman</td>
<td>Yoda</td>
</tr>
<tr>
<td>42</td>
<td>Batman</td>
<td>Moliku</td>
</tr>
<tr>
<td>43</td>
<td>Zed</td>
<td>Lara</td>
</tr>
<tr>
<td>46</td>
<td>Foo</td>
<td>Someone Last Name</td>
</tr>
<tr>
<td>47</td>
<td>Superman</td>
<td>Someone Last Name</td>
</tr>
<tr>
<td>48</td>
<td>Toto</td>
<td>Bar</td>
</tr>
<tr>
<td>48</td>
<td>Batman</td>
<td>Lara</td>
</tr>
<tr>
<td>54</td>
<td>Luke</td>
<td>Bar</td>
</tr>
<tr>
<td>62</td>
<td>Foo</td>
<td>Kyle</td>
</tr>
<tr>
<td>80</td>
<td>Zed</td>
<td>Kyle</td>
</tr>
<tr>
<td>87</td>
<td>Zed</td>
<td>Someone Last Name</td>
</tr>
<tr>
<td>87</td>
<td>Toto</td>
<td>Yoda</td>
</tr>
<tr>
<td>88</td>
<td>Toto</td>
<td>Titi</td>
</tr>
<tr>
<td>89</td>
<td>Luke</td>
<td>Whateveryournameis</td>
</tr>
<tr>
<td>97</td>
<td>Zed</td>
<td>Bar</td>
</tr>
<tr>
<td>101</td>
<td>Someone First Name</td>
<td>Someone Last Name</td>
</tr>
<tr>
<td>104</td>
<td>Toto</td>
<td>Kyle</td>
</tr>
<tr>
<td>105</td>
<td>Toto</td>
<td>Titi</td>
</tr>
<tr>
<td>107</td>
<td>Cartman</td>
<td>Whateveryournameis</td>
</tr>
<tr>
<td>107</td>
<td>Louis</td>
<td>Lara</td>
</tr>
<tr>
<td>113</td>
<td>Foo</td>
<td>Moliku</td>
</tr>
<tr>
<td>114</td>
<td>Someone First Name</td>
<td>Titi</td>
</tr>
<tr>
<td>119</td>
<td>Zed</td>
<td>Someone Last Name</td>
</tr>
<tr>
<td>121</td>
<td>Toto</td>
<td>Bar</td>
</tr>
<tr>
<td>131</td>
<td>Louis</td>
<td>Moliku</td>
</tr>
<tr>
<td>133</td>
<td>Cartman</td>
<td>Moliku</td>
</tr>
<tr>
<td>134</td>
<td>Someone First Name</td>
<td>Someone Last Name</td>
</tr>
<tr>
<td>134</td>
<td>Toto</td>
<td>Whateveryournameis</td>
</tr>
<tr>
<td>135</td>
<td>Superman</td>
<td>Whateveryournameis</td>
</tr>
<tr>
<td>144</td>
<td>Someone First Name</td>
<td>Yoda</td>
</tr>
<tr>
<td>154</td>
<td>Luke</td>
<td>Moliku</td>
</tr>
<tr>
<td>154</td>
<td>Batman</td>
<td>Bar</td>
</tr>
<tr>
<td>155</td>
<td>Louis</td>
<td>Whateveryournameis</td>
</tr>
<tr>
<td>156</td>
<td>Someone First Name</td>
<td>Lara</td>
</tr>
</tbody>
</table>
</div>
- CSS styling (optional)
/* app.component.css */
.table-container {
height: 50vh;
overflow: auto;
}
- Attempt - 1:
exportPDF
function
// app.component.ts
export() {
// 1. Get a reference to the DOM node.
const component = document.getElementById('data-table')!;
// 2. Get a reference to width, height of DOM node.
const componentWidth = component.offsetWidth
const componentHeight = component.offsetHeight
// 3. Generate <canvas> from HTML node.
html2canvas(component).then((canvas) => {
// 4. Generate Data URL from <canvas>
const imgData = canvas.toDataURL('image/png');
// 5. Create an instance of `jsPDF`
// 5.1 `orientation` -> 'landscape' || 'portrait'
// 5.2 `unit` -> 'px' (mandatory)
const pdf = new jsPDF({ orientation: 'landscape', unit: 'px'});
// 6. Use `addImage` to render generated image into PDF page.
pdf.addImage(imgData, 'PNG', 0, 0, componentWidth, componentHeight)
// 7. Download PDF
pdf.save('filename.pdf')
})
}
Results - I
Expected output:
Actual output:
So, what went wrong? There are 3 observations to be made.
The contents inside the PDF file are distorted.
The content hasn't scaled very well making it blurry (I mean look at the actual result screenshot!).
The third column is entirely missing.
After researching this for a while, I came across this StackOverflow post which helped me understand the issue.
My theory is that the width and height properties of the jsPDF's internal page are much smaller than the <table>
DOM node. This means we are trying to fit something so big inside something much smaller. So, when we try to fit in a "large" image inside a jsPDF page, it would (obviously) be cut off.
So, how do we fix this?
We need to somehow adjust the width and height properties of the PDF file to match the DOM's width and height.
Let's try this again.
// app.component.ts
export() {
// 1. Get a reference to the DOM node.
const component = document.getElementById('data-table')!;
// 2. Get a reference to width, height of DOM node.
const componentWidth = component.offsetWidth
const componentHeight = component.offsetHeight
// 3. Generate <canvas> from HTML node.
html2canvas(component).then((canvas) => {
// 4. Generate Data URL from <canvas>
const imgData = canvas.toDataURL('image/png');
// 5. Create an instance of `jsPDF`
// 5.1 `orientation` -> 'landscape' || 'portrait'
// 5.2 `unit` -> 'px' (mandatory)
const pdf = new jsPDF({ orientation: 'landscape', unit: 'px'});
// NEW - set jsPDF page width/height the same as the DOM node.
pdf.internal.pageSize.width = componentWidth;
pdf.internal.pageSize.height = componentHeight;
// 6. Use `addImage` to render generated image into PDF page.
pdf.addImage(imgData, 'PNG', 0, 0, componentWidth, componentHeight)
// 7. Download PDF
pdf.save('filename.pdf')
})
}
Results - II
Yup! It works.
If you're wondering whether the "Actual Results" image from above was taken from the output of this code, then you're right :P
Bonus content
There is a chance you're seeing large file sizes on the generated PDF files. This could happen if your table uses too much CSS.
The solution is to enable compress: true
option in jsPDF
initialization options like so:
// app.component.ts
export() {
...
const pdf = new jsPDF({
...,
compress: true // reduces PDF file size
});
...
}
Conclusion
I hope you've learned something in this post.
If you've liked this post, show your support by using the emojis on the right. It pleases the algorithm :^)
You can also @ me on Mastodon here.
Bye for now.