Available Now: Explore our latest release with enhanced accessibility and powerful IDP features

How to Populate Record Data into PDF Forms in Salesforce

By Thomas Winter | 2021 Feb 08

Sanity Image
Read time

9 min

Many Salesforce users would like an automated flow to populate fields in document templates with Salesforce data ⁠— whether to send auto-filled letters to customers or quickly sign their internal documents, like contracts.

In an earlier post, we showed an easy way to embed features to view, edit, and annotate a Salesforce record attachment in a Salesforce app. This post considers how to use the same technologies to add and programmatically fill form fields as part of an automated workflow.

We'll use the same Salesforce attachment component featured in our previous guide, along with the powerful WebViewer API. On the WebViewer side, we'll follow this example of creating text fields and text widget annotations using JavaScript. Let’s dive in!

Quote

Streamline your Salesforce workflows with Apryse WebViewer. Enhance security and simplify development.

Apryse WebViewer Sample Form

Getting Started

Copied to clipboard

To get started with WebViewer and Salesforce:

  1. Clone our sample Lightning Web Component project on GitHub.
  2. Download WebViewer.

Setting up Salesforce DX

Copied to clipboard

We recommend using Salesforce DX when deploying this app to your scratch org or sandbox. For quick intro tutorials, those new to the tool can visit the Quick Start: Salesforce DX Trailhead Project or the App Development with Salesforce DX Trailhead modules.

Setup steps include:

  1. Enable Dev Hub in your project.
  2. Install the SFDX CLI.
  3. Install Visual Studio Code + Salesforce Extension.

Optimizing WebViewer Source Code for Salesforce

Copied to clipboard

Now you need to optimize Apryse WebViewer code for Salesforce. Extract the WebViewer.zip you downloaded earlier into a folder and run this npm script:

$ npm run optimize

You will encounter the following prompts. Answer y/n as indicated:

Optimize: Do you want us to backup your files before optimizing? [y/n]:  y
Optimize: Will you be using WebViewer Server? ... [y/n]:  n
Optimize: Will you be converting all your documents to XOD? ... [y/n]:  n
Optimize: Do you need client side office support? [y/n]:  y
Optimize: Do you need the full PDF API? ... [y/n]:  y
Optimize: Do you want to use the production version of PDFNet.js? ... [y/n]:  n
Optimize: Do you need to deploy to Salesforce? ... [y/n]:  y

Note: Make sure you answer this prompt with n:

Optimize: Will you be converting all your documents to XOD? ... [y/n]:  n

After answering y to “Do you need to deploy to Salesforce?”, the script produces .zip files no more than 5 MB in size, allowing you to upload them as static resources.

Installing the Sample LWC App Using Salesforce DX

Copied to clipboard

Cloning and Deploying the Repository

Next, you need to clone our sample salesforce-webviewer-prepopulate project. To configure the sample and get it running, follow these steps:

1. Clone the webviewer-salesforce GitHub repo:

git clone git@github.com:PDFTron/salesforce-webviewer-prepopulate.git
cd salesforce-webviewer-prepopulate

2. Copy all the zip files generated after running the npm optimizing script from the output folder webviewer-salesforce into the force-app/main/default/staticresources folder of your newly cloned project.

Screenshot of copying project folders

The files you will need to copy from the “webviewer-salesforce” directory.

3. You can add your WebViewer license key in staticresources/myfiles/config_apex.js file or add it in your WebViewer constructor by passing l: "LICENSE_KEY" option.

WebViewer({
  l: ‘LICENSE_KEY’
})

4. If you haven’t done so, authenticate your org and provide it an alias (DevHub in the command below) from your terminal (macOS) or cmd (Windows). Execute the following command as is.

