By Kevin Herschbach | 2026 Jan 30

13 min
Tags
template
automated document generation
apryse webviewer
The ability to dynamically generate professional invoices from structured data is essential for businesses of all sizes. The challenge lies in creating a maintainable system where business users can update invoice layouts without touching code, where data flows seamlessly into templates, and where the output looks perfect every time.
Template-based PDF generation solves this problem by separating your data from your document design. With the right SDK, you can define invoice layouts in familiar tools like Microsoft Word, then programmatically populate them with real-time data.
In this tutorial, you'll learn how to generate professional invoice PDFs using a template-based PDF generation SDK for invoices with Apryse WebViewer. You’ll take an invoice layout, populate it with structured data in a JavaScript application, and generate a polished PDF; without hardcoding layouts or touching design logic in code.
To achieve this, you'll follow these steps:
The result will look like this:

The invoice's text fields are automatically populated with the data entered into the web form
In this demonstration, you'll use npm as your runtime environment and package manager to install WebViewer SDK v11.10.0. This SDK version is compatible with Node.js 8 through 24.
You can check your installed Node.js version by running node --version.
In your project directory, create a package.json by running npm init -y, then install the WebViewer SDK.
npm install @pdftron/webviewerIn your project root, create a new directory public with a sub-directory lib that in turn contains a sub-directory webviewer.
mkdir -p public/lib/webviewerNext, in node_modules/@pdftron/webviewer/public, locate the directories core, ui, and the file webviewer.min.js. Copy them into your newly created directory public/lib/webviewer.
cp -R node_modules/@pdftron/webviewer/public/core node_modules/@pdftron/webviewer/public/ui node_modules/@pdftron/webviewer/webviewer.min.js public/lib/webviewer/Your public directory's structure should now look like this:
public
└── lib
└── webviewer
├── core
└── ...
├── ui
└── ...
└── webviewer.min.jsIn the public directory, create a basic index.html that imports webviewer.min.js. You'll extend the code in the next step.
Also in the public directory, create an index.css file to apply some styling:
Now you'll create an interactive invoice generator. It takes input from a web form filled out by the user and automatically injects those values into a Word document (.docx) template. It then generates an invoice from that template, which the user can download as a PDF.
To implement this, you can copy the sample code from the documentation and use it inside script tags in your index.html file's body. Make sure to generate a free trial license key from the same page first, which will be added to the sample code automatically.
⚠️ One important thing to change is the path to the WebViewer assets you copied earlier. Instead of path: '/lib', it should say path: '/lib/webviewer' to reflect your directory structure.
Also note that the sample code uses the template file invoice_template.docx. Feel free to take a closer look at its template keys to better understand their relationship with the data they'll be populated with.
After implementing the sample code, your final index.html will look like this:
Let's break down the code.
element: Grabs the viewer divdocumentViewer: Will hold the WebViewer instance (starts as null, gets set later)sampleData: Will store all the invoice data (numbers, names, items)templateApplied: A flag to track whether the template has already been filled (starts false)defaultDoc: URL to the invoice templateWebViewer({ ... }, element): Creates a new WebViewer instance.then((instance) => { ... }): Runs after WebViewer finishes loadingloadTemplateDocument(): Immediately loads the default templateasync: Makes this function asynchronous so it can wait for the document to loadawait documentViewer.loadDocument(...): Loads the DOCX file from the URLtemplateApplied = false: Resets the flagsampleData = { ... }: Creates default invoice datagenerateInputFields(): Creates the input form based on this dataThis is the most complex function. Let's break it into smaller pieces.
Object.keys(sampleData).forEach(key => {sampleDatakey will be: "invoice_number", "bill_to_name", "items", "subtotal", etc.if (Array.isArray(sampleData[key])) {
sampleData[key].forEach((item, index) => {index === 0)<br> after the headeritems_0_description (value: "Item 1")items_0_qty (value: "1")items_0_price (value: "10.00")items_0_total (value: "10.00")splice(index, 1) removes that item from the arraygenerateInputFields() rebuilds the entire form (now with one fewer row)invoice_number:invoice_numberfillTemplate() to generate the invoiceelement.insertBefore(controlsContainer, element.firstChild);insertBefore(new, existing) inserts the new element before the existing oneNow go ahead and test the app.
npx http-server -a localhostAt the top, you can see the web form to be filled out by the user. The data entered there will populate the template keys in the invoice document below when you click on the "Fill Template" button.
To download the invoice as a PDF, click on the hamburger menu in the top left of WebViewer and select "Download".

The document templates take the data from the web form and populate the invoice fields with it
In this tutorial, you've integrated PDF generation for invoices using the Apryse WebViewer SDK's template feature.
However, template-based PDF generation is not restricted to invoices. You can use any data in JSON format to generate any kind of PDF by defining your own template keys. Take a look at the SDK documentation's Data Model page for more details.
To explore more of what the Apryse SDK has to offer, check out its documentation and the various samples. Begin your free trial or contact our sales team to get started.
Tags
template
automated document generation
apryse webviewer

Kevin Herschbach
Technical Content Writer
Share this post
<html>
<head>
<title>PDF Generator for Invoices</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<link rel="stylesheet" href="index.css">
</head>
<!-- Import WebViewer as a script tag -->
<script src='/lib/webviewer/webviewer.min.js'></script>
<body>
<div id='viewer' style='width: 1024px; height: 600px; margin: 0 auto;'></div>
</body>
</html>/* side-by-side divs */
.vertical-container {
display: inline-block;
vertical-align: top;
}
/* General Button Styles */
.btn {
background-color: #007bff;
margin: 0 10px;
padding: 5px 10px;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
color: white;
}
.btn:hover {
background-color: #0056b3;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.btn:active {
transform: translateY(1px);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.btn:disabled {
background-color: #ccc;
cursor: not-allowed;
box-shadow: none;
}
/* Delete Button Styles */
.btn-delete {
background-color: red;
padding: 0 5px;
color: white
}
/* Responsive Design */
@media (max-width: 768px) {
.btn {
width: 100%;
margin: 5px 0;
}
} // Initialize WebViewer.
WebViewer({
// Update the path 👇
path: '/lib/webviewer',
licenseKey: 'YOUR_LICENSE_KEY',
}, element).then((instance) => {
documentViewer = instance.Core.documentViewer;
loadTemplateDocument(); // Load the default template document, initialize sample data, and generate input fields.
});const element = document.getElementById('viewer');
let documentViewer = null;
let sampleData = {};
let templateApplied = false;
const defaultDoc = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/invoice_template.docx';WebViewer({
path: '/lib/webviewer',
licenseKey: 'YOUR_LICENSE_KEY',
}, element).then((instance) => {
documentViewer = instance.Core.documentViewer;
loadTemplateDocument();
});const loadTemplateDocument = async () => {
// Load DOCX template.
await documentViewer.loadDocument(defaultDoc, {extension: 'docx'});
templateApplied = false;
// Initialize sample data.
sampleData = {
invoice_number: '3467821',
bill_to_name: 'Victoria Gutiérrez',
ship_to_name: 'María Rosales',
items: [
{ description: 'Item 1', qty: '1', price: '10.00', total: '10.00' },
{ description: 'Item 2', qty: '20', price: '20.00', total: '400.00' },
{ description: 'Item 3', qty: '1', price: '0.00', total: '0.00' },
{ description: 'Item 4', qty: '1', price: '0.00', total: '0.00' },
],
subtotal: '410.00',
sales_tax_rate: '5.0%',
sales_t: '20.50',
total_t: '500.00',
};
generateInputFields();
};const fillTemplate = async () => {
// Update sampleData from the input field values.
Object.keys(sampleData).forEach(key => {
if (Array.isArray(sampleData[key])) {
// Array field items
sampleData[key].forEach((item, index) => {
Object.keys(item).forEach(subKey => {
const input = document.getElementById(`${key}_${index}_${subKey}`.toLowerCase());
sampleData[key][index][subKey] = input.value;
});
});
}
else {
// Non-array (fixed) fields
const input = document.getElementById(key.toLowerCase());
sampleData[key] = input.value;
}
});
// Apply JSON data to the PDF.
if(templateApplied)
await documentViewer.loadDocument(defaultDoc, {extension: 'docx'});
await documentViewer.getDocument().applyTemplateValues(sampleData);
templateApplied = true;
};const generateInputFields = () => {
// Clear previous controls from the 2 divs.
leftDiv.innerHTML = rightDiv.innerHTML = '';if (index === 0) {
Object.keys(item).forEach(subKey => {
const desc = document.createElement('input');
desc.type = 'text';
desc.disabled = true;
desc.value = subKey || '';
rightDiv.appendChild(desc);
});
rightDiv.appendChild(document.createElement('br'));
}Object.keys(item).forEach(subKey => {
const input = document.createElement('input');
input.id = `${key}_${index}_${subKey}`.toLowerCase();
input.type = 'text';
input.value = item[subKey] || '';
rightDiv.appendChild(input);
});const deleteButton = document.createElement('button');
deleteButton.textContent = '\u2716'; // ✖ symbol
deleteButton.className = 'btn-delete';
deleteButton.onclick = () => {
sampleData[key].splice(index, 1);
generateInputFields();
}
rightDiv.appendChild(deleteButton);
rightDiv.appendChild(document.createElement('br'));else {
const desc = document.createElement('input');
desc.disabled = true;
desc.type = 'text';
desc.value = key + ': ';
leftDiv.appendChild(desc);
const input = document.createElement('input');
input.type = 'text';
input.value = sampleData[key] || '';
input.id = key.toLowerCase();
leftDiv.appendChild(input);
leftDiv.appendChild(document.createElement('br'));
}const addRowButton = document.createElement('button');
addRowButton.textContent = 'Add Row';
addRowButton.className = 'btn';
addRowButton.onclick = () => {
const randQty = Math.floor(Math.random() * 20) + 1;
sampleData.items.push({
description: `Item ${sampleData.items.length + 1}`,
qty: randQty.toString(),
price: '0.00',
total: '0.00'
});
generateInputFields();
};
addRowButton.disabled = sampleData.items.length >= 10;
rightDiv.appendChild(addRowButton);const controlsContainer = document.createElement('div');
const leftDiv = document.createElement('div');
const rightDiv = document.createElement('div');
leftDiv.className = rightDiv.className = 'vertical-container';
leftDiv.style.width = "40%";
rightDiv.style.width = "60%";
controlsContainer.appendChild(leftDiv);
controlsContainer.appendChild(rightDiv);const fillInvoiceButton = document.createElement('button');
fillInvoiceButton.className = 'btn';
fillInvoiceButton.textContent = 'Fill Template';
fillInvoiceButton.onclick = async () => {
await fillTemplate();
};
controlsContainer.appendChild(fillInvoiceButton);const resetDocumentButton = document.createElement('button');
resetDocumentButton.className = 'btn';
resetDocumentButton.textContent = '🗘 Reset Document';
resetDocumentButton.onclick = async () => {
await loadTemplateDocument();
};
controlsContainer.appendChild(resetDocumentButton);<html>
<head>
<title>PDF Generator for Invoices</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<link rel="stylesheet" href="index.css">
</head>
<!-- Import WebViewer as a script tag -->
<script src='/lib/webviewer/webviewer.min.js'></script>
<body>
<div id='viewer' style='width: 1024px; height: 600px; margin: 0 auto;'></div>
<script>
// Global variables
const element = document.getElementById('viewer');
let documentViewer = null;
let sampleData = {};
let templateApplied = false;
const defaultDoc = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/invoice_template.docx';
// Initialize WebViewer.
WebViewer({
path: '/lib/webviewer',
licenseKey: 'YOUR_LICENSE_KEY',
}, element).then((instance) => {
documentViewer = instance.Core.documentViewer;
loadTemplateDocument(); // Load the default template document, initialize sample data, and generate input fields.
});
// Load default template document and initialize sample data.
const loadTemplateDocument = async () => {
// Load DOCX template.
await documentViewer.loadDocument(defaultDoc, {extension: 'docx'});
templateApplied = false;
// Initialize sample data.
sampleData = {
invoice_number: '3467821',
bill_to_name: 'Victoria Guti\u00e9rrez',
ship_to_name: 'Mar\u00eda Rosales',
items: [
{ description: 'Item 1', qty: '1', price: '10.00', total: '10.00' },
{ description: 'Item 2', qty: '20', price: '20.00', total: '400.00' },
{ description: 'Item 3', qty: '1', price: '0.00', total: '0.00' },
{ description: 'Item 4', qty: '1', price: '0.00', total: '0.00' },
],
subtotal: '410.00',
sales_tax_rate: '5.0%',
sales_t: '20.50',
total_t: '500.00',
};
generateInputFields(); // Generate input fields based on default sampleData values.
};
const fillTemplate = async () => {
// Update sampleData from the input field values.
// Each field is identified by its unique ID.
// The ID was originally generated from sampleData key for the fixed fields, or from (key, index, subKey) for array items.
Object.keys(sampleData).forEach(key => {
if (Array.isArray(sampleData[key])) {
// Array field items
sampleData[key].forEach((item, index) => {
Object.keys(item).forEach(subKey => {
const input = document.getElementById(`${key}_${index}_${subKey}`.toLowerCase());
sampleData[key][index][subKey] = input.value;
});
});
}
else {
// Non-array (fixed) fields
const input = document.getElementById(key.toLowerCase());
sampleData[key] = input.value;
}
});
// Apply JSON data to the PDF.
if(templateApplied) // Reload the template document if it has already been applied.
await documentViewer.loadDocument(defaultDoc, {extension: 'docx'});
await documentViewer.getDocument().applyTemplateValues(sampleData);
templateApplied = true;
};
// Generate input fields based on the sampleData structure.
// The left div contains the fixed text inputs for non-array fields.
// The right div contains the dynamic text inputs for array fields (items).
// Each array item corresponds to a row in the table (loop in the template).
const generateInputFields = () => {
// Clear previous controls from the 2 divs.
leftDiv.innerHTML = rightDiv.innerHTML = '';
Object.keys(sampleData).forEach(key => {
if (Array.isArray(sampleData[key])) {
// Array field - Create multiple text inputs for each item in the array in the right div.
sampleData[key].forEach((item, index) => {
// Create a header row before the first row.
if (index === 0) {
Object.keys(item).forEach(subKey => {
const desc = document.createElement('input');
desc.type = 'text';
desc.disabled = true;
desc.value = subKey || '';
rightDiv.appendChild(desc);
});
rightDiv.appendChild(document.createElement('br'));
}
// Create the rows that correspond to each item in the array.
Object.keys(item).forEach(subKey => {
const input = document.createElement('input');
// Generate a unique ID for each input based on the key, index, and subKey.
input.id = `${key}_${index}_${subKey}`.toLowerCase();
input.type = 'text';
input.value = item[subKey] || '';
rightDiv.appendChild(input);
});
// Add a delete button for each row.
const deleteButton = document.createElement('button');
deleteButton.textContent = '\u2716'; // Unicode for '✖' symbol
deleteButton.className = 'btn-delete';
deleteButton.onclick = () => {
sampleData[key].splice(index, 1);
generateInputFields(); // Regenerate the input fields to reflect the deletion.
}
rightDiv.appendChild(deleteButton);
rightDiv.appendChild(document.createElement('br'));
});
}
else { // Create text input for each field in the left div.
const desc = document.createElement('input');
desc.disabled = true;
desc.type = 'text';
desc.value = key + ': ';
leftDiv.appendChild(desc);
const input = document.createElement('input');
input.type = 'text';
input.value = sampleData[key] || '';
// Use the key as the ID for easy lookup later.
input.id = key.toLowerCase();
leftDiv.appendChild(input);
leftDiv.appendChild(document.createElement('br'));
}
});
// Create a button to add a new item to the items array.
const addRowButton = document.createElement('button');
addRowButton.textContent = 'Add Row';
addRowButton.className = 'btn';
addRowButton.onclick = () => {
const randQty = Math.floor(Math.random() * 20) + 1;
sampleData.items.push({ description: `Item ${sampleData.items.length + 1}`, qty: randQty.toString(), price: '0.00', total: '0.00' });
generateInputFields(); // Regenerate the input fields to reflect the new row.
};
addRowButton.disabled = sampleData.items.length >= 10; // Limit to 10 items.
rightDiv.appendChild(addRowButton);
}
// UI section
// Create a container for the controls.
const controlsContainer = document.createElement('div');
// Create 2 divs inside the container for left and right sections.
const leftDiv = document.createElement('div');
const rightDiv = document.createElement('div');
leftDiv.className = rightDiv.className = 'vertical-container'; // Side-by-side divs using (display: inline-block) and (vertical-align: top).
leftDiv.style.width = "40%";
rightDiv.style.width = "60%"; // Right div is wider to accommodate table rows.
controlsContainer.appendChild(leftDiv);
controlsContainer.appendChild(rightDiv);
const fillInvoiceButton = document.createElement('button');
fillInvoiceButton.className = 'btn';
fillInvoiceButton.textContent = 'Fill Template';
fillInvoiceButton.onclick = async () => {
await fillTemplate(); // Generate the invoice by filling the template with data.
};
controlsContainer.appendChild(fillInvoiceButton);
const resetDocumentButton = document.createElement('button');
resetDocumentButton.className = 'btn';
resetDocumentButton.textContent = '🗘 Reset Document';
resetDocumentButton.onclick = async () => {
await loadTemplateDocument(); // Reset document, data and input fields.
};
controlsContainer.appendChild(resetDocumentButton);
element.insertBefore(controlsContainer, element.firstChild);
</script>
</body>
</html>