Easily generate invoices using loops, denoted by {{loop var}} and {{endloop}} clauses. Loops are used to repeat content in a document and fill in unique data for each repetition - especially when building tables.
Note: The demo focuses on generating rows with the provided data. It does not perform calculations on invoice items.
This demo allows you to:
Implementation steps
To add Invoice Generation capability with WebViewer:
Step 1: Get started with WebViewer in your preferred web stack.
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 collects some data regarding your usage of the SDK for product improvement.
The data that Apryse collects include:
For clarity, no other data is collected by the SDK and Apryse has no access to the contents of your documents.
If you wish to continue without data collection, contact us and we will email you a no-tracking trial key for you to get started.
1// ES6 Compliant Syntax
2// GitHub Copilot - GPT-4 Model - October 14, 2025
3// File: invoice-generation/index.js
4
5const licenseKey = 'YOUR_WEBVIEWER_LICENSE_KEY';
6
7// Global variables
8const element = document.getElementById('viewer');
9let documentViewer = null;
10let sampleData = {};
11let templateApplied = false;
12const defaultDoc = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/invoice_template.docx';
13
14// Initialize WebViewer
15WebViewer({
16 path: '/lib',
17 licenseKey: licenseKey,
18}, element).then((instance) => {
19 documentViewer = instance.Core.documentViewer;
20 loadTemplateDocument(); // Load the default template document, initialize sample data and generate input fields
21});
22
23// Load default template document and initialize sample data
24const loadTemplateDocument = async () => {
25 // Load DOCX template
26 await documentViewer.loadDocument(defaultDoc, {extension: 'docx'});
27 templateApplied = false;
28 // Initialize sample data
29 sampleData = {
30 invoice_number: '3467821',
31 bill_to_name: 'Victoria Guti\u00e9rrez',
32 ship_to_name: 'Mar\u00eda Rosales',
33 items: [
34 { description: 'Item 1', qty: '1', price: '10.00', total: '10.00' },
35 { description: 'Item 2', qty: '20', price: '20.00', total: '400.00' },
36 { description: 'Item 3', qty: '1', price: '0.00', total: '0.00' },
37 { description: 'Item 4', qty: '1', price: '0.00', total: '0.00' },
38 ],
39 subtotal: '410.00',
40 sales_tax_rate: '5.0%',
41 sales_t: '20.50',
42 total_t: '500.00',
43 };
44 generateInputFields(); // Generate input fields based on default sampleData values
45};
46
47const fillTemplate = async () => {
48 // Update sampleData from the input field values
49 // Each field is identified by its unique ID
50 // The ID was originally generated from sampleData key for the fixed fields, or from (key, index, subKey) for array items
51 Object.keys(sampleData).forEach(key => {
52 if (Array.isArray(sampleData[key])) {
53 // array field items
54 sampleData[key].forEach((item, index) => {
55 Object.keys(item).forEach(subKey => {
56 const input = document.getElementById(`${key}_${index}_${subKey}`.toLowerCase());
57 sampleData[key][index][subKey] = input.value;
58 });
59 });
60 }
61 else {
62 // non-array (fixed) fields
63 const input = document.getElementById(key.toLowerCase());
64 sampleData[key] = input.value;
65 }
66 });
67 // Apply JSON data to the PDF
68 if(templateApplied) // reload the template document if it has already been applied
69 await documentViewer.loadDocument(defaultDoc, {extension: 'docx'});
70 await documentViewer.getDocument().applyTemplateValues(sampleData);
71 templateApplied = true;
72};
73
74// Generate input fields based on the sampleData structure
75// The left div contains the fixed text inputs for non-array fields
76// The right div contains the dynamic text inputs for array fields (items).
77// Each array item corresponds to a row in the table (loop in the template)
78const generateInputFields = () => {
79 // clear previous controls from the 2 divs
80 leftDiv.innerHTML = rightDiv.innerHTML = '';
81 Object.keys(sampleData).forEach(key => {
82 if (Array.isArray(sampleData[key])) {
83 // array field - create multiple text inputs for each item in the array in the right div
84 sampleData[key].forEach((item, index) => {
85 // create a header row before the first row
86 if (index === 0) {
87 Object.keys(item).forEach(subKey => {
88 const desc = document.createElement('input');
89 desc.type = 'text';
90 desc.disabled = true;
91 desc.value = subKey || '';
92 rightDiv.appendChild(desc);
93 });
94 rightDiv.appendChild(document.createElement('br'));
95 }
96 // create the rows that correspond to each item in the array
97 Object.keys(item).forEach(subKey => {
98 const input = document.createElement('input');
99 // generate a unique ID for each input based on the key, index, and subKey
100 input.id = `${key}_${index}_${subKey}`.toLowerCase();
101 input.type = 'text';
102 input.value = item[subKey] || '';
103 rightDiv.appendChild(input);
104 });
105 // add a delete button for each row
106 const deleteButton = document.createElement('button');
107 deleteButton.textContent = '\u2716'; // Unicode for '✖' symbol
108 deleteButton.className = 'btn-delete';
109 deleteButton.onclick = () => {
110 sampleData[key].splice(index, 1);
111 generateInputFields(); // regenerate the input fields to reflect the deletion
112 }
113 rightDiv.appendChild(deleteButton);
114 rightDiv.appendChild(document.createElement('br'));
115 });
116 }
117 else { // create text input for each field in the left div
118 const desc = document.createElement('input');
119 desc.disabled = true;
120 desc.type = 'text';
121 desc.value = key + ': ';
122 leftDiv.appendChild(desc);
123 const input = document.createElement('input');
124 input.type = 'text';
125 input.value = sampleData[key] || '';
126 // use the key as the ID for easy lookup later
127 input.id = key.toLowerCase();
128 leftDiv.appendChild(input);
129 leftDiv.appendChild(document.createElement('br'));
130 }
131 });
132 // create a button to add a new item to the items array
133 const addRowButton = document.createElement('button');
134 addRowButton.textContent = 'Add Row';
135 addRowButton.className = 'btn';
136 addRowButton.onclick = () => {
137 const randQty = Math.floor(Math.random() * 20) + 1;
138 sampleData.items.push({ description: `Item ${sampleData.items.length + 1}`, qty: randQty.toString(), price: '0.00', total: '0.00' });
139 generateInputFields(); // regenerate the input fields to reflect the new row
140 };
141 addRowButton.disabled = sampleData.items.length >= 10; // limit to 10 items
142 rightDiv.appendChild(addRowButton);
143}
144
145// UI section
146
147// Create a container for the controls
148const controlsContainer = document.createElement('div');
149
150// Create 2 divs inside the container for left and right sections
151const leftDiv = document.createElement('div');
152const rightDiv = document.createElement('div');
153leftDiv.className = rightDiv.className = 'vertical-container'; // side-by-side divs using (display: inline-block) and (vertical-align: top)
154leftDiv.style.width = "40%";
155rightDiv.style.width = "60%"; // right div is wider to accommodate table rows
156controlsContainer.appendChild(leftDiv);
157controlsContainer.appendChild(rightDiv);
158
159const fillInvoiceButton = document.createElement('button');
160fillInvoiceButton.className = 'btn';
161fillInvoiceButton.textContent = 'Fill Template';
162fillInvoiceButton.onclick = async () => {
163 await fillTemplate(); // generate the invoice by filling the template with data
164};
165controlsContainer.appendChild(fillInvoiceButton);
166
167const resetDocumentButton = document.createElement('button');
168resetDocumentButton.className = 'btn';
169resetDocumentButton.textContent = '🗘 Reset Document';
170resetDocumentButton.onclick = async () => {
171 await loadTemplateDocument(); // reset document, data and input fields
172};
173controlsContainer.appendChild(resetDocumentButton);
174element.insertBefore(controlsContainer, element.firstChild);
175
1/* side-by-side divs */
2.vertical-container {
3 display: inline-block;
4 vertical-align: top;
5}
6
7/* General Button Styles */
8.btn {
9 background-color: #007bff;
10 margin: 0 10px;
11 padding: 5px 10px;
12 border: 1px solid #ccc;
13 border-radius: 4px;
14 cursor: pointer;
15 font-size: 14px;
16 transition: all 0.2s ease;
17 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
18 color: white;
19}
20
21.btn:hover {
22 background-color: #0056b3;
23 transform: translateY(-1px);
24 box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
25}
26
27.btn:active {
28 transform: translateY(1px);
29 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
30}
31
32.btn:disabled {
33 background-color: #ccc;
34 cursor: not-allowed;
35 box-shadow: none;
36}
37
38/* Delete Button Styles */
39.btn-delete {
40 background-color: red;
41 padding: 0 5px;
42 color: white
43}
44
45/* Responsive Design */
46@media (max-width: 768px) {
47 .btn {
48 width: 100%;
49 margin: 5px 0;
50 }
51}
52
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales