Pages

Saturday, May 29, 2021

Configurable Lightning Modal Popup with Field Set

 

Motivation behind this


Recently I got a requirement to build a configurable modal popup and it is to be developed using Lightning Web Components. Also, I got motivated watching Aditya Nag's (Salesforce) presentation on Design Patterns and Best Practices to build reusable Lightning Web Components following good design to develop this component.

The component is to be configurable so that even Admin can configure this. So, I have planned to leverage Field Set to make it. Earlier I have written a blog Build Configurable Dynamic Table from Field Set using Lightning Web Component.

Here design and event handling have been made in a different way. Let's get started.


Use Case


Business has a requirement to quickly create a record on a button click where fields will be displayed inside a modal form.

As, this requirement is quite common so developer wants to make it a reusable way using LWC. For testing purposes, create field set on Case Object.

Possible End Results


The component will look like this below:


 

Solution Approach


To develop this functionality, I have followed template-based designing, separation-of-concerns practices, and event propagations in a proper way.

Framed the following design:
  • There will be modal component modalComponentTemplate which will act as a template.
  • There will be a component createRecordFromFieldset which will develop screen elements using lightning-record-edit-form with a backend Apex calls to fetch field details from Field Set.
  • Spinner Component lwcSpinner for displaying spinner during load (here only HTML has been leveraged).
  • Container component lwcCreateRecordModalContainer will reuse this template and inject createRecordFromFieldset and Spinner into the unnamed slot and Submit & Cancel buttons to the footer slot. This component will expose to capture inputs from design attributes which is needed for Admin to set it up. This is the main component which drives the entire functionality.

Component composition is as follows:



Flow Diagram


The components will talk to each other as per the following diagram. This diagram depicts how components are loaded, events are propagated from parent-to-child and child-to-parent respecting each one's responsibilities.


Let's go through each components.

modalComponentTemplate


This template modal component code has been referred from lwc-recipes and customized for my need.

modalComponentTemplate.html

In this file, we can see there are sections for header, footer, and one unnamed slot that can be used for building body. There can be only one unnamed slot (<slot></slot>) in entire html.

 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
<!--
  @component   : modalComponentTemplate
  @description : This acts as a modal template, code has referred from lwcRecipe and slds modal
  @author      : Santanu Boral  
-->
<template>
    <template if:true={showModal}> 
        <section role="dialog" tabindex="-1" 
            aria-labelledby="modal-heading-01" 
            aria-modal="true" aria-describedby="modal-content-id-1" 
            class="slds-modal slds-fade-in-open"
        >
            <div class="slds-modal__container">
                <header class="slds-modal__header slds-theme_success">
                    <lightning-button-icon
                        class="slds-modal__close"
                        title="Close"
                        icon-name="utility:close"
                        icon-class="slds-button_icon-inverse"
                        onclick={handleDialogClose}
                    ></lightning-button-icon>
                    <h2 id="modal-heading-01" class="slds-modal__title slds-hyphenate" >
                        {header}
                    </h2>  
                </header>
                
                <!--unnamed slot where the implementer can inject their component-->
                <div class="slds-modal__content slds-p-around_medium" id="modal-content-id-1">
                    <slot></slot>
                </div>
                
                <footer class="slds-modal__footer">
                    <slot
                        name="footer"
                        onslotchange={handleSlotFooterChange}
                    ></slot>
                </footer>
            </div>
        </section>
        <div class="slds-backdrop slds-backdrop_open"></div>
    </template>
</template>


modalComponentTemplate.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
28
29
30
31
32
33
34
35
36
37
import { LightningElement,api } from 'lwc';

export default class ModelComponentTemplate extends LightningElement {
    showModal = false;
    @api
    set header(value) {
        this._headerPrivate = value;
    }
    get header() {
        return this._headerPrivate;
    }
    
    _headerPrivate;
    
    @api show() {
        this.showModal = true;
    }

    @api hide() {
        this.showModal = false;
    }
    handleDialogClose() {
        //Let parent know that dialog is closed (mainly by that cross button)
        // so it can set proper variables if needed
        const closedialog = new CustomEvent('closedialog');
        this.dispatchEvent(closedialog);
        this.hide();
    }
    
    handleSlotFooterChange() {
        // Only needed in "show" state. If hiding, we're removing from DOM anyway
        if (this.showModal === false) {
            return;
        }
        const footerEl = this.template.querySelector('footer').hide();        
    }
}

Let's go through Create Record Form component.

createRecordFromFieldset.html


In this file lightning-record-edit-form has been used, there will be no buttons, just the fields will be displayed from field set. Form submission will be handled from parent i.e. Container component. Sounds interesting!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!--
  @component   : createRecordFromFieldset
  @description : This component displays fields from fieldset. 
                 There is no submit button in this component, form is submitted programmatically.
  @author      : Santanu Boral  
-->
<template>
  <lightning-record-edit-form 
        object-api-name={sfdcObjectApiName} 
        onsuccess={handleSuccess}>
    <lightning-messages></lightning-messages>
    <template for:each={inputFieldAPIs} for:item="item">  
        <lightning-input-field field-name={item} name={item} key={item} id={item}>          
        </lightning-input-field>
</template> </lightning-record-edit-form>
</template>

createRecordFromFieldset.js


Few notable points in this js file:
  • Container will pass the values of sfdcObjectApiName and fieldSetName as attributes.
  • implicit call to Apex class to fetch field details in connectedCallback()
  • In the renderedCallback() cmploaded event has been raised, so that spinner can be closed by Container. Check Container js code an interesting way to hide spinner.
  • handleSubmit public method is called from Container to submit the lightning-record-edit-form

this.template.querySelector('lightning-record-edit-form').submit();
  • It will automatically fire onsuccess event and handleSuccess() handler will propagate postsuccess event to Container so the modal can be closed.

 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
76
77
78
/*
 *   Author: Santanu Boral
*/
import { LightningElement,api, track } from 'lwc';
import getFieldsFromFieldSet from '@salesforce/apex/FieldSetHelper.getFieldsFromFieldSet';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

export default class CreateRecordFromFieldset extends LightningElement {
    lblobjectName; //this displays the Object Name whose records are getting displayed
    inputFieldAPIs = [];
    renderCall = false;

    //get its value from container component through attributes
    @api sfdcObjectApiName; //Case
    @api fieldSetName; //QuickCaseFS

    //load the record edit form with fields from fieldset.
    connectedCallback(){
        let objectApiName = this.sfdcObjectApiName;
        let fieldSetName = this.fieldSetName;

        //make an implicit call to fetch fields from database
        getFieldsFromFieldSet({ strObjectApiName: objectApiName,
            strfieldSetName: fieldSetName}
        )
        .then(data=>{        
            let items = []; //local array to hold the field api

            //get the entire map
            let objStr = JSON.parse(data);   
            //get the list of fields, its a reverse order to extract from map
            let listOfFields = JSON.parse(Object.values(objStr)[1]);
            //get the object name
            this.lblobjectName = Object.values(objStr)[0];
            //prepare items array using field api names            
            listOfFields.map(element=>items.push(element.fieldPath));      
                  
            this.inputFieldAPIs = items;                              
            this.error = undefined;   
        })
        .catch(error =>{
            this.error = error;
            console.log('error',error);                
            this.lblobjectName = objectApiName;
        })
    }
    
    renderedCallback(){
        if(!this.renderCall){
            this.renderCall = true;
            this.dispatchEvent(
                new CustomEvent('cmploaded')
            );            
        }
    }

    //This method submits the record edit form and getting called from container component
    @api
    handleSubmit(){        
        this.template.querySelector('lightning-record-edit-form').submit();        
    }

    //This event handler fires on post submit and displays success and propagate event to container
    handleSuccess(event) {
        const evt = new ShowToastEvent({
            title:  this.lblobjectName + " created",
            message: "Record ID: " + event.detail.id,
            variant: "success"
        });
        this.dispatchEvent(evt);

        //raise event to parent to handle this and to close the popup, specify bubbles=true
        this.dispatchEvent(
                new CustomEvent('postsuccess'),
                { bubbles: true }
        );        
    }
}

Let's check the apex class which fetches field details from field set.

FieldSetHelper.cls


Few notable points:
  • Using reflection, SObject instance has been created which must faster than Schema.getGlobalDescibe().

