> For the complete documentation index, see [llms.txt](https://docs.apryse.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.apryse.com/web/get-started/samples/showcase-demo-office-template-fill.md).

# Office Template Fill Showcase Demo Code Sample

Securely merge JSON data with MS Word, PowerPoint and Excel templates directly in the browser - no server required. You can fill PDF templates using text or images.

{% hint style="info" %}
**Requirements**

*These packages are required to use these features in production. Trial keys have unlimited access to all features*

<a href="/web/get-started/readme.md" class="button primary">Web SDK</a><a href="https://apryse.com/capabilities#Template" class="button primary">Package: Template</a><a href="https://showcase.apryse.com/office-template-fill" class="button primary">Live demo</a>
{% endhint %}

Securely merge JSON data with MS Word, PowerPoint, and Excel templates directly in the browser - no server required. You can fill PDF templates using text or images.

This demo allows you to:

* Load Office documents, such as Word, PowerPoint, and Excel
* Recognize Office documents containing clauses denoted by double curly braces \`{{FIELD\_NAME}}\`
* Download the updated document as a PDF with your updated clauses

**Implementation steps** To add Office Template Fill capability to WebViewer:

Step 1: Choose your [preferred web stack for WebViewer](/web/get-started/readme.md) Step 2: Add the ES6 JavaScript sample code provided in this guide

Once you generate your license key, it will automatically be included in your sample code below.

{% @apryse-license-key/apryse-license-key platform="WEB\_VIEWER" variant="compact" %}

<pre class="language-js" data-line-numbers><code class="lang-js">// ES6 Compliant Syntax
// Copilot name: GitHub Copilot, version: N/A, model: GPT-4, version: N/A, date: August 13, 2025
// File: index.js

import WebViewer from '@pdftron/webviewer';

const licenseKey = '<code class="expression">visitor.claims.wvKey || "YOUR_WEBVIEWER_LICENSE_KEY"</code>';

const initialDoc = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/letter_template.docx';

// The list of registered panels in the main webviewer
let viewerPanels = null;

// The tab panel, representing the webviewer left panel
const tabPanel = {
  handle: null,
  dataElement: 'tabPanel'
};

// The custom template sub-panel to be registered
const templatePanel = {
  handle: null,
  dataElement: 'templatePanel',
  templateKeys: null,
  render: null,
};

// The logos to choose from
const logos = [
  { title: 'Angular', file: '/assets/angular.png' },
  { title: 'React', file: '/assets/react.png' },
  { title: 'Vue', file: '/assets/vue.png' },
];

// The rows to insert into the table
let insertRows = [
  ['Title', 'Web Developer'],
  ['Start Date', '01/10/2022'],
  ['Expected Salary', '$100,000'],
];

// The logo to insert into the document
const logoJSON = {
  val: { image_url: logos[0].file, width: 64, height: 64 },
  type: 'image',
};

// The table to insert into the document
const tableJSON = {
  val: { insert_rows: insertRows },
  type: 'table',
};

// A sample JSON data
const sampleJSON = {
  dest_street_address: { val: '187 Duizelstraat', type: 'text' },
  applicant_first_name: { val: 'Janice', type: 'text' },
  applicant_surname: { val: 'Symonds', type: 'text' },
  Date: { val: '01/06/2022', type: 'text' },
  sender_name: { val: 'Arnold Smith', type: 'text' },
  company_name: { val: 'React-Faux', type: 'text' },
  logo: logoJSON,
  table: tableJSON,
};

// The initial selected logo. By default it is Angular
let selectedButton = logos[0].title;

WebViewer(
  {
    path: '/lib',
    initialDoc: initialDoc,
    enableFilePicker: true, // Enable file picker to open files. In WebViewer -> menu icon -> Open File
    enableMeasurement: true,
    licenseKey: licenseKey, // Replace with your license key
  },
  document.getElementById('viewer')
).then((instance) => {
  const { documentViewer } = instance.Core;

  // Customize the main webviewer left panel after the load completion.
  // The left panel will display the template sub-panel with input fields.
  // The input fields are representing the template keys were extracted from the loaded document.
  documentViewer.addEventListener('documentLoaded', async () => {
    const doc = documentViewer.getDocument();
    templatePanel.templateKeys = await doc.getTemplateKeys();
    customizeUI(instance);
  });

  console.log('✅ WebViewer loaded successfully.');
}).catch((error) => {
  console.error('❌ Failed to initialize WebViewer:', error);
});

// Customize the main webviewer left panel after the load completion
const customizeUI = (instance) => {
  const { UI } = instance;

  // Close the tab panel (if it's open) for refreshment.
  UI.closeElements([tabPanel.dataElement]);

  // Get the list of registered panels in the main webviewer
  viewerPanels = UI.getPanels();

  // Find the Tab Panel to modify. The template sub-panel will be added to this Tab panel.
  tabPanel.handle = viewerPanels.find((panel) => panel.dataElement === tabPanel.dataElement);

  // Register the custom template sub-panel
  RegisterTemplatePanel(instance);

  // Add the new custom template sub-panel to list of sub-panels under the Tab Panel
  templatePanel.handle = { render: templatePanel.dataElement };
  tabPanel.handle.panelsList = [templatePanel.handle, ...tabPanel.handle.panelsList];

  UI.openElements([tabPanel.dataElement]);
  UI.setPanelWidth(tabPanel.dataElement, 400);
};

// Register the custom template sub-panel
const RegisterTemplatePanel = (instance) => {
  templatePanel.render = createTemplatePanelElements(instance);
  instance.UI.addPanel({
    dataElement: templatePanel.dataElement,
    location: 'left',
    icon: '&#x3C;svg width="18px" height="18px" viewBox="0 0 24 24" id="圖層_1" data-name="圖層 1" xmlns="http://www.w3.org/2000/svg">&#x3C;defs>&#x3C;style>.cls-1{fill:#080808;}&#x3C;/style>&#x3C;/defs>&#x3C;title>template&#x3C;/title>&#x3C;path class="cls-1" d="M21,.5H3a2,2,0,0,0-2,2V22a2,2,0,0,0,2,2H21a2,2,0,0,0,2-2V2.5A2,2,0,0,0,21,.5Zm0,2v2H3v-2ZM3,22V6.5H21V22Z"/>&#x3C;path class="cls-1" d="M12.5,4H20a.5.5,0,0,0,0-1H12.5a.5.5,0,0,0,0,1Z"/>&#x3C;path class="cls-1" d="M4.5,4a.43.43,0,0,0,.19,0,.35.35,0,0,0,.16-.11A.47.47,0,0,0,5,3.5a.43.43,0,0,0,0-.19.36.36,0,0,0-.11-.16.5.5,0,0,0-.7,0A.35.35,0,0,0,4,3.31.43.43,0,0,0,4,3.5a.51.51,0,0,0,.5.5Z"/>&#x3C;path class="cls-1" d="M5.65,3.85A.36.36,0,0,0,5.81,4,.44.44,0,0,0,6,4a.47.47,0,0,0,.35-.15.36.36,0,0,0,.11-.16.6.6,0,0,0,0-.19.51.51,0,0,0-.15-.35A.49.49,0,0,0,5.81,3a.36.36,0,0,0-.16.11.47.47,0,0,0-.15.35.4.4,0,0,0,0,.19A.35.35,0,0,0,5.65,3.85Z"/>&#x3C;path class="cls-1" d="M8,8H4.5a1,1,0,0,0,0,2H8A1,1,0,0,0,8,8Z"/>&#x3C;path class="cls-1" d="M8,11.67H4.5a1,1,0,0,0,0,2H8a1,1,0,0,0,0-2Z"/>&#x3C;path class="cls-1" d="M8,15.33H4.5a1,1,0,0,0,0,2H8a1,1,0,0,0,0-2Z"/>&#x3C;path class="cls-1" d="M8,19H4.5a1,1,0,0,0,0,2H8a1,1,0,0,0,0-2Z"/>&#x3C;path class="cls-1" d="M14,8H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/>&#x3C;path class="cls-1" d="M14,11.67H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/>&#x3C;path class="cls-1" d="M14,15.33H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/>&#x3C;path class="cls-1" d="M14,19H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/>&#x3C;path class="cls-1" d="M19.5,8h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/>&#x3C;path class="cls-1" d="M19.5,11.67h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/>&#x3C;path class="cls-1" d="M19.5,15.33h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/>&#x3C;path class="cls-1" d="M19.5,19h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/>&#x3C;/svg>',
    title: 'Template',
    render: () => templatePanel.render,
  });
};

// Create the template panel elements.
// The elements will be created dynamically, 
// based on the template keys extracted from
// the loaded document.
const createTemplatePanelElements = (instance) => {
  let panelDiv = document.createElement('div');
  panelDiv.id = 'template';
  let paragraph = document.createTextNode('Securely merge JSON data with MS Word, PowerPoint and Excel templates directly in the browser - no server required! Easily generate documents using text/image templates with JavaScript.');
  panelDiv.appendChild(paragraph);

  let dividerDiv = document.createElement('div');
  dividerDiv.style.borderTop = '1px solid #ccc';
  dividerDiv.style.margin = '10px 0';
  panelDiv.appendChild(dividerDiv);

  if (templatePanel.templateKeys.length === 0) {
    panelDiv.appendChild(document.createTextNode('Sorry, there are no template keys in this document.'));
    return panelDiv;
  }

  let labelField = null;
  let inputField = null;
  let logoField = null;
  let tableField = null;

  templatePanel.templateKeys.forEach(element => {
    switch (element) {
      // if logo key is found, then create its div.
      case 'logo':
        logoField = document.createElement('div');
        logoField.appendChild(document.createTextNode(`${element}:`));
        logoField.appendChild(document.createElement('p'));
        logoField.id = 'logo';
        if (sampleJSON[element].type === 'image') {
          logos.forEach(image => {
            const img = document.createElement('img');
            img.src = image.file;
            img.alt = image.title;
            img.style.width = '20px';
            img.style.height = '20px';

            let frameworkBtn = document.createElement('button');
            frameworkBtn.title = image.title;
            frameworkBtn.id = image.title;
            frameworkBtn.style.marginRight = '5px';
            if (image.title === logos[0].title)
              frameworkBtn.style.backgroundColor = 'blue';
            else
              frameworkBtn.style.backgroundColor = 'white';
            frameworkBtn.appendChild(img);
            frameworkBtn.onclick = () => {
              selectedButton = image.title;
              frameworkBtn.style.backgroundColor = 'blue';
              templatePanel.render.querySelectorAll('button').forEach(btn => {
                if (btn !== frameworkBtn)
                  btn.style.backgroundColor = 'white';
              });
            }
            logoField.appendChild(frameworkBtn);
          });

          let heightInput = document.createElement('input');
          heightInput.id = 'height';
          heightInput.title = 'H (pts)';
          heightInput.type = typeof (sampleJSON[element].val.height);
          heightInput.value = sampleJSON[element].val.height;
          heightInput.style.height = '40px';
          heightInput.style.width = '40px';
          heightInput.disabled = true;
          heightInput.style.marginRight = '5px';
          logoField.appendChild(heightInput);

          let widthInput = document.createElement('input');
          widthInput.id = 'width';
          widthInput.title = 'W (pts)';
          widthInput.type = typeof (sampleJSON[element].val.width);
          widthInput.value = sampleJSON[element].val.width;
          widthInput.style.height = '40px';
          widthInput.style.width = '40px';
          widthInput.disabled = true;
          widthInput.style.marginRight = '5px';
          logoField.appendChild(widthInput);
        }
        break;
      // if table key is found, then create its div.
      case 'table':
        tableField = document.createElement('div');
        tableField.appendChild(document.createTextNode(`${element}:`));
        tableField.appendChild(document.createElement('p'));
        tableField.id = 'table';
        sampleJSON[element].val.insert_rows.forEach((row) => {
          let rowLabel = document.createTextNode(`${row[0]}:`);
          let rowInput = document.createElement('input');
          rowInput.id = `${row[0]}`;
          rowInput.type = 'text';
          rowInput.value = row[1];
          tableField.appendChild(document.createElement('p'));
          tableField.appendChild(rowLabel);
          tableField.appendChild(document.createElement('p'));
          tableField.appendChild(rowInput);
        });
        break;
      // input fields for text
      default:
        labelField = document.createTextNode(`${element}:`);
        inputField = document.createElement('input');
        inputField.id = element;
        if (sampleJSON[element] === undefined) {
          inputField.type = 'text';
          inputField.placeholder = 'Text To Fill Template';
        }
        else {
          inputField.type = sampleJSON[element].type;
          inputField.value = sampleJSON[element].val;
        }
    }

    panelDiv.appendChild(labelField);
    panelDiv.appendChild(document.createElement('p'));

    if (inputField)
      panelDiv.appendChild(inputField);

    panelDiv.appendChild(document.createElement('p'));
  });

  if (logoField) {
    panelDiv.appendChild(dividerDiv.cloneNode());
    panelDiv.appendChild(logoField);
  }

  if (tableField) {
    panelDiv.appendChild(dividerDiv.cloneNode());
    panelDiv.appendChild(tableField);
  }

  // Fill template and Reset document buttons
  let fillTemplateButton = document.createElement('button');
  fillTemplateButton.textContent = 'Fill Template';
  fillTemplateButton.style.backgroundColor = 'white';
  fillTemplateButton.style.color = 'blue';
  fillTemplateButton.onclick = () => fillTemplate(instance); // Fill the template with data

  let resetButton = document.createElement('button');
  resetButton.textContent = 'Reset Document';
  resetButton.style.backgroundColor = 'white';
  resetButton.style.color = 'blue';
  resetButton.onclick = () => instance.UI.loadDocument(initialDoc);

  panelDiv.appendChild(dividerDiv.cloneNode());
  panelDiv.appendChild(fillTemplateButton);
  panelDiv.appendChild(document.createElement('p'));
  panelDiv.appendChild(resetButton);

  return panelDiv;
};

// Collect data from the custom template sub-panel fields,
// then format it for the template
// and apply it to the loaded document.
const fillTemplate = async (instance) => {

  const formattedValues = {};

  // Collect data from input fields
  templatePanel.render.querySelectorAll('input').forEach((input) => {
    if (templatePanel.templateKeys.includes(input.id)) {
      formattedValues[input.id] = input.value;
    }
  });

  // Collect data from table fields
  let tableInputs = templatePanel.render.querySelectorAll('#table input');
  if (tableInputs.length > 0) {
    insertRows = [];
    tableInputs.forEach((input) =>
      insertRows.push([input.id, input.value])
    );

    sampleJSON.table.val.insert_rows = insertRows;

    formattedValues['table'] = sampleJSON.table.val;
  }

  // Collect data from logo field
  sampleJSON['logo'].val.image_url = window.location.origin + logos.find(img => img.title === selectedButton).file;
  formattedValues['logo'] = sampleJSON['logo'].val;

  // Apply the formatted template values
  await instance.Core.documentViewer.getDocument().applyTemplateValues(formattedValues);
};
</code></pre>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.apryse.com/web/get-started/samples/showcase-demo-office-template-fill.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
