Angular
Angular uses Free PDF SDK through its Web Component, <free-pdf-viewer>. You register the
element once, allow custom elements in your component, and then bind to it like any other
element: attributes and properties in, fps-* events out.
Install
npm install free-pdf-sdk
npx free-pdf-sdk copy-viewer public/free-pdf-sdkThe second command copies the viewer files (viewer/, version.json, the licences) into a
folder your app serves. Projects created with Angular 18 or later serve public/ at the site root, so the viewer is
at /free-pdf-sdk/. In an older project that uses src/assets/, copy it to
src/assets/free-pdf-sdk and use /assets/free-pdf-sdk/. That URL is the base-url, and it
must be on the same origin as the app.
Run the command again after every upgrade, for example from a postinstall script:
{
"scripts": {
"postinstall": "free-pdf-sdk copy-viewer public/free-pdf-sdk"
}
}Register the element
Call defineFreePdfViewer() in main.ts, before bootstrapping:
// src/main.ts
import { bootstrapApplication } from "@angular/platform-browser";
import { defineFreePdfViewer } from "free-pdf-sdk/element";
import { appConfig } from "./app/app.config";
import { AppComponent } from "./app/app.component";
defineFreePdfViewer();
bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err));Use it in a standalone component
Add CUSTOM_ELEMENTS_SCHEMA so Angular accepts <free-pdf-viewer> and its bindings:
// src/app/contract-viewer.component.ts
import { CUSTOM_ELEMENTS_SCHEMA, Component } from "@angular/core";
@Component({
selector: "app-contract-viewer",
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<free-pdf-viewer
base-url="/free-pdf-sdk/"
src="/files/contract.pdf"
mode="review"
></free-pdf-viewer>
`,
styles: [`free-pdf-viewer { display: block; height: calc(100vh - 64px); }`],
})
export class ContractViewerComponent {}The element is a block with a min-height of 200px; the viewer fills it. Give it a height in
your component's styles, as above.
Bind options
Static values can be plain attributes. For values from your component, bind properties where the element has them and attributes otherwise.
The element has these properties: baseUrl, mode, features, ui and theme, plus
options, which takes several options at once (document, author, locale, password,
disabledElements, timeoutMs, and the ones above). Objects such as features and ui are best
bound as properties, so no JSON is involved.
import { CUSTOM_ELEMENTS_SCHEMA, Component, signal } from "@angular/core";
import type { FeatureFlags, Mode, UiOptions } from "free-pdf-sdk";
@Component({
selector: "app-review",
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<select [value]="mode()" (change)="mode.set($any($event.target).value)">
<option value="view">View</option>
<option value="review">Review</option>
<option value="annotate">Annotate</option>
</select>
<free-pdf-viewer
base-url="/free-pdf-sdk/"
[attr.src]="pdfUrl"
[mode]="mode()"
[features]="features"
[ui]="ui"
[options]="{ author: userName, disabledElements: ['download', 'print'] }"
[attr.theme]="dark ? 'dark' : 'light'"
></free-pdf-viewer>
`,
styles: [`free-pdf-viewer { display: block; height: 80vh; }`],
})
export class ReviewComponent {
pdfUrl = "/files/contract.pdf";
userName = "Priya Raman";
dark = false;
mode = signal<Mode>("review");
features: FeatureFlags = { stamps: false, measurement: false };
ui: UiOptions = { quickToolbar: false };
}Every change calls the matching API method on the running viewer (mode calls setMode, a new
src opens that document, and so on), batched per task. A refused change is reported as an
fps-error event. Changing base-url mounts a new viewer.
Keep these in mind:
- Assign a new object to change
featuresorui; Angular only re-binds when the reference changes.featuresreplaces the overrides (relative to the mode);uioptions you leave out keep their value. optionsmerges into what was set before, and a value set through a property wins over the attribute of the same name from then on.- To open a document from bytes (an upload, a
Blobfrom your API), set it throughoptions:this.pdf.nativeElement.options = { document: { data: file, name: file.name } }(with theViewChildshown below), or bind[options]="{ document: { data: fileBytes, name: 'scan.pdf' } }". - Attributes are strings. Use
[attr.disabled-elements]="'download print'"(comma or space separated) and[attr.features]only with JSON text; properties are simpler.
Listen to events
The element dispatches DOM CustomEvents with the data in detail. Bind them with Angular's
event syntax:
| Event | detail |
|---|---|
(fps-ready) |
{ sdk, pdfcraftRev } |
(fps-document-loaded) |
{ pageCount, permissions } |
(fps-document-error) |
{ code, message } |
(fps-annotations-changed) |
{ count } |
(fps-dirty-changed) |
{ dirty } |
(fps-error) |
{ code, message } |
(fps-save-requested) |
{ name, saveAs, data } (data is the saved PDF as a Uint8Array) |
@Component({
selector: "app-counter",
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<p>{{ count }} comments</p>
<free-pdf-viewer
base-url="/free-pdf-sdk/"
src="/files/contract.pdf"
mode="annotate"
(fps-annotations-changed)="onAnnotationsChanged($event)"
(fps-error)="onError($event)"
></free-pdf-viewer>
`,
styles: [`free-pdf-viewer { display: block; height: 80vh; }`],
})
export class CounterComponent {
count = 0;
onAnnotationsChanged(event: Event) {
this.count = (event as CustomEvent<{ count: number }>).detail.count;
}
onError(event: Event) {
const { code, message } = (event as CustomEvent<{ code: string; message: string }>).detail;
console.error(code, message);
}
}Call the API
Get the element with ViewChild and an ElementRef. Its whenReady() resolves with the
viewer's full API once the viewer is ready and the document from src is open; its viewer
property returns the API synchronously, or null before then.
import { CUSTOM_ELEMENTS_SCHEMA, Component, ElementRef, ViewChild } from "@angular/core";
import { FreePdfError } from "free-pdf-sdk";
import type { FreePdfViewerElement } from "free-pdf-sdk/element";
@Component({
selector: "app-contract-review",
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<header>
{{ count }} comments
<button (click)="saveComments()">Save comments</button>
<button (click)="downloadPdf()">Download PDF</button>
{{ status }}
</header>
<free-pdf-viewer
#pdf
base-url="/free-pdf-sdk/"
[attr.src]="'/api/contracts/' + contractId + '/pdf'"
mode="annotate"
[options]="{ author: userName }"
(fps-document-loaded)="restoreComments()"
(fps-annotations-changed)="count = $any($event).detail.count"
></free-pdf-viewer>
`,
styles: [
`:host { display: flex; flex-direction: column; height: 100vh; }
free-pdf-viewer { display: block; flex: 1; }`,
],
})
export class ContractReviewComponent {
@ViewChild("pdf") pdf!: ElementRef<FreePdfViewerElement>;
contractId = "42";
userName = "Priya Raman";
count = 0;
status = "";
async restoreComments() {
const viewer = await this.pdf.nativeElement.whenReady();
const res = await fetch(`/api/contracts/${this.contractId}/annotations`);
if (res.ok) await viewer.importXfdf(await res.text(), { replace: true });
}
async saveComments() {
try {
const viewer = await this.pdf.nativeElement.whenReady();
const xfdf = await viewer.exportXfdf();
await fetch(`/api/contracts/${this.contractId}/annotations`, {
method: "PUT",
headers: { "Content-Type": "application/vnd.adobe.xfdf" },
body: xfdf,
});
this.status = "Saved";
} catch (e) {
this.status = e instanceof FreePdfError ? `Could not save (${e.code})` : "Could not save";
}
}
async downloadPdf() {
const viewer = await this.pdf.nativeElement.whenReady();
const bytes = await viewer.save();
const url = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));
const a = document.createElement("a");
a.href = url;
a.download = "contract.pdf";
a.click();
URL.revokeObjectURL(url);
}
}whenReady() can be called at any time, including in ngAfterViewInit: it resolves once the
viewer is ready and its first document has loaded (or failed to).
Cleanup is automatic
You don't need ngOnDestroy. When Angular removes <free-pdf-viewer> from the DOM (the
component is destroyed, an @if turns false, the route changes), the element destroys its viewer
and pending calls reject with DESTROYED. When it is rendered again, a new viewer mounts.
Server-side rendering
The element only works in the browser. defineFreePdfViewer() in main.ts runs only in the
browser build; with Angular SSR, keep free-pdf-sdk/element out of main.server.ts and of code
that runs on the server. On the server, <free-pdf-viewer> renders as an empty element and
mounts once the app is running in the browser.
Things to know
- The viewer runs in an iframe served from
base-url, on your origin, and talks to the page overpostMessage. PDFs are processed in the browser with WebAssembly and are not uploaded anywhere by the SDK. - The viewer needs WebGL. If it can't start,
fps-errorreportsTIMEOUT(usually a wrongbase-url) orINTERNAL(the viewer loaded but could not start, for example without WebGL). - If a document can't be opened,
(fps-document-error)reports why and the viewer keeps running; bind anothersrcto try again. - Web Component describes every attribute, property and event of the element in full.