Motivation behind this
I thought of building file uploader component with files related list displayed as thumbnail and also trying to implement event handling framework where child-to-parent and parent-to-child can easily be understandable by Lightning Web Component developer.
I have planned to leverage lightning-file-upload base component instead of lightning-input type="file" which drives me to build this poc where as thumbnail display is icing sugar on a cake.
Let's get started.
Use Case
Business has a requirement to upload multiple files (with huge file size) and wants to see uploaded files as thumbnail with relevant file information.
Developer wants to leverage Lightning base component bypassing pain on writing code for uploading files.
Possible End Results
The component will look like this below:
Initial R&D which has thrown away
To start with a poc, I had created one component which contains both file uploader and attachment related list, as because my initial goal was to display attachments as thumbnail. The files display part had been achieved as shown in picture.
Problem lies on using uploadfinished custom event handler of lightning-file-upload component. The handleUploadFinished event handler method doesn't allow to call any methods in the same class so that I can refresh the related list with newly uploaded files. As, the event doesn't support bubbling, cancelling and composing.
So, I have started coming up with following solution approach.
Solution Approach
Framed the following design:
- There will be a parent container component attachmentComponentThumbnail which will contain 2 children attachmentUploadComponent and attachmentRelatedList components and child components can also be used individually if necessary.
- Parent component will not just behave as a container rather it will also act as business delegate. For example, when attachment-upload component finishes its part of uploading files, it won't directly call relatedList component for refreshing the file list.
Rather, parent container component will take care whom to call for refreshing the list. Because parent knows the responsibilities for its children better. That's the beauty.
Flow Diagram on Event Propagations
Following flow diagram will help to understand event propagation.
attachmentUploadComponent
This component will look like this as shown in the flow diagram.
HTML will like below.
<template> <div class="slds-p-around_medium"> <lightning-file-upload label="Attach your file(s) here..." name="fileUploader" record-id={recordId} onuploadfinished={handleUploadFinished} multiple> </lightning-file-upload> </div> </template>
attachmentUploadComponent.js
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 | import { LightningElement,api } from 'lwc'; import {ShowToastEvent} from 'lightning/platformShowToastEvent'; export default class AttachmentUploadComponent extends LightningElement { @api recordId; //This method fires after files got uploaded handleUploadFinished(event) { const linkedinEntityId = this.recordId; const uploadedFiles = event.detail.files; //show success toast message const evt = new ShowToastEvent({ title: 'File Upload Status...', message: uploadedFiles.length + 'file(s) uploaded successfully.', variant: 'success', }); this.dispatchEvent(evt); /*dispatch this event to the parent, so that parent will take care to delegate this to attachmentRelatedList component for refreshing the list with currently loaded files. */ const evtCustomEvent = new CustomEvent('refreshlist', { detail: {linkedEntityId} }); this.dispatchEvent(evtCustomEvent); } } |
Few notable points in this js file:
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 | <!-- @name : attachmentRelatedList.html @description : This component acts as Related List Component which can be used alone or inside any other component (attachmentComponentThumbnail) @author : Santanu Boral --> <template> <lightning-card title={relatedListTitle}> <div class="slds-grid slds-wrap slds-p-around_medium"> <template if:true={results}> <template for:each={results} for:item="file"> <div class= "slds-col slds-size_1-of-2 slds-p-around_medium" key={file.fileId} > <div style="width:14rem" key={file.fileId}> <div class="slds-file slds-file_card slds-has-title"> <figure> <a href={file.viewUrl} class="slds-file__crop" target="_blank"> <img src={file.thumbnailPath} alt={file.fileName} /> </a> <figcaption class="slds-file__title slds-file__title_card"> <div class="slds-media__body"> <p class="slds-truncate"> <lightning-formatted-url value={file.downloadUrl} tooltip={file.filePath} label={file.fileName} target="_blank"> </lightning-formatted-url> </p> <p> {file.fileDate} . {file.fileSize} . {file.fileExtn} </p> </div> </figcaption> </figure> </div> </div> </div> </template> </template> </div> </lightning-card> </template> |
I have used Lightning Design System for Files to develop this component
js file
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | import { LightningElement, api, track } from 'lwc'; import retrieveFiles from '@salesforce/apex/AttachmentController.retrieveFiles'; export default class AttachmentRelatedList extends LightningElement { @api recordId; //recordId of the record page where it is being placed @track results = []; //array to prepare the thumbnail type file to be shown relatedListTitle; //used for preparing title e.g. Attachments (3) //This method is called from parent component after receiving event on files got uploaded. @api refreshList(linkedEntityId) { this.fetchFileData(linkedEntityId); } //if the component is used alone then it will help to retrieve file details connectedCallback(){ this.fetchFileData(this.recordId); } //It fetches the files and related information fetchFileData(parentRecordId){ retrieveFiles({recordId: parentRecordId}) .then(data=> this.prepareFileRows(data)) .catch(err => { console.log(err) }); } /* This method is used to prepare file data to be displayed as thumbnail with other information like size, format, date of loading, viewURL, downloadURL */ prepareFileRows(data){ if(data){ //prepare title this.relatedListTitle = 'Attachments (' + data.length + ')'; this.results = []; //initialize the array //extract file data and prepare results array with converted information data.map(element=>{ let fSize = this.formatFileSize(element.ContentSize); let fDate = this.formatDateString((element.CreatedDate).slice(0, 10)); this.results = [...this.results, { fileId: element.Id, fileName:element.Title, filePath: element.PathOnClient, fileType:element.FileType, fileExtn: element.FileExtension, fileSize: fSize, fileDate: fDate, thumbnailPath: '/sfc/servlet.shepherd/version/renditionDownload?rendition=thumb120by90&versionId=' + element.Id + '&operationContext=CHATTER&contentId=' + element.ContentDocumentId, downloadUrl: '/sfc/servlet.shepherd/document/download/' + element.ContentDocumentId, viewUrl: '/lightning/r/ContentDocument/' + element.ContentDocumentId + '/view' } ] }); } } //This method is used to prepare date as May 16, 2021 which usally comes as YYYY-MM-DD format formatDateString(dateStr){ const dt = new Date(dateStr); const year = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(dt); const month = new Intl.DateTimeFormat('en', { month: 'short' }).format(dt); const day = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(dt); return month + ' ' + day + ', ' + year; } //This method prepare file size to be displayed in MB or KB formatFileSize(fileSize){ let f = Math.abs(fileSize/1024); return (f > 1024? Math.abs(fileSize/(1024 * 1024)).toFixed(2) + ' MB' : Math.round(f) + ' KB'); } } |
Few notable points in this js file:
- preparing thumbnail URL which is in this format where versionId is ContentVersionId and contentId is ContentDocumentId
/sfc/servlet.shepherd/version/renditionDownload?rendition=thumb120by90&versionId=0682v00000JltJK&operationContext=CHATTER&contentId=05T2v00001lGQxa
- preparing download URL as follows
'/sfc/servlet.shepherd/document/download/' + element.ContentDocumentId
- Conversion of date format to be displayed as May 21, 2021 and fize size to be displayed in MB and KB
- When we click on preview it will open new window to display file details
- Clicking on fileURL, it will download the file.
- As this component can be used independently, so connectedCallback is used to call retrieveFiles method of Apex controller implicitly.
- There is no async-await has been used as ESLint will complain rather used promise.
- refreshList method is public so that it can called from outside, here Parent component will call this method.
- Error handling is omitted for sake of brevity.
AttachmentController.cls
This controller has method method retrieveFiles based on LinkedEntityId i.e. recordId of the record where this component is placed.
Notice that, usually we make method as (cacheable=true), here it is made as false.
If we can't do this then after file upload, if we try to refresh lists then it will give previous files which has been fetched earlier.
This was a bottleneck during refreshing the list.
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 | /** * name : AttachmentController * @description : This class handles for attachment related information collecting * @author : Santanu Boral **/ public with sharing class AttachmentController { //cacheable is omitted otherwise we will not get refreshed data upon uploading @AuraEnabled public static List<ContentVersion> retrieveFiles(String recordId){ //get records from ContentDocumentLink based on LinkedEntityId List<ContentDocumentLink> cdlList = [SELECT ContentDocumentId FROM ContentDocumentLink WHERE LinkedEntityId =: recordId]; List<Id> cdIds = new List<Id>(); //ContentDocumentIds for(ContentDocumentLink obj: cdlList){ cdIds.add(obj.ContentDocumentId); } //get records from ContentVersion based on ContentDocumentIds List<ContentVersion> cvList = [SELECT Id, ContentDocumentId, ContentUrl, VersionNumber, Title, PathOnClient, FileType, FileExtension, ContentSize, CreatedDate FROM ContentVersion WHERE ContentDocumentId IN:cdIds]; return cvList; } } |
Lets move on to Parent Component which holds above two.
attachmentComponentThumbnail
HTML looks like below:
onrefreshlist event handler captures the event raised by attachmentUpload component. And record-id is used to pass recordId to the children.
<template> <lightning-card title="File Uploader"> <c-attachment-upload-component onrefreshlist={delegateRefreshingAttachmentList} record-id={recordId}> </c-attachment-upload-component> <c-attachment-related-list record-id={recordId}></c-attachment-related-list> </lightning-card> </template>
js file
When it captures the event, it extracts the arguments and finally search for attachmentRelatedList component and calls its refreshList public method. That's why this component is used as BusinessDelegate. Not just a container.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import { LightningElement,api} from 'lwc'; export default class AttachmentComponentThumbnail extends LightningElement { @api recordId; /* This method is handler for refreshlist event which is raised by attachment-upload-component. It will search for attachment-related-list in the DOM and call refreshList method passing linkedEntityId as argument */ delegateRefreshingAttachmentList(event) { let args = JSON.parse(JSON.stringify(event.detail)); let linkedEntityId = args.linkedinEntityId; this.template.querySelector('c-attachment-related-list').refreshList(linkedEntityId); } } |
Create meta-files where this component to be exposed. You might have to take care additional things for Community implementations.
Coding is done, place this component to the record detail page and use it.
I have tested with uploading 215 MB single file and it worked.
Finally, thanks for your patience to read this.
Excellent
ReplyDeleteHi Santanu,
ReplyDeleteThanks for sharing.
Please let me know if this will form community as well when site guest users uploading the files.
Thanks for Sharing this Information. Salesforce Training in Gurgaon
ReplyDeleteTo be honest, I don't need this feature but I want to say a big thanks for the documentation provided.
ReplyDeleteI'll keep it in bookmark as a POC of documentation and I'll share with no hesitation as it can really help people understand how to build lwc , how to handle limitation and so on.
So thanks a lot for sharing this solution (quite sure it will help many people) and I'm also sure your document will help a lot newbie lwc developper :)