(SObject)(Type.forName('Schema.'+ strObjectApiName)?.newInstance());
         
    • Use of Safe Navigation Operator to avoid Null Point Exceptions
    • Object label has been returned which will support language translation.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /**
     * @author : Santanu Boral 
    **/
    public with sharing class FieldSetHelper {
        
        @AuraEnabled (cacheable=true)
        public static String getFieldsFromFieldSet(String strObjectApiName, String strfieldSetName){
            if(!String.isEmpty(strObjectApiName) && !String.isEmpty(strfieldSetName)){   
                Map<String, String> returnMap = new Map<String, String>();
    
                //get fields from FieldSet
                SObject sObj = (SObject)(Type.forName('Schema.'+ strObjectApiName)?.newInstance());
                List<Schema.FieldSetMember> lstFSMember = 
                    sObj?.getSObjectType()?.getDescribe()?.fieldSets.getMap().get(strfieldSetName)?.getFields();
                    
                returnMap.put('FIELD_LIST',JSON.serialize(lstFSMember));
                returnMap.put('OBJECT_LABEL', sObj?.getSObjectType()?.getDescribe()?.getLabel());
    
                return JSON.serialize(returnMap);
            }                                                                 
            return null;
        }
    }
    
     
    There is small Spinner component has been created just to use HTML.

    lwcSpinner


    HTML as below

    <template>    
        <div class="slds-spinner_container">
            <div class="slds-spinner_brand  slds-spinner slds-spinner_large slds-is-relative" role="alert">
                <span class="slds-assistive-text">Loading...</span>
                <div class="slds-spinner__dot-a"></div>
                <div class="slds-spinner__dot-b"></div><br>
                <br>            
            </div>
        </div>    
    </template>
    

    css as below

    We will see how we have made it hidden using css classList in Container component.

    .slds-spinner_container {     
        content: 'Please wait.....!';
        text-align: center;
        z-index: 10000;
        position: fixed;
        background-color: rgba(0, 0, 0, 0.002);
    }
    
    .spinner-hidden {
        display: none;
    }
    

    Let's move on final Container component.

    lwcCreateRecordModalContainer


    This component holds showModal button for opening modal, injects child components to the slots and submits the form.


    lwcCreateRecordModalContainer.html


     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
    <!--
      @component   : lwcCreateRecordModalContainer
      @description : This acts as pop-up container which re-uses popup template 
                     and injects fieldset component and buttons into the slots.
      @author      : Santanu Boral 
    -->
    <template>
        <lightning-card title="Create quick case" icon-name="custom:custom19">
            <!--show modal button on open modal-->
            <lightning-button
                    label="Show modal"
                    onclick={handleShowModal}
            ></lightning-button>        
            
            <c-modal-component-template 
                header={header} 
                onclosedialog={handleCancelModal}            
                >
                <!--display create record component inside unnamed slot-->
                <template if:true={showSpinner}>                
                    <c-lwc-spinner></c-lwc-spinner>            
                </template>
                <c-create-record-from-fieldset 
                        sfdc-object-api-name={SFDCobjectApiName} 
                        field-set-name={fieldSetName}   
                        onpostsuccess={handlePostSuccess}
                        oncmploaded={handleCreateRecordCmpLoaded}             
                ></c-create-record-from-fieldset>
    <!-- inject footer submit & cancel buttons into the slot--> <div slot="footer"> <lightning-button label="Cancel" variant="neutral" onclick={handleCancelModal} ></lightning-button>&nbsp;
    <lightning-button label="Submit" type="submit" variant="brand" onclick={handleSubmitModal} ></lightning-button>
    </div> </c-modal-component-template>
    </lightning-card>
    </template>

    lwcCreateRecordModalContainer.js


    Few notable points:
    • It passed attribute values to createRecord component which is getting captured through design attributes like: sfdc-object-api-name={SFDCobjectApiName}
    • Spinner loaded when this component is loaded.
    • After createRecord component loads all the fields then based on oncmploaded event handler, spinner is made hidden. The below code adds display: none into the css class to hide this. No other if-else logic written.

    this.template.querySelector('c-lwc-spinner').classList.add(CSS_CLASS);
      • After createRecord component saves the records, it captures onpostsuccess event and hides the modal.


       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
      /*
       *   Author: Santanu Boral
      */
      import { LightningElement,api } from 'lwc';
      
      const CSS_CLASS = 'spinner-hidden';
      export default class LwcCreateRecordModalContainer extends LightningElement {    
      
          //following values are passed from design attributes
          @api header; //Create Case 
          @api SFDCobjectApiName; //Case
          @api fieldSetName; //'QuickCaseFS'
          showSpinner;
      
          //this calls for modal to display
          handleShowModal() {    
              this.showSpinner = true;
              this.template.querySelector('c-modal-component-template').show();                
          }
      
          //this makes modal is hidden
          handleCancelModal() {
              this.template.querySelector('c-modal-component-template').hide();        
          }
      
          //This event handler calls the handleSubmit public method of fieldset component
          handleSubmitModal(event){
              this.template.querySelector('c-create-record-from-fieldset').handleSubmit();            
          }
      
          //This postSuccess event handler closes the modal
          handlePostSuccess(event){
              event.stopPropagation();
              this.handleCancelModal();
          }
      
          //This makes spinner to be hidden
          handleCreateRecordCmpLoaded(event){
              setTimeout(()=>{
                  this.template.querySelector('c-lwc-spinner').classList.add(CSS_CLASS);
                  this.showSpinner = false;
              }, 1200);        
          }
      }
      

      Finally, we are done with preparing the components.

      Let's Test this!


      First create a field set QuickCaseFS with Subject, Status, Origin and Description.




      Create a page from AppBuilder and place lwcCreateRecordModalContainer component and pass the values.


      Finally, open the page and click on Show Modal button and component will open.

      I have also created a validation rule on Case object to make Subject as Mandatory which works wells during submit as follows.





      Issue fixing


      We might face an issue if we have a dropdown list and when we click on dropdown, there is a right side scrollbar and user will face hard time to scroll and choose the item. This issue can be fixed as below.


      Atlast, we are done and thanks for your patience to read this!

      References


      Further Reading




      Sunday, May 16, 2021

      Building Lightning File Uploader with files as Thumbnail

       

      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:


      File uploader component will work as demonstrated in the following video:




      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.



      Lets see the individual components.

      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:
      • displaying toast message on successful file upload.
      • propagating refreshlist event to parent component

      attachmentRelatedList component


      HTML looks like 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
      <!--
        @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.

      References


      Further Reading