PDFs Another Way: Custom Component + html2pdf

A while back I posted about using jsPDF, and found a way to make it work. Recently, it became more of an issue as we wanted to print nicer (i.e. send to your customer) PDFs.

As we have to produce sometimes a lot of these in a given week, I didn't want to rely on a 3rd-party API with their charges, but I wanted more control over the PDF, especially around page breaking and such. So, a quick trip back to the drawing board, and now we have an ultra-simple custom component that can be fed a string of HTML, and then print a nice version of it (depending on your CSS skills, of course!)

So, 1st-up, you need to create a custom component.

Two key things here:

  1. Our model references a temporary variable. This temp variable is where we will store the HTML string that will be our document to print
  2. You need to select Allow popups to escape sandbox

Generating the HTML to use in your document is an exercise left to the reader

Now, once we've set that up, we need to put our code together. This is going to be an extremely simple custom component.

<html>
<head>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
       <!-- put any fonts you need here -->

	<style>
                <!-- here is where you'll need to style your html for printing, especially if you're also displaying it natively inside of your retool app -->
	</style>
</head>
<body>
   <script src="https://cdn.tryretool.com/js/react.production.min.js" crossorigin></script>
   <script src="https://cdn.tryretool.com/js/react-dom.production.min.js" crossorigin></script>
       <!-- get the HTML to PDF bundle -->
   <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.9.3/html2pdf.bundle.min.js"></script>

<script>
    // subscribe to model changes
 window.Retool.subscribe(function(model) {
   if (!model) { return }
   var content = model.content;

    // move our printed content into an element we can access later 
   document.getElementById("templateContents").innerHTML = content;   
 });

  
 function pdprint() {
     // our function to generate and display the pdf in a new window
   var opt = {
      margin:       0.5,
      filename:     'myfile.pdf',
      image:        { type: 'jpeg', quality: 0.98 },
      html2canvas:  { scale: 2 },
      jsPDF:        { unit: 'in', format: 'letter', orientation: 'portrait' }
  };

  window.html2pdf().set(opt)
    .from(document.getElementById("templateContents").innerHTML)
    .toPdf()
    .output('bloburl')
    .then(function(pdfas) {  window.open(pdfas) } );

 }
</script>

      <!-- now, you just need a button to trigger the printing (you must take an action to open the new window) -->
   <button id="pdfDownloadLink" href="" onclick="pdprint(); return false">Download PDF</button>
        <!-- ... and a place to put our html conten -->
   <div id="templateContents" style="display:none"></div>
 </body>
</html>

That's it, now you have a button that when you click it, it will put the contents of the variable you referenced in the model into a PDF and then open that PDF in a new window.

They key here is that the opening of the PDF in a new window is triggered as a result of you hitting the button (to get around sandbox security issues), and you don't rely on the native print/save function from html2pdf, but instead you take the blob as a datauri, and pass it to window.open().

As you now have full control over the CSS (within the custom component), you can go all out, and make your PDFs look great without relying on any 3rd-party services. You will, of course, need to flex your CSS muscles as printing to a PDF has its own little gotchas.

3 Likes

Wait, you aren't going to make my HTML for me as well!? :laughing: :laughing:

Nicely done!

I may play with this to replace Carbone.io for some things, but I am also creating xlsx files with Carbone so can't get rid it entirely, sigh.) Carbone is still a viable option for those less skillful in HTML/CSS and more do in MS Word or need to create more than PDFs.

@JoeyKarczewski you might find this interesting ^

1 Like

I tried this out and want to report back on some pros/cons I encountered.

The cons with html2pdf were threefold:

  1. There is a known issue with PDF size generated via html2pdf. html2pdf uses the canvas object and there are limitations on its size. I ended up with a couple dozen blank pages. There are workarounds involving breaking down your PDF into multiple pieces and combine them back together - and this can be complex. If you have short documents, then there are no issues with this.

  2. As @church mentioned you need a good foundation in html and css. I newly tried some of these drag/drop html editors and they still create Frankenstein code that you have to edit after the fact. If you want to make changes, do you just change the code directly or use the html editor and do your post processing again?

  3. And you need to edit/build the HTML dynamically. If you are just replacing fields then liberal use of {{ }} within an iFrame or Custom component will suffice. If you need to create tables that loop through query results and such, this is more work, but doable.

Cons with Carbone:

  1. Costs money. How much depends on your volume.
  2. A little more code overall, unless you need to do a lot of work to make your dynamic html.

So if you need a simpler, smaller PDF then html2pdf looks like a solid option. If you need a more complex, larger PDF then consider Carbone.