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
5// Global variables
6const element = document.getElementById('viewer');
7let documentViewer = null;
8let sampleData = {};
9let templateApplied = false;
10const defaultDoc = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/invoice_template.docx';
11
12// Initialize WebViewer.
13WebViewer({
14 path: '/lib',
15 licenseKey: 'YOUR_LICENSE_KEY',
16}, element).then((instance) => {
17 documentViewer = instance.Core.documentViewer;
18 loadTemplateDocument(); // Load the default template document, initialize sample data, and generate input fields.
19});
20
21// Load default template document and initialize sample data.
22const loadTemplateDocument = async () => {
23 // Load DOCX template.
24 await documentViewer.loadDocument(defaultDoc, {extension: 'docx'});
25 templateApplied = false;
26 // Initialize sample data.
27 sampleData = {
28 invoice_number: '3467821',
29 bill_to_name: 'Victoria Guti\u00e9rrez',
30 ship_to_name: 'Mar\u00eda Rosales',
31 items: [
32 { description: 'Item 1', qty: '1', price: '10.00', total: '10.00' },
33 { description: 'Item 2', qty: '20', price: '20.00', total: '400.00' },
34 { description: 'Item 3', qty: '1', price: '0.00', total: '0.00' },
35 { description: 'Item 4', qty: '1', price: '0.00', total: '0.00' },
36 ],
37 subtotal: '410.00',
38 sales_tax_rate: '5.0%',
39 sales_t: '20.50',
40 total_t: '500.00',
41 };
42 generateInputFields(); // Generate input fields based on default sampleData values.
43};
44
45const fillTemplate = async () => {
46 // Update sampleData from the input field values.
47 // Each field is identified by its unique ID.
48 // The ID was originally generated from sampleData key for the fixed fields, or from (key, index, subKey) for array items.
49 Object.keys(sampleData).forEach(key => {
50 if (Array.isArray(sampleData[key])) {
51 // Array field items
52 sampleData[key].forEach((item, index) => {
53 Object.keys(item).forEach(subKey => {
54 const input = document.getElementById(`${key}_${index}_${subKey}`.toLowerCase());
55 sampleData[key][index][subKey] = input.value;
56 });
57 });
58 }
59 else {
60 // Non-array (fixed) fields
61 const input = document.getElementById(key.toLowerCase());
62 sampleData[key] = input.value;
63 }
64 });
65 // Apply JSON data to the PDF.
66 if(templateApplied) // Reload the template document if it has already been applied.
67 await documentViewer.loadDocument(defaultDoc, {extension: 'docx'});
68 await documentViewer.getDocument().applyTemplateValues(sampleData);
69 templateApplied = true;
70};
71
72// Generate input fields based on the sampleData structure.
73// The left div contains the fixed text inputs for non-array fields.
74// The right div contains the dynamic text inputs for array fields (items).
75// Each array item corresponds to a row in the table (loop in the template).
76const generateInputFields = () => {
77 // Clear previous controls from the 2 divs.
78 leftDiv.innerHTML = rightDiv.innerHTML = '';
79 Object.keys(sampleData).forEach(key => {
80 if (Array.isArray(sampleData[key])) {
81 // Array field - Create multiple text inputs for each item in the array in the right div.
82 sampleData[key].forEach((item, index) => {
83 // Create a header row before the first row.
84 if (index === 0) {
85 Object.keys(item).forEach(subKey => {
86 const desc = document.createElement('input');
87 desc.type = 'text';
88 desc.disabled = true;
89 desc.value = subKey || '';
90 rightDiv.appendChild(desc);
91 });
92 rightDiv.appendChild(document.createElement('br'));
93 }
94 // Create the rows that correspond to each item in the array.
95 Object.keys(item).forEach(subKey => {
96 const input = document.createElement('input');
97 // Generate a unique ID for each input based on the key, index, and subKey.
98 input.id = `${key}_${index}_${subKey}`.toLowerCase();
99 input.type = 'text';
100 input.value = item[subKey] || '';
101 rightDiv.appendChild(input);
102 });
103 // Add a delete button for each row.
104 const deleteButton = document.createElement('button');
105 deleteButton.textContent = '\u2716'; // Unicode for '✖' symbol
106 deleteButton.className = 'btn-delete';
107 deleteButton.onclick = () => {
108 sampleData[key].splice(index, 1);
109 generateInputFields(); // Regenerate the input fields to reflect the deletion.
110 }
111 rightDiv.appendChild(deleteButton);
112 rightDiv.appendChild(document.createElement('br'));
113 });
114 }
115 else { // Create text input for each field in the left div.
116 const desc = document.createElement('input');
117 desc.disabled = true;
118 desc.type = 'text';
119 desc.value = key + ': ';
120 leftDiv.appendChild(desc);
121 const input = document.createElement('input');
122 input.type = 'text';
123 input.value = sampleData[key] || '';
124 // Use the key as the ID for easy lookup later.
125 input.id = key.toLowerCase();
126 leftDiv.appendChild(input);
127 leftDiv.appendChild(document.createElement('br'));
128 }
129 });
130 // Create a button to add a new item to the items array.
131 const addRowButton = document.createElement('button');
132 addRowButton.textContent = 'Add Row';
133 addRowButton.className = 'btn';
134 addRowButton.onclick = () => {
135 const randQty = Math.floor(Math.random() * 20) + 1;
136 sampleData.items.push({ description: `Item ${sampleData.items.length + 1}`, qty: randQty.toString(), price: '0.00', total: '0.00' });
137 generateInputFields(); // Regenerate the input fields to reflect the new row.
138 };
139 addRowButton.disabled = sampleData.items.length >= 10; // Limit to 10 items.
140 rightDiv.appendChild(addRowButton);
141}
142
143// UI section
144
145// Create a container for the controls.
146const controlsContainer = document.createElement('div');
147
148// Create 2 divs inside the container for left and right sections.
149const leftDiv = document.createElement('div');
150const rightDiv = document.createElement('div');
151leftDiv.className = rightDiv.className = 'vertical-container'; // Side-by-side divs using (display: inline-block) and (vertical-align: top).
152leftDiv.style.width = "40%";
153rightDiv.style.width = "60%"; // Right div is wider to accommodate table rows.
154controlsContainer.appendChild(leftDiv);
155controlsContainer.appendChild(rightDiv);
156
157const fillInvoiceButton = document.createElement('button');
158fillInvoiceButton.className = 'btn';
159fillInvoiceButton.textContent = 'Fill Template';
160fillInvoiceButton.onclick = async () => {
161 await fillTemplate(); // Generate the invoice by filling the template with data.
162};
163controlsContainer.appendChild(fillInvoiceButton);
164
165const resetDocumentButton = document.createElement('button');
166resetDocumentButton.className = 'btn';
167resetDocumentButton.textContent = '🗘 Reset Document';
168resetDocumentButton.onclick = async () => {
169 await loadTemplateDocument(); // Reset document, data and input fields.
170};
171controlsContainer.appendChild(resetDocumentButton);
172element.insertBefore(controlsContainer, element.firstChild);
173
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