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 { useDocuments, useDocumentsState } from '@embedpdf/react/runtime';
export function Tabs() {
const documents = useDocuments();
const { documents: open, activeId } = useDocumentsState();
return (
<div role="tablist">
{open.map((document) => (
<div key={document.id}>
<button
role="tab"
aria-selected={document.id === activeId}
onClick={() => documents.setActive(document.id)}
>
{document.name}
</button>
<button aria-label={`Close ${document.name}`} onClick={() => documents.close(document.id)}>
×
</button>
</div>
))}
</div>
);
}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 {
Viewer,
DocumentGate,
useDocument,
useDocuments,
useDocumentsState,
} from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage, useStageState } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { localEngine } from '@embedpdf/engine';
import './tabs.css';
const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin()];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', bytes: new Uint8Array(await response.arrayBuffer()) };
};
function Tabs() {
const documents = useDocuments();
const { documents: open, activeId } = useDocumentsState();
return (
<div className="tabs" role="tablist">
{open.map((document) => (
<div key={document.id} className="tab" data-active={document.id === activeId}>
<button
type="button"
role="tab"
className="name"
aria-selected={document.id === activeId}
onClick={() => documents.setActive(document.id)}
>
{document.name}
</button>
<button
type="button"
className="close"
aria-label={`Close ${document.name}`}
onClick={() => documents.close(document.id)}
>
×
</button>
</div>
))}
<button
type="button"
className="button"
aria-label="Open another copy"
onClick={() => documents.open(ebook, { name: `Copy ${open.length + 1}` })}
>
+
</button>
</div>
);
}
// The active tab: rename it, move it to the front, and zoom it. Each tab keeps its own zoom.
function TabTools() {
const documents = useDocuments();
const { id, name } = useDocument();
const stage = useStage();
const zoomLevel = useStageState((state) => state.zoomLevel);
return (
<div className="toolbar">
<input
className="field"
aria-label="Tab name"
value={name ?? ''}
onChange={(event) => documents.rename(id, event.target.value)}
/>
<button type="button" className="button" onClick={() => documents.move(id, 0)}>
Move to front
</button>
<button
type="button"
className="button"
aria-label="Zoom out"
onClick={() => stage.zoomOut()}
>
−
</button>
<button type="button" className="button" aria-label="Zoom in" onClick={() => stage.zoomIn()}>
+
</button>
<output className="readout">{Math.round(zoomLevel * 100)}%</output>
</div>
);
}
export default function App() {
return (
<Viewer
engine={engine}
plugins={plugins}
initialDocuments={[
{ source: ebook, name: 'Contract' },
{ source: ebook, name: 'Report' },
]}
>
<Tabs />
<DocumentGate fallback={<p className="loading">Opening…</p>}>
<TabTools />
<Stage className="stage">{() => <RenderLayer />}</Stage>
</DocumentGate>
</Viewer>
);
}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 useDocumentsState(), 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 <DocumentScope>, it talks to the one you name,
whatever is active:
import { DocumentScope } from '@embedpdf/react/runtime';
import { RenderLayer } from '@embedpdf/react/render';
import { Stage } from '@embedpdf/react/stage';
import { ZoomControls } from './ZoomControls';
export function DocumentView({ documentId }: { documentId: string }) {
return (
<DocumentScope id={documentId}>
<Stage>{() => <RenderLayer />}</Stage>
<ZoomControls />
</DocumentScope>
);
}Everything inside, such as the Stage, useSearch() and useSelection(), 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 {
Viewer,
DocumentGate,
DocumentScope,
useDocument,
useDocumentsState,
} from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin, useStage, useStageState } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import { localEngine } from '@embedpdf/engine';
import './scope.css';
const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin()];
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 <DocumentScope> names.
function Header() {
const { name, pageCount } = useDocument();
const stage = useStage();
const zoomLevel = useStageState((state) => state.zoomLevel);
return (
<div className="header">
<strong>{name}</strong>
{pageCount} pages
<button
type="button"
className="button"
aria-label="Zoom out"
onClick={() => stage.zoomOut()}
>
−
</button>
<output className="readout">{Math.round(zoomLevel * 100)}%</output>
<button type="button" className="button" aria-label="Zoom in" onClick={() => stage.zoomIn()}>
+
</button>
</div>
);
}
function SideBySide() {
const { documents } = useDocumentsState();
return (
<div className="split">
{documents.map((document) => (
<section key={document.id} className="pane">
<DocumentScope id={document.id}>
<DocumentGate fallback={<p className="loading">Opening…</p>}>
<Header />
<Stage className="stage">{() => <RenderLayer />}</Stage>
</DocumentGate>
</DocumentScope>
</section>
))}
</div>
);
}
export default function App() {
return (
<Viewer
engine={engine}
plugins={plugins}
initialDocuments={[
{ source: ebook, name: 'Original' },
{ source: ebook, name: 'Revised' },
]}
>
<SideBySide />
</Viewer>
);
}Side by side#
The view manager arranges documents into panes, each with its own tabs, like split editors in a
code editor. Register viewManagerPlugin(), then render one Stage per pane:
import { DocumentScope } from '@embedpdf/react/runtime';
import { RenderLayer } from '@embedpdf/react/render';
import { Stage } from '@embedpdf/react/stage';
import { useViewManager, useViewManagerState } from '@embedpdf/react/view-manager';
import { PaneTabs } from './PaneTabs';
export function Panes() {
const views = useViewManager();
const { panes, focusedPaneId } = useViewManagerState();
return (
<div className="panes">
{panes.map((pane) => (
<section key={pane.id} data-focused={pane.id === focusedPaneId} onFocus={() => views.setFocusedPane(pane.id)}>
<PaneTabs pane={pane} />
{pane.activeDocumentId && (
<DocumentScope id={pane.activeDocumentId}>
<Stage>{() => <RenderLayer />}</Stage>
</DocumentScope>
)}
</section>
))}
</div>
);
}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 { useEffect, useRef } from 'react';
import { Viewer, DocumentGate, DocumentScope, useDocumentsState } from '@embedpdf/react/runtime';
import type { OpenInput } from '@embedpdf/react/runtime';
import { Stage, stagePlugin } from '@embedpdf/react/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/react/render';
import {
useViewManager,
useViewManagerState,
viewManagerPlugin,
type PaneInfo,
} from '@embedpdf/react/view-manager';
import { localEngine } from '@embedpdf/engine';
import './panes.css';
const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), viewManagerPlugin()];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', bytes: new Uint8Array(await response.arrayBuffer()) };
};
function Pane({ pane, canRemove }: { pane: PaneInfo; canRemove: boolean }) {
const views = useViewManager();
const focusedPaneId = useViewManagerState((state) => state.focusedPaneId);
const { documents } = useDocumentsState();
const nameOf = (id: string) => documents.find((document) => document.id === id)?.name ?? id;
return (
<section
className="pane"
data-focused={pane.id === focusedPaneId}
onPointerDown={() => views.setFocusedPane(pane.id)}
>
<div className="bar" role="tablist">
{pane.documentIds.map((id) => (
<button
key={id}
type="button"
role="tab"
className="tab"
aria-selected={id === pane.activeDocumentId}
onClick={() => views.setActiveDocument(pane.id, id)}
>
{nameOf(id)}
</button>
))}
<button
type="button"
className="button"
disabled={!pane.activeDocumentId || pane.documentIds.length < 2}
onClick={() => views.splitPane(pane.activeDocumentId!, { from: pane.id })}
>
Split
</button>
<button
type="button"
className="button"
disabled={!canRemove}
onClick={() => views.removePane(pane.id)}
>
Close pane
</button>
</div>
{pane.activeDocumentId ? (
<DocumentScope id={pane.activeDocumentId}>
<DocumentGate fallback={<p className="empty">Opening…</p>}>
<Stage className="stage">{() => <RenderLayer />}</Stage>
</DocumentGate>
</DocumentScope>
) : (
<p className="empty">No document in this pane.</p>
)}
</section>
);
}
function Panes() {
const views = useViewManager();
const { panes } = useViewManagerState();
// On load, both documents land in the first pane: put the second one beside it, once.
const splitOnLoad = useRef(true);
useEffect(() => {
if (splitOnLoad.current && panes.length === 1 && panes[0].documentIds.length === 2) {
splitOnLoad.current = false;
views.splitPane(panes[0].documentIds[1]);
}
}, [panes, views]);
return (
<div className="panes">
{panes.map((pane) => (
<Pane key={pane.id} pane={pane} canRemove={panes.length > 1} />
))}
</div>
);
}
export default function App() {
return (
<Viewer
engine={engine}
plugins={plugins}
initialDocuments={[
{ source: ebook, name: 'Contract' },
{ source: ebook, name: 'Report' },
]}
>
<Panes />
</Viewer>
);
}Methods#
What useDocuments() 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 <DocumentScope>. 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 useViewManager() 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#
Everything useDocumentsState() gives you. Your component re-renders when a value 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 useDocument() 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.
Everything useViewManagerState() gives you. Your component re-renders when a value 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 { useRouter } from 'next/navigation';
import { useDocumentsEvent } from '@embedpdf/react/runtime';
export function DocumentRoute() {
const router = useRouter();
useDocumentsEvent(
(documents) => documents.onActiveChanged,
({ documentId }) => router.replace(`/documents/${documentId}`),
);
return null;
}| Event | Fires when | Carries |
|---|---|---|
onActiveChanged | Another document became the active one | documentId, previousDocumentId |
View manager onPaneCreated | A pane was added | paneId |
View manager onPaneRemoved | A pane was closed | paneId |
View manager onFocusChanged | Another pane got the focus | paneId, or null when none has it |
View manager onDocumentMoved | A document moved between panes | documentId, fromPaneId, toPaneId |
The view manager’s events come from useViewManagerEvent(). 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
<DocumentScope>. For two views of one document, see
Two views of one document.
Your feedback goes directly to the documentation team.