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.

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
Step 2: Download any required modules listed in the Demo Dependencies section below
Step 3: Add the ES6 JavaScript sample code provided in this guide

Demo Dependencies
This sample uses the following:

Want to see a live version of this demo?

Try the Office Template Fill demo

1// ES6 Compliant Syntax
2// Copilot name: GitHub Copilot, version: N/A, model: GPT-4, version: N/A, date: August 13, 2025
3// File: index.js
4
5import WebViewer from '@pdftron/webviewer';
6
7const initialDoc = 'https://apryse.s3.us-west-1.amazonaws.com/public/files/samples/letter_template.docx';
8
9// The list of registered panels in the main webviewer
10let viewerPanels = null;
11
12// The tab panel, representing the webviewer left panel
13const tabPanel = {
14 handle: null,
15 dataElement: 'tabPanel'
16};
17
18// The custom template sub-panel to be registered
19const templatePanel = {
20 handle: null,
21 dataElement: 'templatePanel',
22 templateKeys: null,
23 render: null,
24};
25
26// The logos to choose from
27const logos = [
28 { title: 'Angular', file: '/assets/angular.png' },
29 { title: 'React', file: '/assets/react.png' },
30 { title: 'Vue', file: '/assets/vue.png' },
31];
32
33// The rows to insert into the table
34let insertRows = [
35 ['Title', 'Web Developer'],
36 ['Start Date', '01/10/2022'],
37 ['Expected Salary', '$100,000'],
38];
39
40// The logo to insert into the document
41const logoJSON = {
42 val: { image_url: logos[0].file, width: 64, height: 64 },
43 type: 'image',
44};
45
46// The table to insert into the document
47const tableJSON = {
48 val: { insert_rows: insertRows },
49 type: 'table',
50};
51
52// A sample JSON data
53const sampleJSON = {
54 dest_street_address: { val: '187 Duizelstraat', type: 'text' },
55 applicant_first_name: { val: 'Janice', type: 'text' },
56 applicant_surname: { val: 'Symonds', type: 'text' },
57 Date: { val: '01/06/2022', type: 'text' },
58 sender_name: { val: 'Arnold Smith', type: 'text' },
59 company_name: { val: 'React-Faux', type: 'text' },
60 logo: logoJSON,
61 table: tableJSON,
62};
63
64// The initial selected logo. By default it is Angular
65let selectedButton = logos[0].title;
66
67WebViewer(
68 {
69 path: '/lib',
70 initialDoc: initialDoc,
71 enableFilePicker: true, // Enable file picker to open files. In WebViewer -> menu icon -> Open File
72 enableMeasurement: true,
73 licenseKey: 'YOUR_LICENSE_KEY', // Replace with your license key
74 },
75 document.getElementById('viewer')
76).then((instance) => {
77 const { documentViewer } = instance.Core;
78
79 // Customize the main webviewer left panel after the load completion.
80 // The left panel will display the template sub-panel with input fields.
81 // The input fields are representing the template keys were extracted from the loaded document.
82 documentViewer.addEventListener('documentLoaded', async () => {
83 const doc = documentViewer.getDocument();
84 templatePanel.templateKeys = await doc.getTemplateKeys();
85 customizeUI(instance);
86 });
87
88 console.log('✅ WebViewer loaded successfully.');
89}).catch((error) => {
90 console.error('❌ Failed to initialize WebViewer:', error);
91});
92
93// Customize the main webviewer left panel after the load completion
94const customizeUI = (instance) => {
95 const { UI } = instance;
96
97 // Close the tab panel (if it's open) for refreshment.
98 UI.closeElements([tabPanel.dataElement]);
99
100 // Get the list of registered panels in the main webviewer
101 viewerPanels = UI.getPanels();
102
103 // Find the Tab Panel to modify. The template sub-panel will be added to this Tab panel.
104 tabPanel.handle = viewerPanels.find((panel) => panel.dataElement === tabPanel.dataElement);
105
106 // Register the custom template sub-panel
107 RegisterTemplatePanel(instance);
108
109 // Add the new custom template sub-panel to list of sub-panels under the Tab Panel
110 templatePanel.handle = { render: templatePanel.dataElement };
111 tabPanel.handle.panelsList = [templatePanel.handle, ...tabPanel.handle.panelsList];
112
113 UI.openElements([tabPanel.dataElement]);
114 UI.setPanelWidth(tabPanel.dataElement, 400);
115};
116
117// Register the custom template sub-panel
118const RegisterTemplatePanel = (instance) => {
119 templatePanel.render = createTemplatePanelElements(instance);
120 instance.UI.addPanel({
121 dataElement: templatePanel.dataElement,
122 location: 'left',
123 icon: '<svg width="18px" height="18px" viewBox="0 0 24 24" id="圖層_1" data-name="圖層 1" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:#080808;}</style></defs><title>template</title><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"/><path class="cls-1" d="M12.5,4H20a.5.5,0,0,0,0-1H12.5a.5.5,0,0,0,0,1Z"/><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"/><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"/><path class="cls-1" d="M8,8H4.5a1,1,0,0,0,0,2H8A1,1,0,0,0,8,8Z"/><path class="cls-1" d="M8,11.67H4.5a1,1,0,0,0,0,2H8a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M8,15.33H4.5a1,1,0,0,0,0,2H8a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M8,19H4.5a1,1,0,0,0,0,2H8a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M14,8H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M14,11.67H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M14,15.33H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M14,19H10.5a1,1,0,0,0,0,2H14a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M19.5,8h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M19.5,11.67h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M19.5,15.33h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/><path class="cls-1" d="M19.5,19h-3a1,1,0,0,0,0,2h3a1,1,0,0,0,0-2Z"/></svg>',
124 title: 'Template',
125 render: () => templatePanel.render,
126 });
127};
128
129// Create the template panel elements.
130// The elements will be created dynamically,
131// based on the template keys extracted from
132// the loaded document.
133const createTemplatePanelElements = (instance) => {
134 let panelDiv = document.createElement('div');
135 panelDiv.id = 'template';
136 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.');
137 panelDiv.appendChild(paragraph);
138
139 let dividerDiv = document.createElement('div');
140 dividerDiv.style.borderTop = '1px solid #ccc';
141 dividerDiv.style.margin = '10px 0';
142 panelDiv.appendChild(dividerDiv);
143
144 if (templatePanel.templateKeys.length === 0) {
145 panelDiv.appendChild(document.createTextNode('Sorry, there are no template keys in this document.'));
146 return panelDiv;
147 }
148
149 let labelField = null;
150 let inputField = null;
151 let logoField = null;
152 let tableField = null;
153
154 templatePanel.templateKeys.forEach(element => {
155 switch (element) {
156 // if logo key is found, then create its div.
157 case 'logo':
158 logoField = document.createElement('div');
159 logoField.appendChild(document.createTextNode(`${element}:`));
160 logoField.appendChild(document.createElement('p'));
161 logoField.id = 'logo';
162 if (sampleJSON[element].type === 'image') {
163 logos.forEach(image => {
164 const img = document.createElement('img');
165 img.src = image.file;
166 img.alt = image.title;
167 img.style.width = '20px';
168 img.style.height = '20px';
169
170 let frameworkBtn = document.createElement('button');
171 frameworkBtn.title = image.title;
172 frameworkBtn.id = image.title;
173 frameworkBtn.style.marginRight = '5px';
174 if (image.title === logos[0].title)
175 frameworkBtn.style.backgroundColor = 'blue';
176 else
177 frameworkBtn.style.backgroundColor = 'white';
178 frameworkBtn.appendChild(img);
179 frameworkBtn.onclick = () => {
180 selectedButton = image.title;
181 frameworkBtn.style.backgroundColor = 'blue';
182 templatePanel.render.querySelectorAll('button').forEach(btn => {
183 if (btn !== frameworkBtn)
184 btn.style.backgroundColor = 'white';
185 });
186 }
187 logoField.appendChild(frameworkBtn);
188 });
189
190 let heightInput = document.createElement('input');
191 heightInput.id = 'height';
192 heightInput.title = 'H (pts)';
193 heightInput.type = typeof (sampleJSON[element].val.height);
194 heightInput.value = sampleJSON[element].val.height;
195 heightInput.style.height = '40px';
196 heightInput.style.width = '40px';
197 heightInput.disabled = true;
198 heightInput.style.marginRight = '5px';
199 logoField.appendChild(heightInput);
200
201 let widthInput = document.createElement('input');
202 widthInput.id = 'width';
203 widthInput.title = 'W (pts)';
204 widthInput.type = typeof (sampleJSON[element].val.width);
205 widthInput.value = sampleJSON[element].val.width;
206 widthInput.style.height = '40px';
207 widthInput.style.width = '40px';
208 widthInput.disabled = true;
209 widthInput.style.marginRight = '5px';
210 logoField.appendChild(widthInput);
211 }
212 break;
213 // if table key is found, then create its div.
214 case 'table':
215 tableField = document.createElement('div');
216 tableField.appendChild(document.createTextNode(`${element}:`));
217 tableField.appendChild(document.createElement('p'));
218 tableField.id = 'table';
219 sampleJSON[element].val.insert_rows.forEach((row) => {
220 let rowLabel = document.createTextNode(`${row[0]}:`);
221 let rowInput = document.createElement('input');
222 rowInput.id = `${row[0]}`;
223 rowInput.type = 'text';
224 rowInput.value = row[1];
225 tableField.appendChild(document.createElement('p'));
226 tableField.appendChild(rowLabel);
227 tableField.appendChild(document.createElement('p'));
228 tableField.appendChild(rowInput);
229 });
230 break;
231 // input fields for text
232 default:
233 labelField = document.createTextNode(`${element}:`);
234 inputField = document.createElement('input');
235 inputField.id = element;
236 if (sampleJSON[element] === undefined) {
237 inputField.type = 'text';
238 inputField.placeholder = 'Text To Fill Template';
239 }
240 else {
241 inputField.type = sampleJSON[element].type;
242 inputField.value = sampleJSON[element].val;
243 }
244 }
245
246 panelDiv.appendChild(labelField);
247 panelDiv.appendChild(document.createElement('p'));
248
249 if (inputField)
250 panelDiv.appendChild(inputField);
251
252 panelDiv.appendChild(document.createElement('p'));
253 });
254
255 if (logoField) {
256 panelDiv.appendChild(dividerDiv.cloneNode());
257 panelDiv.appendChild(logoField);
258 }
259
260 if (tableField) {
261 panelDiv.appendChild(dividerDiv.cloneNode());
262 panelDiv.appendChild(tableField);
263 }
264
265 // Fill template and Reset document buttons
266 let fillTemplateButton = document.createElement('button');
267 fillTemplateButton.textContent = 'Fill Template';
268 fillTemplateButton.style.backgroundColor = 'white';
269 fillTemplateButton.style.color = 'blue';
270 fillTemplateButton.onclick = () => fillTemplate(instance); // Fill the template with data
271
272 let resetButton = document.createElement('button');
273 resetButton.textContent = 'Reset Document';
274 resetButton.style.backgroundColor = 'white';
275 resetButton.style.color = 'blue';
276 resetButton.onclick = () => instance.UI.loadDocument(initialDoc);
277
278 panelDiv.appendChild(dividerDiv.cloneNode());
279 panelDiv.appendChild(fillTemplateButton);
280 panelDiv.appendChild(document.createElement('p'));
281 panelDiv.appendChild(resetButton);
282
283 return panelDiv;
284};
285
286// Collect data from the custom template sub-panel fields,
287// then format it for the template
288// and apply it to the loaded document.
289const fillTemplate = async (instance) => {
290
291 const formattedValues = {};
292
293 // Collect data from input fields
294 templatePanel.render.querySelectorAll('input').forEach((input) => {
295 if (templatePanel.templateKeys.includes(input.id)) {
296 formattedValues[input.id] = input.value;
297 }
298 });
299
300 // Collect data from table fields
301 let tableInputs = templatePanel.render.querySelectorAll('#table input');
302 if (tableInputs.length > 0) {
303 insertRows = [];
304 tableInputs.forEach((input) =>
305 insertRows.push([input.id, input.value])
306 );
307
308 sampleJSON.table.val.insert_rows = insertRows;
309
310 formattedValues['table'] = sampleJSON.table.val;
311 }
312
313 // Collect data from logo field
314 sampleJSON['logo'].val.image_url = window.location.origin + logos.find(img => img.title === selectedButton).file;
315 formattedValues['logo'] = sampleJSON['logo'].val;
316
317 // Apply the formatted template values
318 await instance.Core.documentViewer.getDocument().applyTemplateValues(formattedValues);
319};

Did you find this helpful?

Trial setup questions?

Ask experts on Discord

Need other help?

Contact Support

Pricing or product questions?

Contact Sales