Opening documents
Give the viewer the documents to open when it starts, and *epdfDocumentGate shows your content once
the document is ready:
import { Component } from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { engine } from './pdf';
import { Spinner } from './spinner';
@Component({
selector: 'app-contract-viewer',
imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, Spinner],
providers: [
provideEmbedPdf(
{ engine, initialDocuments: [{ source: { kind: 'url', url: '/files/contract.pdf' } }] },
withStage(),
withRender(),
),
],
template: `
<epdf-stage *epdfDocumentGate="let document; fallback: opening">
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
<ng-template #opening><app-spinner /></ng-template>
`,
})
export class ContractViewer {}The viewer is on screen at once; the document opens in the background. Components outside
*epdfDocumentGate, such as your toolbar’s frame, render right away.
Open a document later#
inject(EpdfDocuments) opens documents while the app runs, for example one the user picked:
import { Component, inject } from '@angular/core';
import { EpdfDocuments } from '@embedpdf/angular/runtime';
@Component({
selector: 'app-open-button',
template: `<input #picker type="file" accept="application/pdf" (change)="open(picker.files![0])" />`,
})
export class OpenButton {
private readonly documents = inject(EpdfDocuments);
protected async open(file: File) {
await this.documents.open({ kind: 'bytes', bytes: await file.arrayBuffer() }, { name: file.name });
}
}open() resolves with { document } once it’s open, and the new document becomes the active one.
Pass { activate: false } to open it in the background. Pick a PDF of your own here:
Loading live preview…
import { ChangeDetectionStrategy, Component, inject, ViewEncapsulation } from '@angular/core';
import {
EpdfDocument,
EpdfDocumentGate,
EpdfDocuments,
provideEmbedPdf,
} from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { localEngine } from '@embedpdf/engine';
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', bytes: new Uint8Array(await response.arrayBuffer()) };
};
@Component({
selector: 'demo-root',
imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer],
providers: [
provideEmbedPdf(
{ engine: () => localEngine(), initialDocuments: [{ source: ebook, name: 'ebook.pdf' }] },
withStage(),
withRender(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './open.css',
encapsulation: ViewEncapsulation.None,
template: `
<div class="toolbar">
<span class="document">
@if (document.id()) {
<strong>{{ document.name() ?? 'Untitled' }}</strong>
{{
document.status() === 'ready'
? ' · ' + document.pageCount() + ' pages'
: ' · ' + document.status()
}}
} @else {
No document open
}
</span>
<label class="button picker">
Open a PDF…
<input #picker type="file" accept="application/pdf" (change)="pick(picker)" />
</label>
@if (document.id()) {
<button type="button" class="button" (click)="documents.close(document.id())">Close</button>
} @else {
<button type="button" class="button" (click)="documents.open(ebook, { name: 'ebook.pdf' })">
Open the ebook again
</button>
}
</div>
<epdf-stage *epdfDocumentGate="let ready; fallback: empty" class="stage">
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
<!-- The gate's fallback: a document on its way, or none at all. -->
<ng-template #empty>
<p class="loading">
{{ document.id() ? 'Opening…' : 'No document is open. Open a PDF to see it here.' }}
</p>
</ng-template>
`,
})
export class App {
protected readonly documents = inject(EpdfDocuments);
protected readonly document = inject(EpdfDocument);
protected readonly ebook = ebook;
// A file the user picks opens next to the ebook, and becomes the active document.
protected async pick(picker: HTMLInputElement) {
const file = picker.files?.[0];
picker.value = '';
if (!file) return;
await this.documents.open(
{ kind: 'bytes', bytes: await file.arrayBuffer() },
{ name: file.name },
);
}
}When an input or a route parameter names the document, give it to the Stage instead:
<epdf-stage [document]="source()"> opens the document, and when the signal changes, it opens the
new one and closes the old one.
import { Component, computed, input } from '@angular/core';
import { EpdfRenderLayer } from '@embedpdf/angular/render';
import { EpdfPageTemplate, EpdfStage } from '@embedpdf/angular/stage';
@Component({
selector: 'app-contract-page',
imports: [EpdfStage, EpdfPageTemplate, EpdfRenderLayer],
template: `
<epdf-stage [document]="source()">
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
`,
})
export class ContractPage {
/** `/contracts/:id`, bound with `withComponentInputBinding()` */
readonly id = input.required<string>();
protected readonly source = computed(() => ({ kind: 'url', url: `/files/${this.id()}.pdf` }) as const);
}Where a document comes from#
| Source | What it is |
|---|---|
{ kind: 'url', url } | A file the viewer fetches. The URL can be relative to your site. |
{ kind: 'bytes', bytes } | A file you already have: an upload, a file picker, a download. |
async (signal) => source | Anything else: a function that returns one of the above. |
A function is how you add your own fetch, such as one with a login header. The viewer calls it
under the document’s loading state, and signal cancels the download if the user closes the
document first:
const source = async (signal: AbortSignal) => {
const response = await fetch(`/api/files/${id}`, { headers: { authorization }, signal });
return { kind: 'bytes', bytes: await response.arrayBuffer() };
};Loading, errors and passwords#
*epdfDocumentGate has something to show for each state a document can be in:
import { Component } from '@angular/core';
import { EpdfDocumentGate } from '@embedpdf/angular/runtime';
import { EpdfRenderLayer } from '@embedpdf/angular/render';
import { EpdfPageTemplate, EpdfStage } from '@embedpdf/angular/stage';
import { PasswordForm } from './password-form';
import { Spinner } from './spinner';
@Component({
selector: 'app-document-view',
imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, PasswordForm, Spinner],
template: `
<epdf-stage *epdfDocumentGate="let document; fallback: opening; locked: locked; error: failed">
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
<ng-template #opening><app-spinner /></ng-template>
<ng-template #locked let-document><app-password-form [document]="document" /></ng-template>
<ng-template #failed let-document>
<p>Couldn't open {{ document.name }}: {{ document.error.message }}</p>
</ng-template>
`,
})
export class DocumentView {}A password-protected PDF opens locked. Ask for the password and unlock it:
import { Component, inject, input, signal } from '@angular/core';
import { EpdfDocuments, type DocumentInfo } from '@embedpdf/angular/runtime';
import { PasswordPrompt } from './password-prompt';
@Component({
selector: 'app-password-form',
imports: [PasswordPrompt],
template: `<app-password-prompt [wrong]="wrong()" (submitted)="submit($event)" />`,
})
export class PasswordForm {
readonly document = input.required<DocumentInfo>();
private readonly documents = inject(EpdfDocuments);
protected readonly wrong = signal(false);
protected async submit(password: string) {
try {
await this.documents.unlock(this.document().id, { password });
} catch {
this.wrong.set(true); // it stays locked; ask again
}
}
}After an error, such as a network failure, documents.retry(id) tries again. Here the second
document isn’t a PDF, so it shows its error:
Loading live preview…
import {
ChangeDetectionStrategy,
Component,
inject,
input,
linkedSignal,
signal,
ViewEncapsulation,
} from '@angular/core';
import { EpdfDocumentGate, EpdfDocuments, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { DocumentInfo, FailedDocumentInfo, OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { localEngine } from '@embedpdf/engine';
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', bytes: new Uint8Array(await response.arrayBuffer()) };
};
// A file that isn't a PDF, so its document can't be opened.
const broken: OpenInput = { kind: 'bytes', bytes: new TextEncoder().encode('Not a PDF') };
@Component({
selector: 'demo-password-form',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="panel">
<h3>{{ document().name }} needs a password</h3>
@if (wrong()) {
<p>That password isn’t right. Try again.</p>
}
<div class="actions">
<input
#field
class="field"
type="password"
aria-label="Password"
[value]="password()"
(input)="password.set(field.value)"
/>
<button type="button" class="button" (click)="unlock()">Unlock</button>
</div>
</div>
`,
})
export class PasswordForm {
readonly document = input.required<DocumentInfo>();
private readonly documents = inject(EpdfDocuments);
protected readonly password = signal('');
protected readonly wrong = linkedSignal(() => this.document().passwordProvided ?? false);
protected async unlock() {
try {
await this.documents.unlock(this.document().id, { password: this.password() });
} catch {
this.wrong.set(true); // it stays locked; ask again
}
}
}
@Component({
selector: 'demo-open-error',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="panel" data-tone="error">
<h3>Couldn’t open {{ document().name }}</h3>
<p>{{ document().error.message }}</p>
<div class="actions">
<button type="button" class="button" (click)="documents.retry(document().id)">
Try again
</button>
<button type="button" class="button" (click)="documents.close(document().id)">Close</button>
</div>
</div>
`,
})
export class OpenError {
readonly document = input.required<FailedDocumentInfo>();
protected readonly documents = inject(EpdfDocuments);
}
@Component({
selector: 'demo-root',
imports: [
EpdfDocumentGate,
EpdfStage,
EpdfPageTemplate,
EpdfRenderLayer,
PasswordForm,
OpenError,
],
providers: [
provideEmbedPdf(
{
engine: () => localEngine(),
initialDocuments: [
{ source: ebook, name: 'ebook.pdf' },
{ source: broken, name: 'broken.pdf', active: true },
],
},
withStage(),
withRender(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './states.css',
encapsulation: ViewEncapsulation.None,
template: `
<div class="tabs" role="tablist">
@for (document of documents.documents(); track document.id) {
<button
type="button"
role="tab"
class="tab"
[attr.aria-selected]="document.id === documents.activeId()"
(click)="documents.setActive(document.id)"
>
<span class="status" [attr.data-status]="document.status"></span>
{{ document.name }}
</button>
}
</div>
<epdf-stage
*epdfDocumentGate="let ready; fallback: opening; locked: locked; error: failed"
class="stage"
>
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
<ng-template #opening><div class="panel">Opening…</div></ng-template>
<ng-template #locked let-document>
<demo-password-form [document]="document" />
</ng-template>
<ng-template #failed let-document>
<demo-open-error [document]="document" />
</ng-template>
`,
})
export class App {
protected readonly documents = inject(EpdfDocuments);
}To show the state yourself, for example on a tab, inject(EpdfDocument) gives you the document’s
status: 'loading', 'locked', 'ready' or 'error'. Every field is listed under
State.
Who the user is and what they may do#
Tell the viewer who the user is and what they may do, once. Every document it opens uses them:
import { Component } from '@angular/core';
import { provideEmbedPdf } from '@embedpdf/angular/runtime';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { engine } from './pdf';
@Component({
selector: 'app-review-viewer',
imports: [EpdfStage, EpdfPageTemplate, EpdfRenderLayer],
providers: [
provideEmbedPdf(
{
engine,
identity: { userId: 'u_381', displayName: 'Dana Smith' },
scope: ['doc.open', 'doc.render', 'doc.text.select', 'doc.annotate.read', 'annotations:create:self'],
},
withStage(),
withRender(),
),
],
template: `
<epdf-stage>
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
`,
})
export class ReviewViewer {}identityis who the user is. TheirdisplayNamebecomes the author of the annotations they make.scopeis what they may do, as a list of permissions. Leave it out and they may do what the file allows: everything, unless the PDF restricts itself, as in Acrobat.
Loading live preview…
import {
ChangeDetectionStrategy,
Component,
effect,
inject,
signal,
ViewEncapsulation,
} from '@angular/core';
import {
EpdfDocument,
EpdfDocumentGate,
EpdfDocuments,
EpdfViewer,
provideEmbedPdf,
} from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { localEngine } from '@embedpdf/engine';
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', bytes: new Uint8Array(await response.arrayBuffer()) };
};
const dana = { userId: 'u_381', displayName: 'Dana Smith' };
// What each role may do, as permissions.
const roles = {
reader: ['doc.open', 'doc.render', 'doc.text.select'],
editor: [
'doc.open',
'doc.render',
'doc.text.select',
'doc.text.copy',
'doc.download',
'doc.print',
],
};
type Role = keyof typeof roles;
@Component({
selector: 'demo-root',
imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer],
providers: [
provideEmbedPdf(
{
engine: () => localEngine(),
identity: dana,
scope: roles.reader,
initialDocuments: [{ source: ebook, name: 'ebook.pdf' }],
},
withStage(),
withRender(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './identity.css',
encapsulation: ViewEncapsulation.None,
template: `
<div class="toolbar">
<label class="label">
Dana Smith, as
<select #picker class="select" [value]="role()" (change)="role.set(asRole(picker.value))">
<option value="reader">reader</option>
<option value="editor">editor</option>
</select>
</label>
<!-- Read the document's id too, so the checks are read again when it opens or closes. -->
@let id = document.id();
<span class="check" [attr.data-allowed]="!!id && documents.canDownload()"> Download </span>
<span class="check" [attr.data-allowed]="!!id && documents.canPrint()">Print</span>
</div>
<p class="note">
This document opened for a {{ openedAs() }}.
@if (openedAs() !== role()) {
<button type="button" class="button" (click)="openAgain()">
Open it again as {{ role() }}
</button>
}
</p>
<epdf-stage *epdfDocumentGate="let ready; fallback: opening" class="stage">
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
<ng-template #opening><p class="loading">Opening…</p></ng-template>
`,
})
export class App {
protected readonly documents = inject(EpdfDocuments);
protected readonly document = inject(EpdfDocument);
protected readonly role = signal<Role>('reader');
// The role the open document was opened with: it keeps that one.
protected readonly openedAs = signal<Role>('reader');
constructor() {
// The viewer's scope follows the role; documents opened from now on get it.
const viewer = inject(EpdfViewer);
effect(() => viewer.updateSettings({ scope: roles[this.role()] }));
}
protected asRole(value: string): Role {
return value as Role;
}
protected async openAgain() {
const role = this.role();
await this.documents.close(this.document.id());
await this.documents.open(ebook, { name: 'ebook.pdf' });
this.openedAs.set(role);
}
}inject(EpdfViewer).updateSettings({ identity, scope }), for example when
someone else signs in. The documents you open after that use the new values; documents already
open keep theirs.
A document can have its own, which replace the viewer’s for that document:
const initialDocuments = [{ source, scope: ['doc.open', 'doc.render'] }]; // this one is read-only
await documents.open(source, { scope: ['doc.open', 'doc.render'] }); // and so is this onePermissions lists every permission.
Close a document#
await documents.close(document.id);Closing frees the document’s memory in the engine. The next document becomes the active one.
Methods#
What inject(EpdfDocuments) gives you for opening and closing documents and reading their pages. Tabs and
the active document are on Several documents, downloading and printing on
Saving & downloading.
| Method | What it does |
|---|---|
open(source, options?) | Opens a document and makes it the active one. Resolves { document } once it’s ready, or locked with a password; rejects when it can’t be opened. Open a document later |
canOpenFiles() | Whether open() takes a file’s bytes, such as a file the user picked, or a URL it downloads: true on the local engine, false on CloudPDF, where every document comes from your server |
openAll(documents) | Opens several documents, as initialDocuments does: every tab appears at once, in order, and the one marked active (or the first) becomes the active one. A failure shows in that document’s status instead of throwing. |
unlock(id, { password }) | Unlocks a locked document. Rejects on a wrong password, and the document stays locked so you can ask again. Loading, errors and passwords |
retry(id) | Tries a failed document again, with the same source and options. Resolves like open(). |
close(id), closeAll() | Closes one document, or every document, and frees its memory. Close a document |
get(id?) | A document’s fields, the ones inject(EpdfDocument) gives you, or null when it isn’t open. Without an id, the document in scope |
listPages(id?) | The document’s pages, in order. In a component, use document.pages(). |
getPage(page, id?) | One page, by its ref or its index, or null when the document doesn’t have it |
getPageIndex(ref, id?) | A page’s position, from 0, or -1 when the document doesn’t have it |
getRevision(id?) | A number that goes up each time pages are added, removed, moved or rotated; -1 with no document |
options for open() are name (what its tab shows), activate: false (open it in the
background), password, and, on the local engine, scope and identity. Without an id, the page
methods use the document in scope: the one a [epdfDocumentScope]
around your component names, or else the active one. To stop an open that’s still loading, close the
document: that cancels its download too.
State#
inject(EpdfDocument) gives you the document a component talks to: the active one, or the one its
[epdfDocumentScope] names. Each field is a signal: read it in a template or a computed(), and your view updates when it changes. Outside a component,
get(id) gives the same fields. With no document open yet, it reads as one still on its way: an
empty id and status: 'loading', so a component that shows it never has to check for null.
| Field | What it is |
|---|---|
id() | The document’s id, for close(id), unlock(id, …) and [epdfDocumentScope] |
name() | The name its tab shows, from open() or rename() |
status() | 'loading', 'locked', 'ready' or 'error' |
pageCount() | How many pages it has; 0 until it’s ready |
hasUnsavedChanges() | Whether it has changes that weren’t downloaded yet. Warn before changes are lost |
passwordProvided() | When it’s locked: whether a password was tried and was wrong |
error() | Why it couldn’t be opened, when status is 'error': { code, message } |
Every open document, with the same fields, comes from inject(EpdfDocuments).
Events#
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';
import { EpdfDocuments } from '@embedpdf/angular/runtime';
@Component({
selector: 'app-open-error',
template: `<p role="alert">{{ message() }}</p>`,
})
export class OpenError {
private readonly documents = inject(EpdfDocuments);
protected readonly message = toSignal(
this.documents.openFailed$.pipe(map(({ error }) => `Couldn't open the file: ${error.message}`)),
{ initialValue: '' },
);
}| Event | Fires when | Carries |
|---|---|---|
opened$ | A document is open and ready | documentId, info |
openFailed$ | A document couldn’t be opened | documentId, error |
locked$ | A document needs a password | documentId, passwordProvided |
closed$ | A document was closed | documentId |
pagesChanged$ | Pages were added, removed, moved or rotated | documentId, pages, revision |
Switching the active document is on Several documents.
Good to know#
Opening large files#
The local engine needs the whole file before it can show the first page, so a large PDF on a slow connection takes as long as its download.
initialDocuments is read once#
Like engine, provideEmbedPdf() reads initialDocuments once, when the viewer starts. To open
another document later, use documents.open(), or give the Stage a [document] that changes.
Several documents#
A viewer can hold several documents at once, like tabs. See Several documents.
Your feedback goes directly to the documentation team.