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 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 DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales