Generate PDF in Angular with PDFMake

6 min read


Recently I worked on a feature for a client that involved generating a PDF document from his Angular web application. After some research we decided to use PDFMake for this purpose.

PDFMake is an excellent Javascript library for generating PDF documents. This short post is going to discuss how we can integrate the PDFMake library with an Angular 9 app in a way that does not increase our initial bundle size!

Why PDFMake?

We chose PDFMake because it allows us to specify the data for generation using a document definition object format. Other libraries required absolute positioning to position our content in the document. Since we had quite a lot of data and formatting to do, having an easier format saved us a lot of time!

To know more about PDFMake's document definition object format, please refer to their official documentation.

With this out of the way, let's start integrating the library into an Angular app!

Setting it up

First, we'll create a new Angular 9 app by executing the following commands in our terminal or console.

ng new angular-pdf-generator --routing=false

Let's also add the Angular Material library, so we can use a material button to allow the user to generate the PDF.

ng add @angular/material

Next, let's include the required material modules in our app.module.ts file.

import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";

import { AppComponent } from "./app.component";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { MatToolbarModule } from "@angular/material/toolbar";
import { MatButtonModule } from "@angular/material/button";
import { MatProgressSpinnerModule } from "@angular/material/progress-spinner";

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    MatToolbarModule,
    MatButtonModule,
    MatProgressSpinnerModule,
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

Finally, here is a simple UI for our purposes.

<mat-toolbar color="primary"> PDF Generator with PDFMake </mat-toolbar>

<button mat-raised-button color="primary">Generate PDF</button>

A toolbar and a button will do the trick for now! If you want to make complex grids using Angular Material, here's a shameless plug of another of my article on creating a responsive card grid in Angular.

Adding PDFMake to the app

Let's install PDFMake now from npm using the following command.

npm install pdfmake --save

For a better structure to our app, let's encapsulate all of our PDF generation functions inside of a service. We can create that using the following command.

ng generate service pdf

Static import and its effect on app bundle size

At this point, the typical way to include a Javascript library would be to use a static import statement at the top of our service file and then use the imported object for PDF generation. For PDFMake, it can be as follows.

import pdfMake from "pdfmake/build/pdfmake";
import pdfFonts from "pdfmake/build/vfs_fonts";
pdfMake.vfs = pdfFonts.pdfMake.vfs;

However, PDFMake is big library with a bundle size of approximately 3.7 MB. If we use the above approach, it is going to make our app bundle size unnecessarily large, even if only a part of our app needs the PDF generation. With an increasing focus on web performance nowadays, this would be an unwise move!

Add Lazy Loading to our app

To handle such cases, Angular 9 allows us to use lazy loading for our packages. Lazy loading means we're only going to load our PdfMake library when we actually need it. In our case we're going to do it on our button click event.

For lazy loading, we can use webpack's awesome new dynamic import feature. Dynamic imports load our package on the fly and resolves to a Promise - so we can continue our work using the package just loaded.

Let's see how we add it to our Pdf service.

export class PdfService {
  pdfMake: any;

  constructor() {}

  async loadPdfMaker() {
    if (!this.pdfMake) {
      const pdfMakeModule = await import("pdfmake/build/pdfmake");
      const pdfFontsModule = await import("pdfmake/build/vfs_fonts");
      this.pdfMake = pdfMakeModule.default;
      this.pdfMake.vfs = pdfFontsModule.default.pdfMake.vfs;
    }
  }

  async generatePdf() {
    await this.loadPdfMaker();

    const def = {
      content: "A sample PDF document generated using Angular and PDFMake",
    };
    this.pdfMake.createPdf(def).open();
  }
}

Let's go over what we did here.

First, we created a variable in our service to contain the pdfMake reference when we load it using the import statement.

Then we create an async function to load the PDFMake library. We use two dynamic imports because fonts for PDFMake also need to be imported and set for it to work properly. Since both resolve to a Promise, we can use await keyword to wait on the imports before moving forward in the execution.

Lastly, we just use the load function inside of our main generatePdf function, so that our library is loaded before we go on using it!

We're using a simple definition object here. PDFMake provides us a lot of options to create well formatted PDF documents, including with images!

For more details refer to their official documentation.

Calling the service from our component

As a last step, let's add the code to call our service from our component (after linking it with the button).

export class AppComponent {
  constructor(private pdfService: PdfService) {}

  generatePdf() {
    this.pdfService.generatePdf();
  }
}

Testing it all out!

Let's do ng serve and quickly test this out. Let's open up our Developer Tools on the Network tab as well.

When we run our app now and click the 'Generate PDF' button, we should see the PDF generated and opened up in a new tab. Cool!

What's more, if we look closely at the Network tab, we'll see the following two libraries were lazy loaded when we clicked the button. Check out their sizes!

This is exactly what we wanted! Our app's initial size remains the same and we only load the PdfMake library when we need to - which saves us a hefty 3.7 MB in bundle size.

How this works

This works due to a webpack feature called dynamic imports. As soon as webpack notices an import statement in your code with a package name inside it, it will automatically create a separate bundle file for that package during compilation.

This will then be used for lazy loading at runtime. Without us having to do anything else. Sweet!

If you'd like to read up more on this webpack feature, here is the official documentation.

Conclusion

As you can see, it is now quite easy and convenient for us to lazy load our packages when we feel like at runtime in our Angular apps. This opens up a lot of possibilities esp for improving our app's load performance. The less your initial bundle size, the better it is for your Lighthouse score (which is getting increasingly important nowadays).

I hope you found this useful. Post a comment below if you want to share something interesting!

The code for this tutorial is available on this github repository.

Thanks for reading! Bye :)

Check out my Angular and Firebase Authentication crash course

thumbnail
Angular Firebase Authentication: Create Full Sign Up App

Use Angular 16, Angular Material and Firebase Authentication, Firestore and Storage to create a complete Sign Up App!

You may also like...