Hello there.
This is about a Lighting Component for uploading multiple Attachments for any Object.
It would work for all objects and can also be re usable in SF communities. To get an easy understanding i've used the component in Quick Action / lightning action. Thanks to Manoj for actual idea and support.
The Attachment would be inserted simultaneously when selected a file and also deleteing would deleted from database.
Component:
Controller JS:
Helper JS:
Styles:
Apex cls:
This is about a Lighting Component for uploading multiple Attachments for any Object.
It would work for all objects and can also be re usable in SF communities. To get an easy understanding i've used the component in Quick Action / lightning action. Thanks to Manoj for actual idea and support.
The Attachment would be inserted simultaneously when selected a file and also deleteing would deleted from database.
Component:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<aura:component implements="force:hasRecordId,force:lightningQuickActionWithoutHeader" controller="AttachmentUploadCon"> | |
<aura:attribute name="rows" type="List" default="[]" /> | |
<aura:attribute name="objAPIName" type="string" default="Account" access="GLOBAL"/> | |
<aura:attribute name="RecID" type="string" access="GLOBAL"/> | |
<aura:handler name="init" value="{!this}" action="{!c.initComp}"/> | |
<div style="width:100%;"> | |
<div style="float:left;width:70%;font-size: 20px;"><b>Upload Attachments</b></div> | |
<div style="float:left;width:30%;text-align:center;"><lightning:button iconPosition="left" iconName="action:new" variant="brand" onclick="{!c.addRow}" /></div> | |
<div style="clear:both;"></div> | |
</div> | |
<style> | |
.slds-modal__container{max-width: 55rem !important;width:75%;} | |
</style> | |
<div style="overflow-y:auto;height:440px;" > | |
<table class="slds-table slds-table_bordered slds-table_cell-buffer" style="margin: 2% auto;border: 1px solid #ddd;"> | |
<tbody> | |
<aura:iteration items="{!v.rows}" var="row"> | |
<tr> | |
<td scope="row" data-label="Row Number"> | |
{!row.rowNum}. | |
</td> | |
<td data-label="Attachment"> | |
<lightning:input class="{!row.rowNum - 1}" aura:id="fileId" onchange="{!c.handleFilesChange}" type="file" name="{!row.fileId}" label=" " multiple="false"/> | |
{!row.fileName} | |
<!--use aura:if for show-hide the loading spinner image--> | |
</td> | |
<td data-label="Action" class="slds-align_absolute-center action-col"> | |
<aura:if isTrue="{!row.showLoadingSpinner}"> | |
<div style="position: absolute;margin-left: -38%;" class="slds-text-body_small slds-text-color_error"> | |
<img src="/auraFW/resources/aura/images/spinner.gif" class="spinner-img" alt="Loading"/>' | |
</div> | |
</aura:if> | |
<lightning:button iconPosition="left" iconName="action:delete" variant="destructive" name="{!row.rowNum - 1}" class="{!'atcID_'+row.fileId}" onclick="{!c.deleteRow}" /> | |
</td> | |
</tr> | |
</aura:iteration> | |
</tbody> | |
</table> | |
</div> | |
</aura:component> |
Controller JS:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
({ | |
initComp: function(component, event, helper) { | |
helper.getAttachments(component, event); | |
}, | |
deleteRow: function(component, event, helper) { | |
var index = event.getSource().get('v.name'); | |
var clsName = event.getSource().get('v.class'); | |
console.log('recId@ delete', clsName.split('atcID_')); | |
var clasAray = !$A.util.isEmpty(clsName) ? clsName.split('atcID_') : []; | |
var recId = !$A.util.isEmpty(clasAray) ? clasAray[1] : ''; | |
if(!$A.util.isEmpty(recId)) | |
{ | |
var action = component.get("c.deleteAttachment"); | |
action.setParams({"attchId": recId}); | |
action.setCallback(this, function(response) { | |
var state = response.getState(); | |
console.log('state..!',state); | |
if (state === "SUCCESS"){ | |
helper.getAttachments(component, event); | |
} | |
else | |
{ | |
alert('Something went wrong.'); | |
} | |
}); | |
$A.enqueueAction(action); | |
} | |
}, | |
clearError : function(component, event, helper) | |
{ | |
var rows = component.get('v.rows'); | |
var rowIndex = event.getSource().get("v.labelClass"); | |
rows[rowIndex].cDocError = ''; | |
component.set('v.rows',rows); | |
}, | |
handleFilesChange: function(component, event, helper) { | |
//alert(); | |
if (event.getSource().get("v.files").length > 0) { | |
console.log('---@handleFilesChange---'); | |
helper.uploadHelper(component, event); | |
} | |
}, | |
addRow : function(component, event, helper) { | |
var rows = component.get("v.rows"); | |
var newRow = rows.length+1; | |
rows.push({'rowNum':rows.length+1,'fileId':'','cDocError':'','parentId':'','fileName':'No File Selected..','showLoadingSpinner':false}); | |
component.set("v.rows",rows); | |
} | |
}) |
Helper JS:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
({ | |
MAX_FILE_SIZE: 4500000, //Max file size 4.5 MB | |
CHUNK_SIZE: 750000, //Chunk Max size 750Kb | |
uploadHelper: function(component, event) { | |
// start/show the loading spinner | |
// get the selected files using aura:id [return array of files] | |
var rowIndex = event.getSource().get('v.class'); | |
var attachmentId = event.getSource().get('v.name'); | |
var rows = component.get('v.rows'); | |
rows[rowIndex].showLoadingSpinner = true; | |
component.set('v.rows',rows); | |
var fileInput; | |
if(typeof component.find("fileId")[rowIndex] == 'undefined') | |
fileInput = component.find("fileId").get("v.files"); | |
else | |
fileInput = component.find("fileId")[rowIndex].get("v.files"); | |
// get the first file using array index[0] | |
var file = fileInput[0]; | |
var self = this; | |
var parentId = rows[rowIndex].parentId; | |
// check the selected file size, if select file size greter then MAX_FILE_SIZE, | |
// then show a alert msg to user,hide the loading spinner and return from function | |
if (file.size > self.MAX_FILE_SIZE) { | |
console.log('size exceeded.'); | |
rows[rowIndex].showLoadingSpinner = false; | |
rows[rowIndex].fileName = 'Alert : File size cannot exceed ' + self.MAX_FILE_SIZE + ' bytes.\n' + ' Selected file size: ' + file.size; | |
component.set('v.rows',rows); | |
return; | |
} | |
// create a FileReader object | |
var objFileReader = new FileReader(); | |
// set onload function of FileReader object | |
objFileReader.onload = $A.getCallback(function() { | |
console.log('--@objFileReader.onload --'); | |
var fileContents = objFileReader.result; | |
var base64 = 'base64,'; | |
var dataStart = fileContents.indexOf(base64) + base64.length; | |
fileContents = fileContents.substring(dataStart); | |
// call the uploadProcess method | |
self.uploadProcess(component, file, fileContents,rowIndex,attachmentId,parentId); | |
}); | |
objFileReader.readAsDataURL(file); | |
}, | |
uploadProcess: function(component, file, fileContents,rowIndex,attachmentId,parentId) { | |
// set a default size or startpostiton as 0 | |
var startPosition = 0; | |
// calculate the end size or endPostion using Math.min() function which is return the min. value | |
var endPosition = Math.min(fileContents.length, startPosition + this.CHUNK_SIZE); | |
// start with the initial chunk, and set the attachId(last parameter)is null in begin | |
this.uploadInChunk(component, file, fileContents, startPosition, endPosition, '',rowIndex,attachmentId,parentId); | |
}, | |
uploadInChunk: function(component, file, fileContents, startPosition, endPosition, attachId,rowIndex,attachmentId,parentId) { | |
console.log('--@uploadInChunk---'); | |
// call the apex method 'saveChunk' | |
var getchunk = fileContents.substring(startPosition, endPosition); | |
var action = component.get("c.saveChunk"); | |
action.setParams({ | |
parnttId: component.get("v.recordId"), | |
attachmentName: file.name, | |
attachmentBody: encodeURIComponent(getchunk), | |
attachmentType: file.type, | |
fileId: attachId, | |
attachmentId:attachmentId | |
}); | |
// set call back | |
action.setCallback(this, function(response) { | |
console.log('--@uploadInChunk- setCallback--'); | |
// store the response / Attachment Id | |
attachId = response.getReturnValue(); | |
var state = response.getState(); | |
if (state === "SUCCESS") { | |
console.log('--@uploadInChunk- success!!'); | |
// update the start position with end postion | |
startPosition = endPosition; | |
endPosition = Math.min(fileContents.length, startPosition + this.CHUNK_SIZE); | |
// check if the start postion is still less then end postion | |
// then call again 'uploadInChunk' method , | |
// else, diaply alert msg and hide the loading spinner | |
if (startPosition < endPosition) { | |
this.uploadInChunk(component, file, fileContents, startPosition, endPosition, attachId,rowIndex); | |
} else { | |
var rows = component.get('v.rows'); | |
rows[rowIndex].showLoadingSpinner = false; | |
rows[rowIndex].fileName = file.name; | |
rows[rowIndex].fileId = attachId; | |
component.set('v.rows',rows); | |
//alert('your File is uploaded successfully'); | |
this.getAttachments(component, event); | |
} | |
// handel the response errors | |
} else if (state === "INCOMPLETE") { | |
alert("From server: " + response.getReturnValue()); | |
} else if (state === "ERROR") { | |
var errors = response.getError(); | |
if (errors) { | |
if (errors[0] && errors[0].message) { | |
console.log("Error message: " + errors[0].message); | |
} | |
} else { | |
console.log("Unknown error"); | |
} | |
} | |
}); | |
// enqueue the action | |
$A.enqueueAction(action); | |
}, | |
getAttachments : function(component, event) | |
{ | |
var action = component.get("c.loadAttachments"); | |
var recID = !$A.util.isEmpty(component.get("v.recordId")) ? component.get("v.recordId") : component.get("v.RecID"); | |
action.setParams({ | |
parentId: recID, | |
sobjAPIName: component.get("v.objAPIName"), | |
}); | |
action.setCallback(this, function(response) { | |
var state = response.getState(); | |
console.log('state..!',state); | |
if (state === "SUCCESS") | |
{ | |
var cAttachments = response.getReturnValue(); | |
console.log('attachments..!',cAttachments); | |
var rows = []; | |
for(var i = 0;i<cAttachments.length;i++) | |
{ | |
var row = {}; | |
row.rowNum = i+1; | |
row.fileId = cAttachments[i].attachmentId; | |
row.fileName = cAttachments[i].attachmentName; | |
row.parentId = cAttachments[i].parentId; | |
row.cDocError = ''; | |
row.showLoadingSpinner = false; | |
rows.push(row); | |
} | |
if(cAttachments.length == 0) | |
rows.push({'rowNum':1,fileId:'','cDocError':'','parentId':'','fileName':'No File Selected..','showLoadingSpinner':false}); | |
component.set("v.rows",rows); | |
} | |
else | |
{ | |
alert('Something went wrong.'); | |
} | |
}); | |
$A.enqueueAction(action); | |
} | |
}) |
Styles:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.THIS .action-col button{ | |
margin:21%; | |
cursor:pointer; | |
} | |
.THIS .error-msg{ | |
color:#a94442; | |
} | |
.THIS .Name-td input{ | |
border-left: 5px solid #a94442; | |
} |
Apex cls:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public with sharing class AttachmentUploadCon | |
{ | |
@AuraEnabled | |
public static list<attachmentWrapper> loadAttachments(Id parentId, string sobjAPIName) | |
{ | |
list<attachmentWrapper> awList = new list<attachmentWrapper>(); | |
string qryStr = 'Select Id,Name,(Select Id,Name FROM Attachments) FROM '+sobjAPIName+' where id=:parentId'; | |
list<sobject> records = DataBase.Query(qryStr); | |
for(Sobject sobj : records) | |
{ | |
for(Attachment att : sobj.getSObjects('Attachments')) | |
{ | |
attachmentWrapper aw = new attachmentWrapper(); | |
aw.parentId = (string)sobj.get('Id'); | |
aw.attachmentId = att.Id; | |
aw.attachmentName = att.Name; | |
awList.add(aw); | |
} | |
} | |
return awList; | |
} | |
public class attachmentWrapper | |
{ | |
@AuraEnabled public String parentId; | |
@AuraEnabled public String attachmentId; | |
@AuraEnabled public String attachmentName; | |
} | |
@AuraEnabled | |
public static void deleteAttachment(string attchId) | |
{ | |
system.debug('attchId '+attchId); | |
if(string.isNotBlank(attchId)){ | |
list<Attachment> atchList = [select id from Attachment WHERE ID=: attchId]; | |
system.debug('--atchList --'+atchList.size()); | |
if(atchList.size()>0){ | |
delete atchList; | |
} | |
} | |
} | |
@AuraEnabled | |
public static Id saveChunk(Id parnttId, String attachmentName, String attachmentBody, String attachmentType,String attachmentId) { | |
attachmentBody = EncodingUtil.urlDecode(attachmentBody, 'UTF-8'); | |
Attachment oAttachment; | |
if(String.isNotBlank(attachmentId)) | |
{ | |
oAttachment = [Select Id,parentId FROM Attachment WHERE Id=:attachmentId]; | |
} | |
else | |
{ | |
oAttachment = new Attachment(); | |
oAttachment.parentId = parnttId; | |
} | |
oAttachment.Body = EncodingUtil.base64Decode(attachmentBody); | |
oAttachment.Name = attachmentName; | |
oAttachment.ContentType = attachmentType; | |
upsert oAttachment; | |
return oAttachment.Id; | |
} | |
} |
By — anil
11:05