Sunday, October 11, 2020

Generate PDF from Salesforce Lightning Web Component

 

Motivation behind this


I have been looking for this option to generate PDF file from Lightning Web Component (LWC) quite often. Salesforce didn't provide any support to render page or component as pdf (like Visualforce) in LWC. So, tried a find an option to do so.

Without using external JavaScript library, I have tried to achieve here. This concept can be leveraged for any use case for pdf generation.

Use Case


Business has requirement to send user input data or data fetched from database to be saved as pdf format.

Developer wants to build with LWC.

Possible End Result


After building the use case, it will perform the functionality as following video:



Solution Approach


The main challenges with this use case:

  • Till today, Salesforce doesn't provide any JS library to display page as pdf
  • If we try to use Visualforce with renderAs="pdf" with embedding LWC into it then it will not work, because this doesn't support any JavaScript to be included.
  • There are many third party JS library can be used but maintaining that is a challenge.

Approach has been taken following way:
  • Create a LWC component adding lightning-input-rich-text field. This field has value attribute which returns the HTML content (this is main trick)
  • Create a Visualforce page with renderAs="pdf" attribute and use those HTML text as value of apex:outputText with escape="false". Here, visualforce has been used only for pdf generation, nothing else. So it will be very slim. 
  • When we click on "Save As PDF" button it will implicitly call the apex class' method and use the PageReference of the visualforce and save this body content as pdf.
It's simple.

displayRichTextComponent:

displayRichTextComponent.html will prepare a screen like this:



Code as follows:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<template>
    <lightning-card>    
        <lightning-input-rich-text
            placeholder="Type something interesting"
            formats={allowedFormats}
            value={myVal}>
        </lightning-input-rich-text>
        <lightning-button label="Save as PDF"
                        onclick={saveAsPdf}>
        </lightning-button>
        <lightning-button label="Do Something"
                        onclick={handleClick}>
        </lightning-button>
    </lightning-card>
</template>

Few notable points on the above HTML:

  • lightning-input-rich-text supports those format of font, size, image etc. Refer Documentation
  • value attribute of  lightning-input-rich-text shows initial data
  • Save as PDF button click event calls saveAsPdf method.
  • Clicking  on "Do Something" button, replaces any selected text with "Journey to Salesforce" with defined format with setRangeText() method, which is still in beta (Winter 21 release)
displayRichTextComponent.js

Entire code as below



 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import { LightningElement } from 'lwc';
import generatePDF from '@salesforce/apex/DisplayRichTextHelper.generatePDF';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

export default class DisplayRichTextComponent extends LightningElement {
    allowedFormats =  ['font', 'size', 'bold', 'italic', 'underline', 'strike',
    'list', 'indent', 'align', 'link', 'image', 'clean', 'table', 'header', 'color',
    'background','code','code-block'];

    //this method will display initial text
    get myVal() {
        return '**Generate PDF using LWC Component**';
    }

    attachment; //this will hold attachment reference

    /*This method extracts the html from input rich text 
        and pass this to apex class' method via implcit call
    */
    saveAsPdf(){
        const editor = this.template.querySelector('lightning-input-rich-text');
        
        //implicit calling apex method
        generatePDF({txtValue: editor.value})
        .then((result)=>{
            this.attachment = result;
                console.log('attachment id=' + this.attachment.Id);
                //show success message
                this.dispatchEvent(
                    new ShowToastEvent({
                        title: 'Success',
                        message: 'PDF generated successfully with Id:' + this.attachment.Id,
                        variant: 'success',
                    }),
                );
        })
        .catch(error=>{
            //show error message
            this.dispatchEvent(
                new ShowToastEvent({
                    title: 'Error creating Attachment record',
                    message: error.body.message,
                    variant: 'error',
                }),
            );
        })
    }
    
