Thursday, July 27, 2017

Lightning: Generate PDF from Lightning components with in-memory data

I'm sure as everyone is diving into lightning components development, they are getting acquainted with the nuances of the Lightning components framework. As well as, its current limitations. Being a new framework, this is bound to happen. Although we have our users still using salesforce classic, we have started using lightning components framework our primary development platform and Visualforce is considered primarily for rendering lightning components within Classic Service console.

Recently, while re-architecting a critical module, we encountered a problem wherein we needed to generate PDF from lightning components. Now, being Javascript intensive framework, it has limited room for such features (may be included in future roadmap). As of now, there is no native feature within the lightning framework to do so (at least I didn't find anything).

Common Scenario - Create Visualforce page to retrieve data and generate PDF

For scenarios where the data exist within Salesforce, it may be a comparatively easier task, by developing a visualforce page, which can accept arguments in querystring and generate PDF by retrieving data via Apex controller. Further, Lightning component can generate desired URL to retrieve the desired PDF and open that URL in a new window.

Complex Scenario - Generate PDF from Lightning components with in-memory data

However, in our scenario, all the data is being retrieved from an external application at run-time. This data is never saved within Salesforce and hence we have to make use of in-memory data. However, we do have this feature available within Visualforce. As we are still using Salesforce classic and embedding our lightning components on visualforce page, we have an advantage in this case.

So, here is a quick list of challenges and how I solved them in this solution (not saying that there could not be any better solutions)

Challenge Solution
Generate PDF Generate PDF via Visualforce
Send Data to PDF page Use hidden fields or method arguments to pass data from Lightning components to Visualforce page controller

Process flow

Process flow - Generate PDF from Lightning components with in-memory data


Steps

So, I utilized Visualforce PDF generation feature triggered from Lightning component. The main steps in this task are:-
  1. Create Lightning component to accept an external method and invoke it on button click
  2. Embed Lightning component within Visualforce Page
  3. Passing in-memory data within lightning component to VF Page Controller
  4. Generating PDF

Create Lightning component to accept an external method and invoke it on button click

In this step, we will create a lightning component which accepts a javascript method as an attribute and on button click (of button within lightning component), invokes visualforce page's javascript method

We can create an attribute within Lightning component to accept a function. For demo purposes, I've added a button, which will invoke PDF generation

1
2
3
4
5
6
7
8
<aura:component access="global">

    <!-- attribute to accept Visualforce page's javascript method -->
    <aura:attribute name="sendData" type="object"/>

    <!-- Button component to invoke PDF download -->
    <lightning:button label="Download Document" onclick="{!c.downloadDocument}" />
</aura:component>


We can create lightning component's controller to invoke VF Page javscript method on button click

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/* Component controller */
({
 downloadDocument : function(component, event, helper){

  var sendDataProc = component.get("v.sendData");
  var dataToSend = {
   "label" : "This is test"
  }; //this is data you want to send for PDF generation

  //invoke vf page js method
  sendDataProc(dataToSend, function(){
              //handle callback
  });
 }
})

Embed Lightning component within Visualforce Page

Here, we will embed the newly created lightning component to our VF page MainPage. We can pass a javascript function (defined within v the sualforce page) to lightning component upon initialization.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function saveData(data, callback){
    //TO DO
}

$Lightning.use("DemoApp", function() {
    $Lightning.createComponent("PDFGenertorExample",
    { 
        sendData : saveData
    },
    "lightning",
    function(cmp) {
        // do some stuff
    });
});

Passing in-memory data within lightning component to VF Page Controller

The way I have achieved it is by using a hidden control to store the data and bind it with controller property.


1
2
3
4
/* Controller code */
public class VFPageController {
    public String PDFData {get; set;}    
}

Second, embed an apex:inputhidden component to bind this property to page
1
2
<!-- Page code -->
<apex:inputhidden id="hidData" value="{!PDFData}"/>


Thirdly, bind the data received from component to page's control

1
2
3
4
5
6
<!-- Page JS code -->
function saveData(data, callback){
    
    var hidData = document.getElementById('{!$Component.hidData}')
    hidData.value = JSON.stringify(data);
}


Generate PDF

So far, we have successfully brought lightning component's in-memory data to Visualforce page control, where the page control is further bound to Apex controller's property.

Now, the last part is to actually invoke the PDF generation logic. Now, we need to create a visualforce page to generate the actual PDF and invoke this page.

So, for the sake of simplicity, we create a visualforce page PDFPage as


1
2
3
4
<!-- PDFPage -->
<apex:page controller="VFPageController" renderAs="pdf">
    {!PDFData}
</apex:page>


Now, we need to create an apex controller method in VFPageController class to open the PDF page (to trigger generation and download of PDF document).



1
2
3
4
5
6
7
8
9
<!-- generate and download pdf document -->
public PageReference downloadPDF(){
    System.PageReference pageRef = new System.PageReference('/apex/PDFPage');

    //ensure pdf downloads and is assigned with defined name
    pageRef.getHeaders().put('content-disposition', 'attachment; filename=TestPDF.pdf');
 
    return pageRef;
}


Lastly, we need to add an actionfunction to MainPage, to invoke controller method; and invoke this actionfunction method on receiving invocation from Lightning component

So, our actionfunction code will be:


<apex:actionfunction name="jsGeneratePDF" action="{!downloadPDF}" />
and, our updated saveData method will be:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function saveData(data, callback){
    //bind lightning component data to page component
    var hidData = document.getElementById('{!$Component.hidData}')
    hidData.value = JSON.stringify(data);

    //invoke PDF Generation
    jsGeneratePDF();
 
    //invoke callback;
    if(typeof callback == 'function') callback();
}


Popular Posts