Several documents
A viewer can hold several documents at once, like tabs in a browser. One of them is the active document, and every component talks to it unless you say otherwise:
import { Component, inject } from '@angular/core';
import { EpdfDocuments } from '@embedpdf/angular/runtime';
@Component({
selector: 'app-tabs',
template: `
<div role="tablist">
@for (document of documents.documents(); track document.id) {
<div>
<button
role="tab"
[attr.aria-selected]="document.id === documents.activeId()"
(click)="documents.setActive(document.id)"
>
{{ document.name }}
</button>
<button [attr.aria-label]="'Close ' + document.name" (click)="documents.close(document.id)">
×
</button>
</div>
}
</div>
`,
})
export class Tabs {
protected readonly documents = inject(EpdfDocuments);
}Each document keeps its own state: its zoom and scroll position, its selection, its search and its active tool. Switch tabs and back, and everything is where you left it.
Loading live preview…
import {
ChangeDetectionStrategy,
Component,
inject,
input,
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-tabs',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="tabs" role="tablist">
@for (document of documents.documents(); track document.id) {
<div class="tab" [attr.data-active]="document.id === documents.activeId()">
<button
type="button"
role="tab"
class="name"
[attr.aria-selected]="document.id === documents.activeId()"
(click)="documents.setActive(document.id)"
>
{{ document.name }}
</button>
<button
type="button"
class="close"
[attr.aria-label]="'Close ' + document.name"
(click)="documents.close(document.id)"
>
×
</button>
</div>
}
<button type="button" class="button" aria-label="Open another copy" (click)="openCopy()">
+
</button>
</div>
`,
})
export class Tabs {
protected readonly documents = inject(EpdfDocuments);
protected openCopy() {
void this.documents.open(ebook, { name: `Copy ${this.documents.documents().length + 1}` });
}
}
// The active tab: rename it, move it to the front, and zoom it. Each tab keeps its own zoom.
@Component({
selector: 'demo-tab-tools',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="toolbar">
<input
#field
class="field"
aria-label="Tab name"
[value]="document.name() ?? ''"
(input)="documents.rename(document.id(), field.value)"
/>
<button type="button" class="button" (click)="documents.move(document.id(), 0)">
Move to front
</button>
<button type="button" class="button" aria-label="Zoom out" (click)="stage().zoomOut()">
−
</button>
<button type="button" class="button" aria-label="Zoom in" (click)="stage().zoomIn()">
+
</button>
<output class="readout">{{ percent(stage().zoomLevel()) }}%</output>
</div>
`,
})
export class TabTools {
readonly stage = input.required<EpdfStage>();
protected readonly documents = inject(EpdfDocuments);
protected readonly document = inject(EpdfDocument);
protected percent(zoomLevel: number): number {
return Math.round(zoomLevel * 100);
}
}
@Component({
selector: 'demo-root',
imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, Tabs, TabTools],
providers: [
provideEmbedPdf(
{
engine: () => localEngine(),
initialDocuments: [
{ source: ebook, name: 'Contract' },
{ source: ebook, name: 'Report' },
],
},
withStage(),
withRender(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './tabs.css',
encapsulation: ViewEncapsulation.None,
template: `
<demo-tabs />
<ng-container *epdfDocumentGate="let document; fallback: opening">
<demo-tab-tools [stage]="stage" />
<epdf-stage #stage="epdfStage" class="stage">
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
</ng-container>
<ng-template #opening><p class="loading">Opening…</p></ng-template>
`,
})
export class App {}Manage the tabs#
documents.setActive(id);
documents.rename(id, 'Signed contract');
documents.move(id, 0); // to the front
await documents.close(id);Each entry in documents, from inject(EpdfDocuments), has the document’s id, its name, and its
status: 'loading', 'locked', 'ready' or 'error', so a tab can show a spinner or a lock.
Every field is listed under State.
Talk to a specific document#
A component talks to the active document. Inside [epdfDocumentScope], it talks to the one you name,
whatever is active:
import { Component, input } from '@angular/core';
import { EpdfDocumentScope } from '@embedpdf/angular/runtime';
import { EpdfRenderLayer } from '@embedpdf/angular/render';
import { EpdfPageTemplate, EpdfStage } from '@embedpdf/angular/stage';
import { ZoomControls } from './zoom-controls';
@Component({
selector: 'app-document-view',
imports: [EpdfDocumentScope, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, ZoomControls],
template: `
<div [epdfDocumentScope]="documentId()">
<epdf-stage>
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
<app-zoom-controls />
</div>
`,
})
export class DocumentView {
readonly documentId = input.required<string>();
}Everything inside, such as the Stage, inject(EpdfSearch) and inject(EpdfSelection), uses that document.
That’s how you show two documents at once, or a preview of one while another is active.
Loading live preview…
import {
ChangeDetectionStrategy,
Component,
inject,
input,
ViewEncapsulation,
} from '@angular/core';
import {
EpdfDocument,
EpdfDocumentGate,
EpdfDocuments,
EpdfDocumentScope,
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()) };
};
// The same component in both panes: each talks to the document its [epdfDocumentScope] names.
@Component({
selector: 'demo-header',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="header">
<strong>{{ document.name() }}</strong>
{{ document.pageCount() }} pages
<button type="button" class="button" aria-label="Zoom out" (click)="stage().zoomOut()">
−
</button>
<output class="readout">{{ percent(stage().zoomLevel()) }}%</output>
<button type="button" class="button" aria-label="Zoom in" (click)="stage().zoomIn()">
+
</button>
</div>
`,
})
export class Header {
readonly stage = input.required<EpdfStage>();
protected readonly document = inject(EpdfDocument);
protected percent(zoomLevel: number): number {
return Math.round(zoomLevel * 100);
}
}
@Component({
selector: 'demo-root',
imports: [
EpdfDocumentScope,
EpdfDocumentGate,
EpdfStage,
EpdfPageTemplate,
EpdfRenderLayer,
Header,
],
providers: [
provideEmbedPdf(
{
engine: () => localEngine(),
initialDocuments: [
{ source: ebook, name: 'Original' },
{ source: ebook, name: 'Revised' },
],
},
withStage(),
withRender(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './scope.css',
encapsulation: ViewEncapsulation.None,
template: `
<div class="split">
@for (document of documents.documents(); track document.id) {
<section class="pane" [epdfDocumentScope]="document.id">
<ng-container *epdfDocumentGate="let ready; fallback: opening">
<demo-header [stage]="stage" />
<epdf-stage #stage="epdfStage" class="stage">
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
</ng-container>
</section>
}
</div>
<ng-template #opening><p class="loading">Opening…</p></ng-template>
`,
})
export class App {
protected readonly documents = inject(EpdfDocuments);
}Side by side#
The view manager arranges documents into panes, each with its own tabs, like split editors in a
code editor. Register withViewManager(), then render one Stage per pane:
import { Component, inject } from '@angular/core';
import { EpdfDocumentScope } from '@embedpdf/angular/runtime';
import { EpdfRenderLayer } from '@embedpdf/angular/render';
import { EpdfPageTemplate, EpdfStage } from '@embedpdf/angular/stage';
import { EpdfViewManager } from '@embedpdf/angular/view-manager';
import { PaneTabs } from './pane-tabs';
@Component({
selector: 'app-panes',
imports: [EpdfDocumentScope, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, PaneTabs],
template: `
<div class="panes">
@for (pane of views.panes(); track pane.id) {
<section
[attr.data-focused]="pane.id === views.focusedPaneId()"
(focusin)="views.setFocusedPane(pane.id)"
>
<app-pane-tabs [pane]="pane" />
@if (pane.activeDocumentId; as documentId) {
<div [epdfDocumentScope]="documentId">
<epdf-stage>
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
</div>
}
</section>
}
</div>
`,
})
export class Panes {
protected readonly views = inject(EpdfViewManager);
}views.splitPane(documentId); // the document in a new pane, beside the focused one
views.setActiveDocument(paneId, documentId); // show another of the pane's tabs
views.moveDocumentBetween(fromPaneId, toPaneId, documentId); // drag a tab to another pane
views.removePane(paneId); // its documents move to another paneA document opened with documents.open() lands in the focused pane. Every call is listed under
Methods.
Loading live preview…
import {
ChangeDetectionStrategy,
Component,
effect,
inject,
untracked,
ViewEncapsulation,
} from '@angular/core';
import {
EpdfDocumentGate,
EpdfDocuments,
EpdfDocumentScope,
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 { EpdfViewManager, withViewManager, type PaneInfo } from '@embedpdf/angular/view-manager';
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: [EpdfDocumentScope, EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer],
providers: [
provideEmbedPdf(
{
engine: () => localEngine(),
initialDocuments: [
{ source: ebook, name: 'Contract' },
{ source: ebook, name: 'Report' },
],
},
withStage(),
withRender(),
withViewManager(),
),
],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './panes.css',
encapsulation: ViewEncapsulation.None,
template: `
<div class="panes">
@for (pane of views.panes(); track pane.id) {
<section
class="pane"
[attr.data-focused]="pane.id === views.focusedPaneId()"
(pointerdown)="views.setFocusedPane(pane.id)"
>
<div class="bar" role="tablist">
@for (id of pane.documentIds; track id) {
<button
type="button"
role="tab"
class="tab"
[attr.aria-selected]="id === pane.activeDocumentId"
(click)="views.setActiveDocument(pane.id, id)"
>
{{ nameOf(id) }}
</button>
}
<button
type="button"
class="button"
[disabled]="!pane.activeDocumentId || pane.documentIds.length < 2"
(click)="split(pane)"
>
Split
</button>
<button
type="button"
class="button"
[disabled]="views.panes().length < 2"
(click)="views.removePane(pane.id)"
>
Close pane
</button>
</div>
@if (pane.activeDocumentId; as documentId) {
<ng-container [epdfDocumentScope]="documentId">
<epdf-stage *epdfDocumentGate="let ready; fallback: opening" class="stage">
<ng-template epdfPage>
<epdf-render-layer />
</ng-template>
</epdf-stage>
</ng-container>
} @else {
<p class="empty">No document in this pane.</p>
}
</section>
}
</div>
<ng-template #opening><p class="empty">Opening…</p></ng-template>
`,
})
export class App {
protected readonly views = inject(EpdfViewManager);
private readonly documents = inject(EpdfDocuments);
constructor() {
// On load, both documents land in the first pane: put the second one beside it, once.
const splitOnLoad = effect(() => {
const panes = this.views.panes();
if (panes.length !== 1 || panes[0].documentIds.length !== 2) return;
splitOnLoad.destroy();
untracked(() => this.views.splitPane(panes[0].documentIds[1]));
});
}
protected nameOf(id: string): string {
return this.documents.documents().find((document) => document.id === id)?.name ?? id;
}
protected split(pane: PaneInfo) {
if (pane.activeDocumentId) this.views.splitPane(pane.activeDocumentId, { from: pane.id });
}
}Methods#
What inject(EpdfDocuments) gives you for several documents. Opening and closing are on
Opening documents, downloading and printing on
Saving & downloading.
| Method | What it does |
|---|---|
setActive(id) | Makes a document the active one: the one every component talks to outside [epdfDocumentScope]. Manage the tabs |
rename(id, name) | Changes the name its tab shows. The file doesn’t change. |
move(id, index) | Moves a document to another place in the tab order |
swap(id, otherId) | Swaps two documents in the tab order |
setOrder(ids) | Puts every document in a new order at once. ids holds the id of every open document. |
Everything inject(EpdfViewManager) gives you.
| Method | What it does |
|---|---|
splitPane(documentId, { from? }) | Moves an open document into a new pane beside the focused one, or beside from, and focuses it. Returns the new pane’s id. Side by side |
createPane({ documentIds? }) | Adds a pane, empty or with these documents moved into it, and focuses it. Returns its id. |
removePane(paneId, { moveDocumentsTo? }) | Closes a pane. Its documents move to moveDocumentsTo, or else to the focused or the last pane. |
movePane(paneId, index) | Moves a pane to another place in the order, for reordering panes by dragging |
setFocusedPane(paneId) | Makes a pane the focused one, where new documents open. null focuses none. |
setActiveDocument(paneId, documentId) | Shows another of the pane’s documents |
addDocument(paneId, documentId, index?) | Adds a document to a pane’s tabs. A document is in one pane at a time, so it leaves the pane it was in. |
removeDocument(paneId, documentId) | Takes a document out of a pane, without closing it |
moveDocumentWithin(paneId, documentId, index) | Moves a tab to another place inside its pane |
moveDocumentBetween(fromPaneId, toPaneId, documentId, index?) | Moves a tab from one pane to another, for dragging tabs between panes |
State#
Every value is a signal on inject(EpdfDocuments): read it in a template or a computed(), and your view updates when it changes. The method in the last column reads the same value once.
| Field | What it is | Read once |
|---|---|---|
documents() | Every open document, in tab order, including ones that are loading, locked or failed. Each has the fields inject(EpdfDocument) gives you. | list(), has(id), getCount(), getOrder() |
activeId() | The active document’s id, or null with no document open | getActiveId(), or getActive() for its fields |
has(id) says whether a document is open, getCount() how many are, and getOrder() gives just their
ids. One document’s fields come from get(id), on Opening documents.
Every value is a signal on inject(EpdfViewManager): read it in a template or a computed(), and your view updates when it changes. The method in the last column reads the same value once.
| Field | What it is | Read once |
|---|---|---|
panes() | Every pane, in order. Each has its id, its documentIds (its tabs, in order) and the activeDocumentId it shows. | listPanes(), getPane(paneId), getPaneOrder(), getPaneOfDocument(documentId) |
focusedPaneId() | The pane new documents open in, or null | getFocusedPaneId() |
getPane(paneId) reads one pane, getPaneOrder() gives just their ids, and
getPaneOfDocument(documentId) says which pane holds a document.
Events#
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Router } from '@angular/router';
import { EpdfDocuments } from '@embedpdf/angular/runtime';
@Component({ selector: 'app-document-route', template: '' })
export class DocumentRoute {
private readonly router = inject(Router);
constructor() {
inject(EpdfDocuments)
.activeChanged$.pipe(takeUntilDestroyed())
.subscribe(({ documentId }) => this.router.navigate(['/documents', documentId], { replaceUrl: true }));
}
}| Event | Fires when | Carries |
|---|---|---|
activeChanged$ | Another document became the active one | documentId, previousDocumentId |
View manager paneCreated$ | A pane was added | paneId |
View manager paneRemoved$ | A pane was closed | paneId |
View manager focusChanged$ | Another pane got the focus | paneId, or null when none has it |
View manager documentMoved$ | A document moved between panes | documentId, fromPaneId, toPaneId |
The view manager’s events are streams on inject(EpdfViewManager). Opening and closing documents are on
Opening documents.
Good to know#
Memory#
Each open document holds its file in the engine’s memory. Close documents the user is done with, especially large ones.
One Stage per view#
A Stage shows one document. For two documents at once, render two Stages, each in its own
[epdfDocumentScope]. For two views of one document, see
Two views of one document.
Your feedback goes directly to the documentation team.