EmbedPDF

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:

tabs.ts
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:

document-scope.ts
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:

panes.ts
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 pane

A 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.

MethodWhat 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.

MethodWhat 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.

FieldWhat it isRead 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 opengetActiveId(), 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.

FieldWhat it isRead 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 nullgetFocusedPaneId()

getPane(paneId) reads one pane, getPaneOrder() gives just their ids, and getPaneOfDocument(documentId) says which pane holds a document.

Events#

active-route.ts
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 }));
  }
}
EventFires whenCarries
activeChanged$Another document became the active onedocumentId, previousDocumentId
View manager paneCreated$A pane was addedpaneId
View manager paneRemoved$A pane was closedpaneId
View manager focusChanged$Another pane got the focuspaneId, or null when none has it
View manager documentMoved$A document moved between panesdocumentId, 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.

Was this page helpful?

Your feedback goes directly to the documentation team.