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
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