sfdx force:auth:web:login --setalias [your-alias] [--instanceurl https://test.salesforce.com for sandboxes]

Alternatively, you can authenticate from VS Code:

Alternatively, you can authenticate from VS Code

5. Enter your org credentials in the browser that opens. Type dev hub in the quick find search and toggle to enable as shown in the picture below.

Enter your org credentials in the browser

6. Create a scratch org using the config/project-scratch-def.json file, set the username as your default, and assign it an alias by replacing my-scratch-org with your own alias name. Alternatively, you can also deploy to your sandbox and skip this step.

sfdx force:org:create --setdefaultusername -f config/project-scratch-def.json --setalias my-scratch-org

7. Push the app to your org:

sfdx force:source:push -f [-a your-alias if default not specified]

Or right click + Deploy Source to Org.

8. Open the org:

sfdx force:org:open [-a your-alias]

9. Navigate to the Object you would like to use for editing your record files. Click on the gear wheel and select ‘Edit Page’ to open the Lightning App Builder.

Navigate to the Object you would like to use for editing your record files. Click on the gear wheel and select ‘Edit Page’ to open the Lightning App Builder.

Now select where to launch the Lightning Web Component from and drag and drop the pdftronWebviewerContainer component there.

Select where to launch the Lightning Web Component from and drag and drop the pdftronWebviewerContainer component there

Uploading the Sample PDF

You can use the Sample PDF provided in the /staticresources/ folder of the repository for testing this sample. After adding your pdftronWebviewerContainer to a record page, upload the attach the sample PDF file to your records Notes & Attachments or Files.

Implementation Details for Developers

Copied to clipboard

Drawing and Filling Fields on the Document

Once your document opens in WebViewer inside Salesforce, you can then use JavaScript to draw form fields over top and populate these fields with data! You can hook into the documentLoaded event or execute code based on your custom events.

To pass Salesforce data to WebViewer, leverage our previously created ContentVersionWrapper and include any needed object data like so:

   //ContentVersionWrapper.cls - used as inner class
public class ContentVersionWrapper {
       @AuraEnabled
       public String name {get; set;}
       @AuraEnabled
       public String body {get; set;}
       @AuraEnabled
       public Account acc {get; set;}
      
       public ContentVersionWrapper(ContentVersion contentVer, Account acc) {
           this.name = contentVer.Title + '.' + contentVer.FileExtension;
           this.body = EncodingUtil.base64Encode(contentVer.VersionData);
           this.acc = acc;
       }
   }

Once you have your wrapper class set up and ready to go, query for ContentVersion data and your sObject data in the same method and pass them to your wrapper. In our sample, we grab Account record data from the current record like so:

// PDFTron_ContentVersionController.cls - snipped for brevity
@AuraEnabled(Cacheable=true)
   public static List<ContentVersionWrapper> getAttachments(String recordId){
       try {
           List<String> cdIdList = new List<String> ();
           List<ContentVersionWrapper> cvwList = new List<ContentVersionWrapper> ();
 
           // Query for account data to be included on PDF document
           Account acc = [ SELECT Name, ShippingAddress, SLAExpirationDate__c, Phone FROM Account WHERE Id = :recordId ];
 
           // Find links between record & document
           for(ContentDocumentLink cdl :
                   [   SELECT id, ContentDocumentId, ContentDocument.LatestPublishedVersionId
                       FROM ContentDocumentLink
                       WHERE LinkedEntityId = :recordId    ]) {
               cdIdList.add(cdl.ContentDocumentId);
           }
           // Use links to get attachments
           for(ContentVersion cv :
                   [   SELECT Id, Title,FileExtension, VersionData
                       FROM ContentVersion
                       WHERE ContentDocumentId IN :cdIdList
                       AND IsLatest = true ]) {
               if(fileFormats.contains(cv.FileExtension.toLowerCase())) {
                   // pass account data to ContentVersionWrapper
                   cvwList.add(new ContentVersionWrapper(cv, acc));
               }
           }
           return cvwList;
       } catch (Exception e) {
           throw new AuraHandledException(e.getMessage());
       }
   }

You can now modify the payload passed to WebViewer and include your account data:


// pdftronWvInstance.js - snipped for brevity
handleBlobSelected(record) {
   record = JSON.parse(record);
 
   const blob = new Blob([_base64ToArrayBuffer(record.body)], {
     type: mimeTypes[record.FileExtension]
   });
 
   const payload = {
     blob: blob,
     extension: record.cv.FileExtension,
     filename: record.cv.Title + "." + record.cv.FileExtension,
     documentId: record.cv.Id,
     account: record.acc // account data added to payload here
   };
   this.iframeWindow.postMessage({type: 'OPEN_DOCUMENT_BLOB', payload} , '*');
 }

Finally, in your config_apex.js file, you can now process the Account data. In our sample, we are using a for … of loop to iterate through Object.entries(account) to bulkify this sample for our Salesforce context. Next, we are using Annotations.WidgetFlags to determine which fields are required or multi-line. As we iterate through account record fields, we dynamically create WebViewer’s counterpart Annotations.Forms.Field for each form field we want included. In the same iteration, we use the WebViewer field to create a Annotations.WidgetAnnotation and dynamically place it on our form.

8.0JS:

// config_apex.js
function receiveMessage(event) {
 if (event.isTrusted && typeof event.data === 'object') {
   switch (event.data.type) {
     case 'OPEN_DOCUMENT_BLOB':
       const { blob, extension, filename, documentId, account } = event.data.payload;
       instance.loadDocument(blob, { extension, filename, documentId });
 
       const docViewer = instance.Core.documentViewer;
       const annotManager = docViewer.getAnnotationManager();
 
       // standard text flag
       const flags = new Annotations.WidgetFlags();
       flags.set('Multiline', false);
       flags.set('Required', false);
 
       // multiline address flag
       const addressFlags = new Annotations.WidgetFlags();
       flags.set('Multiline', true);
       flags.set('Required', false);
 
       let yVal = 142;
       let widgets = [];
       for (const [key, val] of Object.entries(account)) {
         let field = {};
         if (key.toLowerCase().includes('id')) {
           continue;
         } else if(key.toLowerCase().includes('address')) {
           field = new Annotations.Forms.Field(key, {
             type: 'Tx',
             value: val.street,
             addressFlags,
           });
         } else {
           field = new Annotations.Forms.Field(key, {
             type: 'Tx',
             value: val,
             flags,
           });
         }
 
         // create a widget annotation
         const widgetAnnot = new Annotations.TextWidgetAnnotation(field);
        
         // Add customization here
         // "Annotations" can be directly accessed since we're inside the iframe
         widgetAnnot.PageNumber = 1;
         widgetAnnot.X = 150;
         widgetAnnot.Y = yVal;
         widgetAnnot.Width = 400;
         widgetAnnot.Height = 30;
 
         //add the form field and widget annotation
         annotManager.addAnnotation(widgetAnnot);
         widgets.push(widgetAnnot);
         annotManager.getFieldManager().addField(field);
 
         yVal += 60;
       }
      
       annotManager.drawAnnotationsFromList(widgets);
       break;
     default:
       break;
   }
 }
}

6.0JS:

// config_apex.js
function receiveMessage(event) {
 if (event.isTrusted && typeof event.data === 'object') {
   switch (event.data.type) {
     case 'OPEN_DOCUMENT_BLOB':
       const { blob, extension, filename, documentId, account } = event.data.payload;
       event.target.readerControl.loadDocument(blob, { extension, filename, documentId });
 
       const docViewer = readerControl.docViewer;
       const annotManager = docViewer.getAnnotationManager();
 
       // standard text flag
       const flags = new Annotations.WidgetFlags();
       flags.set('Multiline', false);
       flags.set('Required', false);
 
       // multiline address flag
       const addressFlags = new Annotations.WidgetFlags();
       flags.set('Multiline', true);
       flags.set('Required', false);
 
       let yVal = 142;
       let widgets = [];
       for (const [key, val] of Object.entries(account)) {
         let field = {};
         if (key.toLowerCase().includes('id')) {
           continue;
         } else if(key.toLowerCase().includes('address')) {
           field = new Annotations.Forms.Field(key, {
             type: 'Tx',
             value: val.street,
             addressFlags,
           });
         } else {
           field = new Annotations.Forms.Field(key, {
             type: 'Tx',
             value: val,
             flags,
           });
         }
 
         // create a widget annotation
         const widgetAnnot = new Annotations.TextWidgetAnnotation(field);
        
         // Add customization here
         // "Annotations" can be directly accessed since we're inside the iframe
         widgetAnnot.PageNumber = 1;
         widgetAnnot.X = 150;
         widgetAnnot.Y = yVal;
         widgetAnnot.Width = 400;
         widgetAnnot.Height = 30;
 
         //add the form field and widget annotation
         annotManager.addAnnotation(widgetAnnot);
         widgets.push(widgetAnnot);
         annotManager.getFieldManager().addField(field);
 
         yVal += 60;
       }
      
       annotManager.drawAnnotationsFromList(widgets);
       break;
     default:
       break;
   }
 }
}

This approach works well for creating new fields and filling them. When updating existing fields with a value, you can also call annotManager.getFieldManager().addField(field); which either creates a new field or updates an existing field by name.

How to Communicate with the WebViewer iframe

To better understand how you can interact with the WebViewer iframe, you can read documentation about config.js files here.

Check out some of our previous guides to learn more about WebViewer use cases in Salesforce.

File Size Limitations

You can review this segment to learn more about file-size limitations.

Setting Worker Paths in Config.js

Optimizing the original WebViewer source code for the Salesforce platform means that we will also have to set a few paths in config.js. Get more details in our Salesforce as a Lightning Web Component blog.

Wrap up

Copied to clipboard

For more on what you can do with Salesforce and WebViewer, see our Salesforce documentation section or watch our previously recorded webinar covering integrating WebViewer into Salesforce. We’ll also be adding another article in our WebViewer and Salesforce series soon⁠—how to draw fields on a document via the UI. So stay tuned to our blog for more!

If you have any questions about this guide or our Salesforce-specific build of WebViewer, please feel free to get in touch, and we will be happy to help!

Sanity Image

Thomas Winter

Share this post

email
linkedIn
twitter