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:
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:
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 fullAPI: true,
74 licenseKey: 'YOUR_LICENSE_KEY', // Replace with your license key
75 },
76 document.getElementById('viewer')
77).then((instance) => {
78 const { documentViewer } = instance.Core;
79
80 // Customize the main webviewer left panel after the load completion.
81 // The left panel will display the template sub-panel with input fields.
82 // The input fields are representing the template keys were extracted from the loaded document.
83 documentViewer.addEventListener('documentLoaded', async () => {
84 const doc = documentViewer.getDocument();
85 templatePanel.templateKeys = await doc.getTemplateKeys();
86 customizeUI(instance);
87 });
88
89 console.log('✅ WebViewer loaded successfully.');
90}).catch((error) => {
91 console.error('❌ Failed to initialize WebViewer:', error);
92});
93
94// Customize the main webviewer left panel after the load completion
95const customizeUI = (instance) => {
96 const { UI } = instance;
97
98 // close the tab panel (if it's open) for refreshment.
99 UI.closeElements([tabPanel.dataElement]);
100
101 // Get the list of registered panels in the main webviewer
102 viewerPanels = UI.getPanels();
103
104 // Find the Tab Panel to modify. The template sub-panel will be added to this Tab panel.
105 tabPanel.handle = viewerPanels.find((panel) => panel.dataElement === tabPanel.dataElement);
106
107 // Register the custom template sub-panel
108 RegisterTemplatePanel(instance);
109
110 // Add the new custom template sub-panel to list of sub-panels under the Tab Panel
111 templatePanel.handle = { render: templatePanel.dataElement };
112 tabPanel.handle.panelsList = [templatePanel.handle, ...tabPanel.handle.panelsList];
113
114 UI.openElements([tabPanel.dataElement]);
115 UI.setPanelWidth(tabPanel.dataElement, 400);
116};
117
118// Register the custom template sub-panel
119const RegisterTemplatePanel = (instance) => {
120 templatePanel.render = createTemplatePanelElements(instance);
121 instance.UI.addPanel({
122 dataElement: templatePanel.dataElement,
123 location: 'left',
124 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>',
125 title: 'Template',
126 render: () => templatePanel.render,
127 });
128};
129
130// Create the template panel elements.
131// The elements will be created dynamically,
132// based on the template keys extracted from
133// the loaded document.
134const createTemplatePanelElements = (instance) => {
135 let panelDiv = document.createElement('div');
136 panelDiv.id = 'template';
137 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.');
138 panelDiv.appendChild(paragraph);
139
140 let dividerDiv = document.createElement('div');
141 dividerDiv.style.borderTop = '1px solid #ccc';
142 dividerDiv.style.margin = '10px 0';
143 panelDiv.appendChild(dividerDiv);
144
145 if (templatePanel.templateKeys.length === 0) {
146 panelDiv.appendChild(document.createTextNode('Sorry, there are no template keys in this document.'));
147 return panelDiv;
148 }
149
150 let labelField = null;
151 let inputField = null;
152 let logoField = null;
153 let tableField = null;
154
155 templatePanel.templateKeys.forEach(element => {
156 switch (element) {
157 // if logo key is found, then create its div.
158 case 'logo':
159 logoField = document.createElement('div');
160 logoField.appendChild(document.createTextNode(`${element}:`));
161 logoField.appendChild(document.createElement('p'));
162 logoField.id = 'logo';
163 if (sampleJSON[element].type === 'image') {
164 logos.forEach(image => {
165 const img = document.createElement('img');
166 img.src = image.file;
167 img.alt = image.title;
168 img.style.width = '20px';
169 img.style.height = '20px';
170
171 let frameworkBtn = document.createElement('button');
172 frameworkBtn.title = image.title;
173 frameworkBtn.id = image.title;
174 frameworkBtn.style.marginRight = '5px';
175 if (image.title === logos[0].title)
176 frameworkBtn.style.backgroundColor = 'blue';
177 else
178 frameworkBtn.style.backgroundColor = 'white';
179 frameworkBtn.appendChild(img);
180 frameworkBtn.onclick = () => {
181 selectedButton = image.title;
182 frameworkBtn.style.backgroundColor = 'blue';
183 templatePanel.render.querySelectorAll('button').forEach(btn => {
184 if (btn !== frameworkBtn)
185 btn.style.backgroundColor = 'white';
186 });
187 }
188 logoField.appendChild(frameworkBtn);
189 });
190
191 let heightInput = document.createElement('input');
192 heightInput.id = 'height';
193 heightInput.title = 'H (pts)';
194 heightInput.type = typeof (sampleJSON[element].val.height);
195 heightInput.value = sampleJSON[element].val.height;
196 heightInput.style.height = '40px';
197 heightInput.style.width = '40px';
198 heightInput.disabled = true;
199 heightInput.style.marginRight = '5px';
200 logoField.appendChild(heightInput);
201
202 let widthInput = document.createElement('input');
203 widthInput.id = 'width';
204 widthInput.title = 'W (pts)';
205 widthInput.type = typeof (sampleJSON[element].val.width);
206 widthInput.value = sampleJSON[element].val.width;
207 widthInput.style.height = '40px';
208 widthInput.style.width = '40px';
209 widthInput.disabled = true;
210 widthInput.style.marginRight = '5px';
211 logoField.appendChild(widthInput);
212 }
213 break;
214 // if table key is found, then create its div.
215 case 'table':
216 tableField = document.createElement('div');
217 tableField.appendChild(document.createTextNode(`${element}:`));
218 tableField.appendChild(document.createElement('p'));
219 tableField.id = 'table';
220 sampleJSON[element].val.insert_rows.forEach((row) => {
221 let rowLabel = document.createTextNode(`${row[0]}:`);
222 let rowInput = document.createElement('input');
223 rowInput.id = `${row[0]}`;
224 rowInput.type = 'text';
225 rowInput.value = row[1];
226 tableField.appendChild(document.createElement('p'));
227 tableField.appendChild(rowLabel);
228 tableField.appendChild(document.createElement('p'));
229 tableField.appendChild(rowInput);
230 });
231 break;
232 // input fields for text
233 default:
234 labelField = document.createTextNode(`${element}:`);
235 inputField = document.createElement('input');
236 inputField.id = element;
237 if (sampleJSON[element] === undefined) {
238 inputField.type = 'text';
239 inputField.placeholder = 'Text To Fill Template';
240 }
241 else {
242 inputField.type = sampleJSON[element].type;
243 inputField.value = sampleJSON[element].val;
244 }
245 }
246
247 panelDiv.appendChild(labelField);
248 panelDiv.appendChild(document.createElement('p'));
249
250 if (inputField)
251 panelDiv.appendChild(inputField);
252
253 panelDiv.appendChild(document.createElement('p'));
254 });
255
256 if (logoField) {
257 panelDiv.appendChild(dividerDiv.cloneNode());
258 panelDiv.appendChild(logoField);
259 }
260
261 if (tableField) {
262 panelDiv.appendChild(dividerDiv.cloneNode());
263 panelDiv.appendChild(tableField);
264 }
265
266 // Fill template and Reset document buttons
267 let fillTemplateButton = document.createElement('button');
268 fillTemplateButton.textContent = 'Fill Template';
269 fillTemplateButton.style.backgroundColor = 'white';
270 fillTemplateButton.style.color = 'blue';
271 fillTemplateButton.onclick = () => fillTemplate(instance); // Fill the template with data
272
273 let resetButton = document.createElement('button');
274 resetButton.textContent = 'Reset Document';
275 resetButton.style.backgroundColor = 'white';
276 resetButton.style.color = 'blue';
277 resetButton.onclick = () => instance.UI.loadDocument(initialDoc);
278
279 panelDiv.appendChild(dividerDiv.cloneNode());
280 panelDiv.appendChild(fillTemplateButton);
281 panelDiv.appendChild(document.createElement('p'));
282 panelDiv.appendChild(resetButton);
283
284 return panelDiv;
285};
286
287// Collect data from the custom template sub-panel fields,
288// then format it for the template
289// and apply it to the loaded document.
290const fillTemplate = async (instance) => {
291
292 const formattedValues = {};
293
294 // Collect data from input fields
295 templatePanel.render.querySelectorAll('input').forEach((input) => {
296 if (templatePanel.templateKeys.includes(input.id)) {
297 formattedValues[input.id] = input.value;
298 }
299 });
300
301 // Collect data from table fields
302 let tableInputs = templatePanel.render.querySelectorAll('#table input');
303 if (tableInputs.length > 0) {
304 insertRows = [];
305 tableInputs.forEach((input) =>
306 insertRows.push([input.id, input.value])
307 );
308
309 sampleJSON.table.val.insert_rows = insertRows;
310
311 formattedValues['table'] = sampleJSON.table.val;
312 }
313
314 // Collect data from logo field
315 sampleJSON['logo'].val.image_url = window.location.origin + logos.find(img => img.title === selectedButton).file;
316 formattedValues['logo'] = sampleJSON['logo'].val;
317
318 // Apply the formatted template values
319 await instance.Core.documentViewer.getDocument().applyTemplateValues(formattedValues);
320};
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales