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.
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.
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.
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.
ReplyDeleteWonderful! I have been looking for something like this for a long time. Thanks!
ReplyDeleteLooking forward to many more articles
ReplyDeleteHi, mi PDF look like this.
ReplyDelete"htmlattribute"Journey toSalesforce"htmlattribute"
Could you help me? nice explanation, by the way.
This comment has been removed by the author.
ReplyDeleteThis comment has been removed by the author.
ReplyDeleteWe need to add below two attributes to the meta file html page.
ReplyDeleteisExposed = True
target = lightning__AppPage
We need to add them in html format.
Excellent article
ReplyDeleteGreat article. So helpful for me like delelopers
ReplyDeleteReally helpful article, Thank you.
ReplyDelete