Today, we are going to take a deep dive into File Upload component.

upload-file-component

If you are building Lightning Component, you can simply use this code below to include a File Upload component:

<lightning:fileUpload  name="fileUploader"
                       label= "File Upload"
                       multiple="{!v.multiple}"
                       accept="{!v.accept}"
                       disabled="{!v.disabled}"
                       recordId="{!v.recordId}"
                       onuploadfinished="{! c.handleUploadFinished }"/>

If you are using Flow Builder, it is even easier for you to simply add the File Upload component into your flow like this:

upload-file-component-flow

Make sure you have set the related record ID so that the uploaded files will be able to show in related files.

But... is it really satisfying our basic needs?

Don't you think the component is missing something very important, for example, you might want to view the files that are uploaded, or delete the uploaded files?

I was hoping that Salesforce would make improvement on that, but I don't know whether this request will be even considered or not, so I figured why don't I implement a custom file upload component then?

Aren't you interested to know how to create your own custom advanced file upload component like this where you can actually see the uploaded files and delete them as needed?

advanced-file-upload-component

Custom advanced file upload component in flow.

Without further ado, let's get into the code together!


First, let's create a controller for this Lightning Component. Let's call it LightningFileUploadHandler.apxc:

// LightningFileUploadHandler.apxc

public class LightningFileUploadHandler {
    
    @AuraEnabled  
    public static List<ContentDocument> getFiles(String recordId){ 
        Set<Id> recordIds = new Set<Id>{recordId};
        List<ContentDocumentLink> cdlList = [SELECT Id, LinkedEntityId, ContentDocumentId FROM 
                                             ContentDocumentLink WHERE LinkedEntityId IN :recordIds
                                            ];                         
        Set<Id> documentIds = new Set<Id>(); 
        for(ContentDocumentLink cdl:cdlList){  
            documentIds.add(cdl.ContentDocumentId);  
        }    
        return [SELECT Id, Title, FileType FROM ContentDocument WHERE Id IN :documentIds];        
    } 
    
    @AuraEnabled  
    public static void deleteFile(String contentDocumentId){ 
        delete [SELECT Id from ContentDocument WHERE Id = :contentDocumentId];       
    }  
}

getFiles() is used to retrieve the uploaded files associated with the current record and deleteFile() is used to delete the ContentDocument file.

Next, we need to create our main Lightning Component LightningFileUploadComponent.cmp:

<!-- LightningFileUploadComponent.cmp -->
<aura:component controller="LightningFileUploadHandler" implements="lightning:availableForFlowScreens,force:appHostable,force:hasRecordId" access="global" >  
    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>  
    <aura:attribute name="files" type="ContentDocument[]"/>  
    <aura:attribute name="recordId" type="String" />  
    <aura:attribute name="acceptFileTypes" type="String" />  
    <aura:attribute name="showFileHeader" type="Boolean" default="true" />  
    <aura:attribute name="fileHeaderText" type="String" default="Files" /> 
    <aura:attribute name="uploadMultiple" type="Boolean" default="true"/>      
    <aura:attribute name="showSpinner" type="boolean" default="false"/>
    
    <div class="slds-card slds-card_boundary">  
        
        <!-- show or hide header -->
        <aura:if isTrue="{!v.showFileHeader}">
            <div class="slds-page-header">{!v.fileHeaderText}</div>  
        </aura:if>
        
        <div class="slds-align_absolute-center">  
                <lightning:fileUpload multiple="{!v.uploadMultiple}"   
                                      accept="{!v.acceptFileTypes}" 
                                      recordId="{!v.recordId}"   
                                      onuploadfinished="{!c.uploadFinished}" />  
        </div>  
        <br/> 
        <div class="slds-form--compound">
            <table class="slds-table slds-table--bordered">  
                <!-- hide the table header if file is empty -->
                <aura:if isTrue="{!not(empty(v.files))}">
                    <thead>  
                        <tr>  
                            <th>Title</th>  
                            <th>File Type</th>  
                            <th></th>                     
                        </tr>  
                    </thead>  
                </aura:if>
                <tbody>
                    <aura:iteration items="{!v.files}" var="f">  
                        <tr>  
                            <td>
                                <a href="javascript:void(0)" id="{!f.Id}" onclick="{!c.previewFile}">
                                    {!f.Title}
                                </a>
                            </td>  
                            <td>{!f.FileType}</td>      
                            <td>
                                <a href="javascript:void(0)" id="{!f.Id}" onclick="{!c.deleteSelectedFile}">
                                    <lightning:icon size="x-small" iconName="utility:delete" alternativeText="Delete" />
                                </a>
                            </td>
                        </tr>  
                    </aura:iteration>  
                </tbody>  
            </table>  
            <!-- show spinner logo when needed -->
            <aura:if isTrue="{!v.showSpinner}">
                <div class="slds-spinner_container">
                    <div class="slds-spinner slds-spinner--medium" aria-hidden="false" role="alert">
                        <div class="slds-spinner__dot-a"></div>
                        <div class="slds-spinner__dot-b"></div>
                    </div>
                </div>
            </aura:if>
        </div> 
    </div>  
</aura:component>

Since we are going to use the component in our flow, the component needs to implement lightning:availableForFlowScreens. I didn't include other interfaces such as flexipage:availableForAllPageTypes or flexipage:availableForRecordHome into this component because I have included the specific attribute types in the design resource which are not supported by those interfaces. If you do so, you will see an error message like this:

