Skip to content

Repository files navigation

office-viewer

npm version

Headless Web Component orchestration layer for browser-based Office Open XML viewing built on @silurus/ooxml.

Status: not production ready yet. This is an early 0.x release. The API, lifecycle behavior, and upstream dependency can still change between minor versions. Try it and report issues, but do not rely on it in production.

Live demo: office-viewer-pi.vercel.app

<office-viewer> is an orchestration shell, not a viewer widget. It loads DOCX, XLSX, and PPTX documents, manages the upstream viewer lifecycle, and exposes the upstream viewer instance directly so consumers can build their own UI.

  • Open Shadow DOM protects the viewer surface from global styles.
  • No UI chrome, toolbars, or status widgets.
  • Direct access to the upstream getViewer().

Installation

npm install @missing-elements/office-viewer

Usage

<script type="module">
  import { defineOfficeViewerElement } from '@missing-elements/office-viewer';
  defineOfficeViewerElement();

  const element = document.querySelector('office-viewer');
  await element.load('/report.docx', { format: 'docx' });

  const viewer = element.getViewer();
  viewer.setScale(1.5);
</script>

<office-viewer style="height: 100svh"></office-viewer>

The same load can be declared with attributes:

<office-viewer src="/report.docx" file-type="docx" style="height: 100svh"></office-viewer>

Where the upstream assets come from

This package is a few kilobytes and leaves @silurus/ooxml external. Whatever serves that dependency also serves its parser WASM and its render workers, and that decides which render mode is available:

Setup worker mode main mode
Bundler (Vite, webpack, …) yes yes
Self-hosted static files with an import map yes yes
CDN such as esm.sh (CodePen, JSFiddle, …) no yes

Bundlers

Install the package and import it. The bundler resolves @silurus/ooxml from node_modules and emits its WASM and worker files alongside your assets; nothing else is needed. The demo/ folder is a Vite example of this.

Self-hosting without a bundler or CDN

Copy node_modules/@silurus/ooxml/dist/ (about 15 MB) and dist/office-viewer.es.js to your server, serve .wasm files as application/wasm, and map the bare specifiers with an import map:

<script type="importmap">
{
  "imports": {
    "@silurus/ooxml/docx": "/vendor/ooxml/docx.mjs",
    "@silurus/ooxml/xlsx": "/vendor/ooxml/xlsx.mjs",
    "@silurus/ooxml/pptx": "/vendor/ooxml/pptx.mjs"
  }
}
</script>
<script type="module">
  import { defineOfficeViewerElement } from '/vendor/office-viewer.es.js';
  defineOfficeViewerElement();
</script>

Everything is on your origin, so both modes work and no third party is involved.

CodePen and other CDN-loaded pages

Import from a CDN that resolves dependencies, such as esm.sh, and load with mode: 'main':

import { defineOfficeViewerElement } from 'https://esm.sh/@missing-elements/office-viewer';

defineOfficeViewerElement();
await element.load('/report.docx', { format: 'docx', mode: 'main' });

Pin a version in real pages. Worker mode cannot work here: the render worker would have to start from the CDN's origin, which browsers refuse, so the load fails with "Worker error".

wasmUrl

If your setup does not serve the parser WASM next to the upstream module, for example an asset pipeline that moves or renames it, pass wasmUrl (or the wasm-url attribute) with the asset for the document format, pinned to the installed @silurus/ooxml version:

await element.load(file, {
  format: 'docx',
  wasmUrl: 'https://cdn.jsdelivr.net/npm/@silurus/ooxml@0.86.1/dist/docx_parser_bg.wasm'
});

Replace docx in both places with xlsx or pptx for those formats.

Public API

Attributes

Attribute Description
src URL to load. Requires file-type. Clearing it unloads a document that was loaded from attributes.
file-type Format of src: docx, xlsx, or pptx. Required for attribute-driven loading; a missing or unknown value emits loaderror.
mode Rendering mode: worker (default) or main. Unknown values fall back to the default.
wasm-url Optional absolute URL of the parser WASM asset. See wasmUrl above.

Attributes set together in one synchronous run start a single load. A load() call made after attribute changes in the same run outranks them. Moving the element in the DOM without changing attributes does not reload it.

Methods

