Blog Post

How to Build a Web Component PDF Viewer with PSPDFKit

Igor Perzić
Illustration: How to Build a Web Component PDF Viewer with PSPDFKit

In this post, we’ll look at how you can build a custom web component PDF viewer that uses the PSPDFKit for Web SDK.

Creating the PDF Viewer

Let’s start by creating the PDF viewer the same way as we’d integrate PSPDFKit for Web into a vanilla JavaScript project.

Vanilla JavaScript

For the purposes of this blog post, follow the steps outlined below.

  1. Download the SDK, and copy the entire contents of the dist folder to your assets folder.

  2. Add a PDF example document to the root folder. Feel free to use our demo PDF.

  3. Create a basic HTML file and insert an empty div element with a unique ID, width, and height defined:

<div id="pspdfkit" style="width: 100%; height: 100vh;"></div>
  1. Include the PSPDFKit for Web SDK in the HTML:

<script src="assets/pspdfkit.js"></script>
  1. Load PSPDFKit for Web with the example document:

<script>
	PSPDFKit.load({
		container: '#pspdfkit',
		document: 'example.pdf',
	});
</script>

ℹ Info: For a more extensive guide to integrating PSPDFKit for Web into a vanilla JavaScript project, check out our getting started guide.

Following the steps above, our HTML document now looks like this:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>Title</title>
		<style>
			* {
				margin: 0;
				padding: 0;
			}
		</style>
	</head>
	<body>
		<div id="pspdfkit" style="width: 100%; height: 100vh;"></div>

		<script src="assets/pspdfkit.js"></script>

		<script>
			PSPDFKit.load({
				container: '#pspdfkit',
				document: 'example.pdf',
			});
		</script>
	</body>
</html>

To serve this folder using a server and see our vanilla JavaScript PDF viewer in action, execute the following command:

npx serve

After the server success message appears in the terminal, the local address will be copied to the clipboard automatically. Open a web browser and paste the link from the clipboard to see the PDF viewer.

Although this approach is nice and fast, it’s not very reusable. Fortunately, we can use Web Components to improve it, as they move all the unnecessary logic into a single component, which is independent and reusable.

Web Components

Web Components are a set of three main technologies that allow users to create their own reusable custom HTML elements with interoperability and encapsulated functionality.

The three main technologies are:

  • Custom elements — A set of JavaScript APIs that allow you to build custom HTML elements and their functionality.

  • Shadow DOM — A set of JavaScript APIs that allow the web browser to render DOM elements but not inside main DOM tree. This provides encapsulation so that it’s hidden from other code on the page.

  • HTML templates — The mechanism for writing HTML that won’t be rendered on the page. Templates can serve as the basis for a custom element’s structure.

To create a custom element, we first need to write a class that defines its functionality, together with some lifecycle methods. Go ahead and create a new file in the root folder, call it web-component.js, and place the following code inside:

class PSPDFKitViewer extends HTMLElement {
	static get observedAttributes() {
		// Here we define attributes this element will accept.
		// In our case, we'll display a string path to the PDF.
		return ['src'];
	}

	constructor() {
		super();
	}

	connectedCallback() {
		// This lifecycle method executes each time a custom element is appended into a document-connected element.
		console.log('[pspdfkit-viewer]: mounted');
	}

	attributeChangedCallback(attr, oldVal, val) {
		// This method executes every time an attribute is added, changed, or removed.
		console.log(`[pspdfkit-viewer]: ${attr} attribute changed`);
	}
}

// This is how a new custom element is registered and made available for use.
customElements.define('pspdfkit-viewer', PSPDFKitViewer);

Now we can use the custom element and provide the PDF source path, as long as we import the web-component.js script too:

<pspdfkit-viewer src="example.pdf"></pspdfkit-viewer>
<!-- Import `web-component.js` in order to use our custom HTML element.-->
<script type="module" src="web-component.js"></script>

Adding Functionality

This is all great, but our custom element still doesn’t do anything other than logging to the web browser console that the src attribute changed. So let’s go ahead and add real functionality to the web-component.js file:

+ import './assets/pspdfkit.js'
+
class PSPDFKitViewer extends HTMLElement {
+ readyPromise;
+ resolveReady;
+
  static get observedAttributes() {
    // Here we define attributes this element will accept.
    // In our case, we'll display a string path to the PDF.
    return ["src"];
  }

  constructor() {
    super();
+
+   this.readyPromise = new Promise((resolve) => {
+  	this.resolveReady = resolve;
+   });
  }

 connectedCallback() {
-  // This lifecycle method executes each time a custom element is appended into a document-connected element.
-  console.log('[pspdfkit-viewer]: mounted');
+  this.resolveReady();
+  this.attachShadow({ mode: 'open' });
+  this.shadowRoot.innerHTML = `<div style="width: 100%; height: 100vh;"></div>`
 }

  attributeChangedCallback(attr, oldVal, val) {
-  // This method executes every time an attribute is added, changed, or removed.
-  console.log('[pspdfkit-viewer]: src attribute changed');
+  if (attr === 'src' && val !== null) {
+    this.loadPdf(val);
+  }
  }
+
+ async loadPdf(path) {
+   await this.readyPromise;
+
+   await PSPDFKit.load({
+     baseUrl: `${window.location.protocol}//${window.location.host}/assets/`,
+     container: this.shadowRoot.querySelector("div"),
+     document: path,
+   });
+ }
}

// This is how a new custom element is registered and made available for use.
customElements.define('pspdfkit-viewer', PSPDFKitViewer);

There are some things from the changes above that we need to mention. The most obvious change is the addition of the loadPdf asynchronous method. This actually loads PSPDFKit for Web with a specific configuration once the custom element has been inserted into the DOM.

In the PSPDFKit for Web configuration object, there are three things we need to set:

  • baseUrl — A property that tells PSPDFKit for Web where to look for its other files.

  • container — Either the CSS selector or the actual DOM element where the PDF will be displayed.

  • document — A string path to the PDF we want to display.

ℹ Info: To see all configuration options available to you when loading a PDF document, check out our API reference documentation.

In addition to the loadPdf method, we used the attachShadow built-in method to attach a shadow DOM tree, and we introduced a mechanism in the form of readyPromise to let the component know when it has been inserted into the DOM.

Now that we’ve changed the web-component.js file, we need to change the index.html file too, since we’re using the custom element directly:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
     <title>Title</title>
     <style>
         * {
             margin: 0;
             padding: 0;
         }
     </style>
  </head>
  <body>
-   <div id="pspdfkit" style="width: 100%; height: 100vh;"></div>
+   <pspdfkit-viewer src="example.pdf"></pspdfkit-viewer>

-   <script src="assets/pspdfkit.js"></script>
+   <script type="module" src="web-component.js"></script>
-
-  <script>
-    PSPDFKit.load({
-      container: "#pspdfkit",
-      document: "example.pdf",
-    })
-  </script>
  </body>
</html>

Changes to the index.html file are fairly simple: We’re removing everything related to PSPDFKit for Web (because we’ve moved it to the custom element) and replacing it with an import of the custom web component.

One important change that needs to be mentioned is that the addition of type="module" to the script tag is necessary. Otherwise, we wouldn’t have been able to import the pspdfkit.js module into our custom HTML component.

And that’s basically it: Our custom element is ready to be used. If you restart the server you previously started, you’ll be able to see an example PDF loaded using a custom element.

To see all the changed files, click below to expand individual sections.

Project Structure
pspdfkit-web-component-example
├─ assets
│  ├─ modern
│  │	└─ ...
│  ├─ pspdfkit-lib
│  │	└─ ...
│  ├─ index.d.ts
│  └─ pspdfkit.js
├─ example.pdf
├─ index.html
└─ web-component.js
HTML
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<title>Title</title>
		<style>
			* {
				margin: 0;
				padding: 0;
			}
		</style>
	</head>
	<body>
		<pspdfkit-viewer src="example.pdf"></pspdfkit-viewer>
		<script type="module" src="web-component.js"></script>
	</body>
</html>
JavaScript
import './assets/pspdfkit.js';

class PSPDFKitViewer extends HTMLElement {
	readyPromise;
	resolveReady;

	static get observedAttributes() {
		return ['src'];
	}

	constructor() {
		super();

		this.readyPromise = new Promise((resolve) => {
			this.resolveReady = resolve;
		});
	}

	connectedCallback() {
		this.resolveReady();
		this.attachShadow({ mode: 'open' });
		this.shadowRoot.innerHTML = `<div style="width: 100%; height: 100vh;"></div>`;
	}

	attributeChangedCallback(attr, oldVal, val) {
		if (attr === 'src' && val !== null) {
			this.loadPdf(val);
		}
	}

	async loadPdf(path) {
		await this.readyPromise;

		await PSPDFKit.load({
			baseUrl: `${window.location.protocol}//${window.location.host}/assets/`,
			container: this.shadowRoot.querySelector('div'),
			document: path,
		});
	}
}

customElements.define('pspdfkit-viewer', PSPDFKitViewer);

Next Steps

We successfully used two of three technologies provided to us for building Web Components. This is because our example is simple enough that it doesn’t require using HTML templates.

If you’d like to improve upon this example, I encourage you to try and refactor it so that it uses HTML templates. While refactoring, you can even try to add a dropdown to the web component you created to change the PSPDFKit for Web locale. Check out our localization guide for information on how to change the PSPDFKit for Web locale during runtime.

Apart from using templates and adding new features, we could try bundling our app using a common bundler such as webpack. Doing this will make our custom element better supported across browsers and allow use of npm modules. However, we’re still limited by Web Components browser support, but fortunately, there’s an available polyfill.

Conclusion

In this blog post, we saw how we can use Web Components to hide the unnecessary implementation details of displaying a PDF using PSPDFKit for Web. If you hit any snags, don’t hesitate to reach out to our support team for help.

You can also deploy our vanilla JavaScript PDF viewer or use one of our many web framework deployment options like Vue.js, React.js, and jQuery. To see a list of all web frameworks, start your free trial. Or, launch our demo to see our viewer in action.

Related Products
Share Post
Free 60-Day Trial Try PSPDFKit in your app today.
Free Trial

Related Articles

Explore more
TUTORIALS  |  Web • PDF.js • JavaScript • How To • PDF Viewer

How to Print PDFs Using PDF.js

BLOG  |  Web • C++ • WebAssembly

Render Performance Improvements in PSPDFKit for Web

PRODUCT  |  Web

Handling Browser Support of Media Files in PSPDFKit for Web