file-integrity-exception

NOTE |If you really want to include those interfaces, you might as well remove the files attribute from the LightningFileUploadComponent.design, not a big deal. You can always retrieve the files again using Get Records action in Flow Builder.

Next, we are going to look at LightningFileUploadComponentController.js:

// LightningFileUploadComponentController.js
({
    doInit : function(component, event, helper){  
       helper.getUploadedFiles(component, event);
    },      
    
    previewFile : function(component, event, helper){  
        $A.get('e.lightning:openFiles').fire({ 
            recordIds: [event.currentTarget.id]
        });  
    },  
    
    uploadFinished : function(component, event, helper) {  
        helper.getUploadedFiles(component, event);    
        var toastEvent = $A.get("e.force:showToast");
        // show toast on file uploaded successfully 
        toastEvent.setParams({
            "message": "Files have been uploaded successfully!",
            "type": "success",
            "duration" : 2000
        });
        toastEvent.fire();
    }, 
    
    deleteSelectedFile : function(component, event, helper){
        if( confirm("Confirm deleting this file?")){
            component.set("v.showSpinner", true); 
            helper.deleteUploadedFile(component, event);                
        }
    }
 })

We define the functions as below:

  • doInit() : called automatically when component is initiated
  • previewFile() : click to display the file screen in Salesforce
  • uploadFinished() : callback function when the files are successfully uploaded, display a toast afterwards
  • deleteSelectedFile() : show a confirmation dialog box to confirm file deletion

And our LightningFileUploadComponentHelper.js:

// LightningFileUploadComponentHelper.js
({  
    getUploadedFiles : function(component, event){
        var action = component.get("c.getFiles");  
        action.setParams({  
            "recordId": component.get("v.recordId") 
        });      
        action.setCallback(this,function(response){  
            var state = response.getState();  
            if(state=='SUCCESS'){  
                var result = response.getReturnValue();           
                component.set("v.files",result);  
            }  
        });  
        $A.enqueueAction(action);  
    },
    
    deleteUploadedFile : function(component, event) {  
        var action = component.get("c.deleteFile");           
        action.setParams({
            "contentDocumentId": event.currentTarget.id            
        });  
        action.setCallback(this,function(response){  
            var state = response.getState();  
            if(state=='SUCCESS'){  
                this.getUploadedFiles(component);
                component.set("v.showSpinner", false); 
                // show toast on file deleted successfully
                var toastEvent = $A.get("e.force:showToast");
                toastEvent.setParams({
                    "message": "File has been deleted successfully!",
                    "type": "success",
                    "duration" : 2000
                });
                toastEvent.fire();
            }  
        });  
        $A.enqueueAction(action);  
    },  
 })

Every time getUploadedFiles() is called, we retrieve the files from Apex to make sure the data is up-to-date. deleteUploadedFile() will be called when the user clicks on the delete icon, then display a toast on file successfully deleted.

A little css styling to make the file upload button larger in LightningFileUploadComponent.css:

// LightningFileUploadComponent.css
.THIS .slds-file-selector__body{
	padding: 1em;    
}

Lastly, we need to define the LightningFileUploadComponent.design so that we can pass in input variables to the component and receive outputs in the next flow screen.

<!-- LightningFileUploadComponent.design -->
<design:component>
    <design:attribute name="recordId" label="Record Id" />
    <design:attribute name="showFileHeader" label="Show File Header?" />
    <design:attribute name="fileHeaderText" label="File Header Text" />
    <design:attribute name="uploadMultiple" label="Upload Multiple?" />
    <design:attribute name="acceptFileTypes" label="Accept File Types (.pdf, .jpg and etc.)" />
    <design:attribute name="files" label="Files" />
</design:component>

You can decide what needs to pass into the component.


That's all! If you have followed along, you will be able to see something like this:

  1. Select files to upload.
    file-upload-part-1

  2. Click Done to continue.
    file-upload-part-2

  3. View the uploaded files instantly. You can click the on title link to view the file or the delete icon to delete the file.
    file-upload-part-3

New Updates

For those who are seeking for test class to get the code coverage, here's one for your reference:

// LightningFileUploadHandlerTest.apxc

@IsTest
public class LightningFileUploadHandlerTest {
    
    @IsTest
    public static void LightningFileUploadHandlerTestMethod() {
        
        Account acc = new Account(
        	BillingStreet = '123 ST',
            BillingState = 'NY',
            BillingCountry = 'US',
            BillingPostalCode = '00000',
            BillingCity = 'City',
            Name = 'Test'
        );
        
        insert acc;
        
        ContentVersion cv = new ContentVersion(
            Title = 'Test',
            PathOnClient = 'test.jpg',
            VersionData = Blob.valueOf('Test Content'),
            IsMajorVersion = true
        );
        
        insert cv;    
        
        ContentDocument cd = [SELECT Id, Title, LatestPublishedVersionId FROM ContentDocument LIMIT 1];
        
        //create ContentDocumentLink record 
        ContentDocumentLink cdl = New ContentDocumentLink(
        	LinkedEntityId = acc.id,
            ContentDocumentId = cd.Id,
            ShareType = 'V'
        );

        insert cdl;
        
        LightningFileUploadHandler.getFiles(acc.Id);
        LightningFileUploadHandler.deleteFiles(cd.Id);
    }
}

Hope you guys enjoy reading this post! The code is referenced and inspired by the post from this website.

Post was published on , last updated on .

Like the content? Support the author by paypal.me!