2016-08-17

PDFs in JavaScript are a pain in the butt. In the past, several gallant attempts were made but most fell short. We could get really close to getting a quality PDF, but it was plagued by poor rendering, or truckloads of code to get the result we were after. No bueno. Fortunately, things have evolved quite a bit over the last few years. Now, combining a handful of solutions, we can get a decent PDF rendered using HTML and CSS as opposed to drawing lines by hand or some other tricky, not-so-fun approach.

As React moves closer and closer to ubiquity in the JavaScript community, it’s a keen choice to use for generating the markup we’ll need for a PDF. In this tutorial, we’re going to learn how to make use of React’s server-side rendering capabilities (via the react-dom library) to convert a component to HTML and then render a PDF from that HTML using the fabulous html-pdf library by Marc Bachmann. Finally, we’ll learn how to take the PDF that’s generated, convert it to base64 and then toss it to the client for downloading. Hurry please, we have so much time and so little to see. Wait a minute, strike that, reverse it.

Installation

For this sinppet, we’ll want to make sure we clone a copy of Base and run npm install to pull in all of its dependencies. In addition to the dependencies loaded with Base, we’ll need to install the following packages:

Terminal

This package will give us access to the html-pdf library that we’ll use to generate PDFs.

Terminal

Because our PDFs will not have access to our application’s CSS, we’ll use the react-inline-css package to help us write inline styles for the component we’ll be rendering.

Terminal

We’ll use the file-saver package to help us download our PDF on the client once it’s ready to go.

If you want to skip all of this, you can also download the full source of this snippet here.

Updating our schema and AddDocument component

Before we dig into generating our PDFs, we’ll need to make a little change to two things: the schema of our Documents collection and our <AddDocument /> component. We’ll be updating these to support a new body field in addition to the existing title field.

/imports/api/documents/documents.js

Modifying the existing Documents collection in Base, here, all we’re doing is adding in a new body property to our document’s schema. Pretty simple! Why are we doing this? Well, a PDF with just a title would be pretty boring, so this gives us some more content to work with. Technically speaking this is optional, so if you want to leave it out go for it! Just make sure to snip out any references to body later on.

/imports/ui/components/add-document.js

Our <AddDocument /> component is seeing some more substantial changes. In Base, we start with just a single title input. Here, we’re adding a new textarea field to support our new body property. The big change here is how we’re handling our call to insert our new document. In Base, we have a simple onKeyUp event attached to our title input. Here, we’re eschewing that in favor of converting <AddDocument /> into a more traditional form with a submit button. When the form is submitted, we call its onSubmit function which points to our handleInsertDocument method defined up above.

Inside handleInsertDocument, we make sure to prevent our form’s default functionality and then scoop up our title and body inputs via document.querySelector(). Next, if we can confirm that both field’s values are not empty, we go ahead and call to the insertDocument method on the server. If all goes well here, we simply reset our form and call it a day. If our title or body is empty, we make sure to throw an error for the user.

Cool! This was a tiny detail but very important. Next, we need to define (or rather, refactor) our Document component that we’ll pass to our PDF generator for rendering.

Defining our Document component

Our <Document /> component will be living a few lives during this tutorial. First, it will be used to render out freshly added documents in our interface, but it will also be used as the contents of our PDF. How?! Magic. To get started, let’s spec out our component and then step through what it’s doing.

/imports/ui/components/document.js

Not much to it, but pay close attention. Down in our render() method (we’re using a stateless functional component here), we’re pulling in another component from the react-inline-css component we added earlier and setting it as our root element. On that element, we pass a prop stylesheet which contains an ES2015 template string containing the CSS we want applied to our component. Yes, this is as cool as it looks. We’re literally writing vanilla CSS here and the <InlineCss /> component will take that and apply it to whatever it’s wrapping.

Inside <InlineCss />, notice that we have our title and body property being output, with an additional <Button /> component. What’s happening here? Well, if you take a close look at our CSS, our goal is to show this button when we’re viewing our <Document /> component in the browser, but hide it when we convert to PDF (hint: PDF’s respond to CSS’s @media print {} media query). So cool!

Last but not least, if we look close we’ll see that our <Button /> has an onClick event pointing to a method downloadPDF. Where is that? Let’s save that for later. For now, let’s jump up to the server and write the code we’ll need to actually convert our component to a PDF.

Handling PDF conversion

In order to generate our PDF, we’ll need to complete a few steps. First, we’ll want to take in our component and get back its raw HTML with props rendered into it. Once we have our HTML, we’ll take that and generate our PDF file from it. Lastly, we’ll take that PDF file and convert it to a base64 stringso that we can move it back to the client for download.

/imports/modules/server/generate-pdf.js

Woah smokies! This may seem like a lot, but bare with me. Here, we’re making use of a promise-based module to make our PDF generation a synchronous process. Why? Well, we want to ensure that we have a PDF file generated and a base64 string available to send back to the client before we signify to the client that we’re done. Using a Promise, we can more easily control this flow.

Starting toward the bottom, after we define our export generateComponentAsPDF, we make a call to our handler() method, passing in any options we received from our module’s invocation—more on this later—as well as our Promise’s resolve and reject arguments (we bundle these in a single object for convenience sake). Next, inside our handler, we take in our options using a bit of destructuring along with our promise. To make sure we can call our Promise’s resolve and reject methods from anywhere in our file, we assign them to a file-scoped variable module up top. This means that when we need to, we can call module.resolve() or module.reject() from anywhere in this file.

