Handling PDF files within a web application has always been painful to deal with. If you're lucky, your users only need to download the file. Sometimes, though, your users need more. In the past, I've been lucky, but this time, our users needed our application to display a PDF document so they could save metadata related to each individual page. Previously, one might have accomplished this with an expensive PDF plugin, such as Adobe Reader, running inside the browser. However, with some time and experimentation, I found a better way to integrate PDF viewers in a web application. Today, we'll take a look at how we can simplify PDF handling, using Aurelia and PDF.js.
Overview: The Goal
Our goal, today, is to build a PDF viewer component in Aurelia that allows two-way data flow between the viewer and our application. We have three main requirements.
- We want the user to be able to load the document, scroll, and zoom in and out, with decent performance.
- We want to be able to two-way-bind viewer properties (such as the current page, and the current zoom level) to properties in our application.
- We want this viewer to be a reusable component; we want to be able to drop multiple viewers into our application simultaneously with no conflicts and little effort.
You can find the code for this tutorial on our GitHub repo, as well as a demo of the finished code here.
Introducing PDF.js
PDF.js is a JavaScript library, written by the Mozilla Foundation. It loads PDF documents, parses the file and associated metadata, and renders page output to a DOM node (typically a <canvas>
element). The default viewer included with the project powers the embedded PDF viewer in Chrome and Firefox, and can be used as a standalone page or as a resource (embedded within an iframe).
This is, admittedly, pretty cool. The problem here is that the default viewer, while it has a lot of functionality, is designed to work as a standalone web page. This means that while it can be integrated within a web application, it essentially would have to operate inside an iframe sandbox. The default viewer is designed to take configuration input through its query string, but we can't change configuration easily after the initial load, and we can't easily get info and events from the viewer. In order to integrate this with an Aurelia web application — complete with event handling and two-way binding — we need to create an Aurelia custom component.
Note: if you need a refresher on PDF.js, check out our tutorial: Custom PDF Rendering in JavaScript with Mozilla’s PDF.js
The Implementation
To accomplish our goals, we're going to create an Aurelia custom element. However, we're not going to drop the default viewer into our component. Instead, we're going to create our own viewer that hooks into the PDF.js core and viewer libraries, so that we can have maximum control over our bindable properties and our rendering. For our initial proof-of-concept, we'll start with the skeleton Aurelia application.
The boilerplate
As you can see if you follow the link above, the skeleton app has a lot of files in it, many of which we're not going to need. To make life simpler, we have prepared a stripped down version of the skeleton, to which we have added a couple of things:
- A Gulp task to copy our PDF files to the
dist
folder (which Aurelia uses for bundling).
- The PDF.js dependency has been added to
package.json
.
- In the root of the app,
index.html
and index.css
have received some initial styling.
- Empty copies of the files we're going to be working in have been added.
- The file
src/resources/elements/pdf-document.css
contains some CSS styling for the custom element.
So let's get the app up and running.
First off, ensure that gulp and jspm are installed globally:
npm install -g gulp jspm
Then clone the skeleton and cd
into it.
git clone git@github.com:sitepoint-editors/aurelia-pdfjs.git -b skeleton
cd aurelia-pdfjs
Then install the necessary dependencies:
npm install
jspm install -y
Finally run gulp watch
and navigate to http://localhost:9000. If everything worked as planned, you should see a welcome message.
Some more set-up
The next thing to do is to find a couple of PDFs and place them in src/documents
. Name them one.pdf
and two.pdf
. To test our custom component to the max, it would be good if one of the PDFs were really long, for example War and Peace which can be found on the Gutenberg Project.
With the PDFs in place, open up src/app.html
and src/app.js
(by convention the App
component is the root or the Aurelia app) and replace the code that is there with the contents of these two files: src/app.html and src/app.js. We'll not touch on these files in this tutorial, but the code is well commented.
Gulp will detect these changes automatically and you should see the UI of our app render. That's it for the setup. Now it's on with the show ...
Creating an Aurelia custom element
We want to create a drop-in component that can be used in any Aurelia view. Since an Aurelia view is just a fragment of HTML wrapped inside of an HTML5 template tag, an example might look like this:
<template>
<require from="resources/elements/pdf-document"></require>
<pdf-document url.bind="document.url"
page.bind="document.pageNumber"
lastpage.bind="document.lastpage"
scale.bind="document.scale">
</pdf-document>
</template>
The <pdf-document>
tag is an example of a custom element. It, and its attributes (like scale
and page
) aren't native to HTML, but we can create this using Aurelia custom elements. Custom elements are straightforward to create, using the basic building blocks of Aurelia: Views and ViewModels. As such, we'll first scaffold our ViewModel, named pdf-document.js
, like so:
// src/resources/elements/pdf-document.js
import {customElement, bindable, bindingMode} from 'aurelia-framework';
@customElement('pdf-document')
@bindable({ name: 'url' })
@bindable({ name: 'page', defaultValue: 1, defaultBindingMode: bindingMode.twoWay })
@bindable({ name: 'scale', defaultValue: 1, defaultBindingMode: bindingMode.twoWay })
@bindable({ name: 'lastpage', defaultValue: 1, defaultBindingMode: bindingMode.twoWay })
export class PdfDocument {
constructor () {
// Instantiate our custom element.
}
detached () {
// Aurelia lifecycle method. Clean up when element is removed from the DOM.
}
urlChanged () {
// React to changes to the URL attribute value.
}
pageChanged () {
// React to changes to the page attribute value.
}
scaleChanged () {
// React to changes to the scale attribute value.
}
pageHandler () {
// Change the current page number as we scroll
}
renderHandler () {
// Batch changes to the DOM and keep track of rendered pages
}
}
The main thing to notice here is the @bindable
decorator; by creating bindable properties with the configuration defaultBindingMode: bindingMode.twoWay
, and by creating handler methods in our ViewModel (urlChanged
, pageChanged
, etc) we can monitor and react to changes to the associated attributes that we place on our custom element. This will allow us to control our PDF viewer simply by changing properties on the element.
Then, we'll create the initial view to pair with our ViewModel.
// src/resources/elements/pdf-document.html
<template>
<require from="./pdf-document.css"></require>
<div ref="container" class="pdf-container">
My awesome PDF viewer.
</div>
</template>
Integrating PDF.js
PDF.js is split into three parts. There's the core library, which handles parsing and interpreting a PDF document; the display library, which builds a usable API on top of the core layer; and finally, the web viewer plugin, which is the prebuilt web page we mentioned before. For our purposes, we'll be using the core library through the display API; we'll be building our own viewer.
Continue reading %Adventures in Aurelia: Creating a Custom PDF Viewer%
by Jedd Ahyoung via SitePoint