    /*
        This method updates the selected text with defined format
    */
    handleClick() {
        const editor  = this.template.querySelector('lightning-input-rich-text');
        const textToInsert = 'Journey to Salesforce'
        editor.setRangeText(textToInsert, undefined, undefined, 'select')
        editor.setFormat({bold: true, size:24, color: 'green', align: 'center',});
    }
}


Now, let's talk about visualforce page

renderAsPdf.page


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<apex:page controller="DisplayPDFController" renderAs="pdf"  
		   applyHtmlTag="false" showHeader="false" cache="true" readOnly="true" >
    <html>
        <head>
            <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
            <style>
                @page {
                    size: a4 portrait;    
                    padding-left: 2px;    
                    padding-right: 2px;
                }            
            </style>
        </head>
        <apex:outputText value = "{!displayText}" escape = "false"/>
    </html>
</apex:page>

You can see apex:outputText is used to display content, be sure to escape. I have added a style to display it as portrait with some padding option.

Now, see Visualforce Controller

DisplayPDFController.cls


1
2
3
4
5
6
7
8
public with sharing class DisplayPDFController {

    public String displayText {get; set;}
    public DisplayPDFController() {
        displayText = String.escapeSingleQuotes(
            ApexPages.currentPage().getParameters().get('displayText'));
    }
}

In the constructor, values are assigned to displayText. It can be done in page action method.

Finally, the Apex Class which is getting called from js file which actually creates the pdf file.

DisplayRichTextHelper.cls

Here, based on PageReference we are getting the page content which is being converted to pdf using getContentAspdf() method.

When we initially try to save attachment, we could face this below error if (cacheable=true) is used with @AuraEnabled

Too many DML statements: 1 out of 0 Error

which means that component is readonly and it doesn't allow to perform DML operation.

That's why cacheable=true is omitted. 

The file is getting attached to a contact record. For sake of brevity, error handling has been omitted and hardcoded Contact Id has been used.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public with sharing class DisplayRichTextHelper {
    
    @AuraEnabled
    public static Attachment generatePDF(String txtValue){
        
        Pagereference pg = Page.renderAsPDF;
        pg.getParameters().put('displayText', txtValue);

        Contact con = new Contact(Id='0032v00002ypAntAAE');
        Attachment objAttachment = new Attachment();
        objAttachment.Name = 'J2S.pdf';
        objAttachment.ParentId = con.Id;
        objAttachment.Body = pg.getContentaspdf();   
        objAttachment.IsPrivate = false;
        insert objAttachment;
        return objAttachment;
    }

}

Final pdf


The output is showing based on the element added into the rich text field.



meta files should include where this component will be available.

Create a Lightning App Builder page with one region and place this component and run the application, it will display the screen as above.

This concept can be leveraged easily at any project. For example, capturing fields from screen, then display results with some formats into the rich text box and finally generating pdf.

We are done and thanks for reading.


References



Further Reading


10 comments:

  1. Great article Shantanu. I agree its a shame salesforce does not provide any native support around pdf. While visualforce lets you render as pdf, that's where the buck stops. I've had requirements where we have had to split pdf documents, merge pdf documents (prior to emailing/faxing), and we resorted to external services to do that for us. While in LWC, we have the option to use third-party js libraries, but that would be additional maintenance overhead.

    ReplyDelete
  2. Wonderful! I have been looking for something like this for a long time. Thanks!

    ReplyDelete
  3. Looking forward to many more articles

    ReplyDelete
  4. Hi, mi PDF look like this.
    "htmlattribute"Journey toSalesforce"htmlattribute"
    Could you help me? nice explanation, by the way.

    ReplyDelete
  5. This comment has been removed by the author.

    ReplyDelete
  6. This comment has been removed by the author.

    ReplyDelete
  7. We need to add below two attributes to the meta file html page.

    isExposed = True
    target = lightning__AppPage

    We need to add them in html format.

    ReplyDelete
  8. Great article. So helpful for me like delelopers

    ReplyDelete
  9. Really helpful article, Thank you.

    ReplyDelete