Now we’re into the good stuff. You’ll notice that we’re assigning the result of a call to generateComponentAsHTML() to a variable html for use later. If we look at generateComponentAsHTML(), there’s actually not much to it. Inside, we guard ourselves a bit using a try...catch statement. To handle our component’s conversion, we take it in as an argument and call it directly as a function, passing our props as an argument to it. Pay close attention. While we haven’t done it yet, when we eventually call this module, we’ll pass our Document component through which will be assigned to this generic component argument. So, imagine this code reading like ReactDOMServer.renderToStaticMarkup(Document(props));. Make sense?

Once we get our HTML back and rendered, we get into rendering our PDF. Back in our handler method, we call to generatePDF only if we have a value for html and fileName (we’ll pass this when we invoke our module). If we have both, we zip up to generatePDF and get to work. Inside, following the same try...catch pattern as before, we make use of our html-pdf dependency by calling pdf.create(). As arguments, we pass our html-ified version of our component in the first position, and then an object with some options on it as the second. Our options are just for style here and ensuring that the resulting PDF we generate is letter sized and that the margins (here, border) are set properly.

With our configuration in place, we take the output of pdf.create() and chain it into .toFile(). Can you guess what this is doing? This is taking our file and literally creating it on our server. Because we’ll only want this file around long enough to convert it to a base64 string, we place it in a /tmp directory to remind us to delete it later. As a callback to this, if we get an error during the generation we reject() our module, passing in whatever error message we receive. If all goes as planned, we resolve our module, passing in the name of our file and the base64 version of our PDF.

If we move up a bit, we’ll see that getBase64String is a pretty simple function that’s simply taking in the path to a file, using Node’s fs (file system) to read the file synchronously, and then returning a buffer of that file as a base64 string. If we move back down to generatePDF we can see that our path is coming from our .toFile() method as the .filename property on its response. Last but not least, because we have our base64 string at this point, we no longer need our PDF stored on the server. To blast it away, we use fs.unlink passing in the path we received back from .toFile(). Done!

Now, we have a means for generating our PDF. Moving forward, let’s wire up a method on the server that we can call from the client. To wrap things up, we’ll look at calling that method from the client and processing our download.

Exporting via base64 string

Now that we have our PDF generated and converted to a base64 string, we’re ready to get this thing out of the matrix and onto our computer! First step: we need a method to call from the client.

/imports/api/documents/server/methods.js

Because we’ll only want to run our PDF generation code on the server, here, we’re defining our file inside of documents/server. While we won’t cover it directly, this suggests that we need to add an additional import at /imports/startup/server/api.js for this file. If you try to call this and get a “does not exist” error, make sure to check this. Cool?

As for our actual method, here, we’re following the Base convention of using validated methods. In terms of work being done, the only argument we expect is for a documentId. The idea here is that when we click the <Button /> we defined inside of our <Document /> component later, we’ll send up the _id value of that specific document. When our method runs, it will grab that document from the Documents collection and then pass its contents to the generateComponentAsPDF module we wrote in the previous section.

Because we’re using a Promise-based module, we rely on the .then() and .catch() methods to help us either return our expected base64 string and fileName (as result, here), or, throw an error if one ocurrs in the module. Notice: we make sure to return our invocation of generateComponentAsPDF from our method to ensure the value we return makes it back to the client. One last note. Remember how we set up our module to take in a generic component earlier? Notice that at the top here, we’re literally importing our Document component and passing it to generateComponentAsPDF as the component property in our options object. Make a bit more sense now?

With all of this wired up, we’re ready to trigger a download on the client. Let’s do it!

Saving our file on the client

If we head back over to our <Document /> component now, we can wire up our missing downloadPDF method that will call to the server-side method we just defined.

/imports/ui/components/document.js

A few things going on here. First, when we call downloadPDF we pull in the _id of the document that we’re after from our <Button /> component’s data-id attribute. Why? Well, because we’re using a stateless component here, we don’t have access to a component instance to call this.props.document._id on. We could technically call a .bind() when passing downloadPDF but that feels a bit messy.

After we have our idea, we trigger a bit of state on our button to let our user know we’re downloading their file and then making our call to documents.download on the server. Notice, we’re calling our validated method by its name property, and not its exported name. Why? Remember that we defined our method inside of a /server directory. This means that our method is only accessible on the server. Using a traditional Meteor.call() we can get to it because Meteor will know what to look for. Make sense?

Now for the wild part. Once we get a response back from the server, we need to re-convert our base64 string into a file blob for downloading. To manage this, we’ve written a module base64ToBlob. We won’t look at it directly here, but you can review how it works in the repo for this tutorial. With our blob ready, we make use of our file-saver dependency, calling its .saveAs() method. We pass in the blob we just generated along with the file name we created on the server and blam! If all goes well after a few seconds we should see our PDF download.

Pretty awesome, right?! Go forth and generate PDFs. May the Almighty Brethren of Invoice guide you down a gentle and rewarding path.

Takeaways

We need to inline our CSS styles so that they’re properly bundled with our HTML. Using react-inline-css makes this a cinch.

In order to easily move our file between the server and the client, base64 is a quick and easy format.

When saving a file with file-saver we need to make sure that file is in a blob format.

Show more