Method Description
load(source, options) Load a document. source is a URL string, ArrayBuffer, Blob/File, or ReadableStream<Uint8Array>. options.format is required, options.mode defaults to worker, and options.wasmUrl may point to a served parser WASM asset. Resolves after ready and rejects after loaderror. A newer load(), destroy(), or removal from the document cancels an in-flight load at once: its promise rejects with a DOMException named AbortError and no loaderror is emitted.
reload() Repeat the most recent load() request, including one that failed. URL strings are fetched again; Blob/File sources are read again; ArrayBuffer and stream sources are replayed from a copy kept by the element. Rejects if nothing has been requested since the last destroy().
destroy() Cancel an in-flight load, tear down the upstream viewer, forget the retained request, reset all state, and emit destroy. Safe to call at any time.
getViewer() Returns the upstream viewer instance (DocxScrollViewer, XlsxViewer, or PptxScrollViewer), or null while nothing is loaded.

Properties

Property Description
ready true while a document is loaded. Stays true for the current document while a newer load is in flight.
error Error from the most recent load, or null. Cleared when a load starts and on destroy().
format Format of the loaded document, or null.
mode Rendering mode the loaded document's viewer was created with (worker when none was given), or null.

format and mode describe the document returned by getViewer(). They change when ready fires, not when a load starts. Upstream may fall back to main-thread rendering internally for some documents; mode reports what was requested, not that fallback.

Events

Event Detail
loadstart
ready
loaderror { error } (typed as OfficeViewerLoadErrorDetail)
destroy

Every load() emits loadstart and then exactly one of ready or loaderror, unless it is cancelled, in which case neither follows. destroy is emitted by destroy() and when a removed element releases its viewer.

Lifecycle

  • Loads are last-writer-wins. Calling load() while another load is running cancels the older one immediately: its stream read is cancelled, its upstream viewer is destroyed, and its promise rejects with AbortError without waiting for the parser. Element state and events only ever reflect the newest request.
  • Removing an element that holds a viewer or an in-flight load releases them and emits destroy. The request stays retained, so adding the element back loads the same document again, whether it came from load() or from attributes. Attributes changed while detached take precedence. Call destroy() to discard the request instead. The release is deferred by one microtask, so moving the element synchronously (remove, then append in the same task) keeps the viewer alive. An idle element is not affected by removal.

Reacting to loads

Use the element's events for client-side load state. The ready property is true only after a successful load, and error contains the latest failure.

<button id="open" type="button">Open report</button>
<span id="state" role="status"></span>

<office-viewer style="height: 80svh;"></office-viewer>

<script type="module">
  import { defineOfficeViewerElement } from '@missing-elements/office-viewer';

  defineOfficeViewerElement();

  const viewer = document.querySelector('office-viewer');
  const openButton = document.querySelector('#open');
  const state = document.querySelector('#state');

  viewer.addEventListener('loadstart', () => {
    openButton.disabled = true;
    state.textContent = 'Loading report...';
  });

  viewer.addEventListener('ready', () => {
    openButton.disabled = false;
    state.textContent = 'Report ready.';
  });

  viewer.addEventListener('loaderror', (event) => {
    openButton.disabled = false;
    state.textContent = `Could not open report: ${event.detail.error.message}`;
  });

  openButton.addEventListener('click', async () => {
    try {
      await viewer.load('/report.docx', { format: 'docx' });
    } catch (error) {
      // loaderror has already updated the UI; AbortError means a newer load took over.
      if (error.name !== 'AbortError') console.error(error);
    }
  });
</script>

Source types

load() accepts a URL string, ArrayBuffer, Blob (including File), or ReadableStream<Uint8Array>. Convert Uint8Array to ArrayBuffer before calling load(). The parser may take ownership of an ArrayBuffer it is given, leaving the buffer detached (empty) after the load, so the element keeps a copy for reload(). Streams are read once into a Blob that reload() replays.

// Load from a File object (e.g., from an <input type="file">)
const file = document.querySelector('input[type="file"]').files[0];
await element.load(file, { format: 'docx' });

// Or load directly from a URL string
await element.load('/report.docx', { format: 'docx' });

Detecting format

Use file-type to detect a local file before loading it:

import { fileTypeFromBuffer } from 'file-type';

const arrayBuffer = await file.arrayBuffer();
const detected = await fileTypeFromBuffer(arrayBuffer);

if (!detected || !['docx', 'xlsx', 'pptx'].includes(detected.ext)) {
  throw new Error('Select a DOCX, XLSX, or PPTX file.');
}

await element.load(arrayBuffer, { format: detected.ext });

Browser support

  • Modern evergreen browsers with custom element support.
  • Web Workers are used by default; use mode="main" where workers are blocked or when @silurus/ooxml is loaded from another origin such as a CDN.

License

MIT

Third-Party Notices

https://github.com/yukiyokotani/office-open-xml-viewer#third-party-notices

About

A standalone, read-only browser-based Office Open XML viewer Web Component for DOCX, XLSX, and PPTX

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages