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