Tag Archives: Lightning

Dynamic Add Delete Row In Lightning Component

In below example I’m adding account record in Salesforce Lightning Component with Add Delete Rows functionality.

Apex Class:

public with sharing class AuraSampleController{
    
    @AuraEnabled
    public static void saveAccounts(List<Account> accList){
        Insert accList;
    }
}

Lightning Component:

<aura:component controller="AuraSampleController" Implements="flexipage:availableForRecordHome,force:hasRecordId">
    
    <aura:attribute name="accountList" type="Account[]"/> 
    
    <div class="slds-m-around--xx-large">
        <div class="slds-float_right slds-p-bottom_small">
            <h1 class="slds-page-header__title">Add Row 
                <lightning:buttonIcon iconName="utility:add"  size="large" variant="bare" alternativeText="Add" onclick="{!c.addRow}"/>
            </h1>
        </div>
        <div class="container-fluid">        
            <table class="slds-table slds-table_bordered slds-table_cell-buffer"> 
                <thead>
                    <tr class="slds-text-title_caps">
                        <th scope="col">
                            <div class="slds-truncate">#</div>
                        </th>
                        <th scope="col">
                            <div class="slds-truncate" title="Account Name">Account Name</div>
                        </th>
                        <th scope="col">
                            <div class="slds-truncate" title="Account Number">Account Number</div>
                        </th>
                        <th scope="col">
                            <div class="slds-truncate" title="Phone">Phone</div>
                        </th>
                        <th scope="col">
                            <div class="slds-truncate" title="Action">Action</div>
                        </th>
                    </tr>
                </thead>   
                <tbody>      
                    <aura:iteration items="{!v.accountList}" var="acc" indexVar="index">
                        <tr>
                            <td> 
                                {!index + 1}
                            </td>
                            <td>
                                <lightning:input name="accName" type="text" required="true" maxlength="50" label="Account Name" value="{!acc.Name}" />
                            </td>
                            <td>
                                <lightning:input name="accNumber" type="text"  maxlength="10" label="Account Number" value="{!acc.AccountNumber}" />
                            </td>
                            <td>
                                <lightning:input name="accPhone" type="phone" maxlength="10" label="Phone" value="{!acc.Phone}" />
                            </td>
                            <td>
                                <a onclick="{!c.removeRow}" data-record="{!index}">
                                    <lightning:icon iconName="utility:delete" size="small" alternativeText="Delete"/>
                                    <span class="slds-assistive-text">Delete</span>
                                </a>
                            </td> 
                        </tr>
                    </aura:iteration>
                </tbody>
            </table>
            <div class="slds-align_absolute-center slds-p-top_small">
                <lightning:button variant="brand" label="Submit" title="Brand action" onclick="{!c.save}" />
            </div>
        </div>
    </div>
</aura:component>

Lightning JS Controller:

({
    addRow: function(component, event, helper) {
        helper.addAccountRecord(component, event);
    },
    
    removeRow: function(component, event, helper) {
        //Get the account list
        var accountList = component.get("v.accountList");
        //Get the target object
        var selectedItem = event.currentTarget;
        //Get the selected item index
        var index = selectedItem.dataset.record;
        accountList.splice(index, 1);
        component.set("v.accountList", accountList);
    },
    
    save: function(component, event, helper) {
        if (helper.validateAccountList(component, event)) {
            helper.saveAccountList(component, event);
        }
    },
})

Lightning JS Helper:

({
    addAccountRecord: function(component, event) {
        //get the account List from component  
        var accountList = component.get("v.accountList");
        //Add New Account Record
        accountList.push({
            'sobjectType': 'Account',
            'Name': '',
            'AccountNumber': '',
            'Phone': ''
        });
        component.set("v.accountList", accountList);
    },
    
    validateAccountList: function(component, event) {
        //Validate all account records
        var isValid = true;
        var accountList = component.get("v.accountList");
        for (var i = 0; i < accountList.length; i++) {
            if (accountList[i].Name == '') {
                isValid = false;
                alert('Account Name cannot be blank on row number ' + (i + 1));
            }
        }
        return isValid;
    },
    
    saveAccountList: function(component, event, helper) {
        //Call Apex class and pass account list parameters
        var action = component.get("c.saveAccounts");
        action.setParams({
            "accList": component.get("v.accountList")
        });
        action.setCallback(this, function(response) {
            var state = response.getState();
            if (state === "SUCCESS") {
                component.set("v.accountList", []);
                alert('Account records saved successfully');
            }
        }); 
        $A.enqueueAction(action);
    },
})

Create Account Record :

Created Account Records :

Adding and Removing Styles on a Component During Runtime

You can add or remove a CSS style on a component or element during runtime. You can use the $A.util.addClass(cmpTarget, 'class') method to append CSS classes and $A.util.removeClass(cmpTarget, 'class') method to remove CSS classes from a component or element.

And you can use component.find('myCmp').get('v.class') method to retrieve the class name on a component, where myCmp is the aura:id attribute value.

Here is an example to adding and removing Styles on a Component during runtime.

Component:

<aura:component>    
    <div aura:id="hwDiv">Hello World!</div><br />
    <lightning:button onclick="{!c.addCSS}" label="Add Style" />
    <lightning:button onclick="{!c.removeCSS}" label="Remove Style" />
</aura:component>

JS Controller:

({
    addCSS: function(cmp, event) {
        var cmpDiv = cmp.find('hwDiv');
        $A.util.addClass(cmpDiv, 'changeStyle');
    },
    
    removeCSS: function(cmp, event) {
        var cmpDiv = cmp.find('hwDiv');
        $A.util.removeClass(cmpDiv, 'changeStyle');
    }  
})

CSS Stylesheet:

.THIS.changeStyle {
    background-color:blue;
    color:red;
}

Output:

Custom File Upload In Salesforce Lightning Component

File Upload Component:
Create a new Lightning Component FileUpload.cmp.

<!--FileUpload.cmp-->
<aura:component controller="FileUploadController">
    <!-- 'parentId' Aura Attribute for store the Id for Parent Record where we are attach our file -->  
    <aura:attribute name="parentId" type="Id" default="00128000002KuXUAA0" />
    <!-- 'fileName' attribute for display the selected file name -->  
    <aura:attribute name="fileName" type="String" default="No File Selected.." />
    
    <!-- Lightning Input with file type and on file change call the 'handleFilesChange' controller --> 
    <lightning:input aura:id="fuploader" onchange="{!c.handleFilesChange}" type="file" name="file" label="Upload File" multiple="false"/>
    <div class="slds-text-body_small slds-text-color_error">{!v.fileName} </div>
    <br/>
    <lightning:button label="Cancel" onclick="{!c.handleCancel}" class="slds-m-top--medium" />
    <lightning:button label="Save" onclick="{!c.handleSave}"
                      variant="brand" class="slds-m-top--medium"/>
</aura:component>


File Upload JavaScript Controller:

Now create below JavaScript FileUploadController.js controller for above FileUpload.cmp component.

({    
    handleSave: function(component, event, helper) {
        if (component.find("fuploader").get("v.files").length > 0) {
            helper.uploadHelper(component, event);
        } else {
            alert('Please Select a Valid File');
        }
    },
    
    handleFilesChange: function(component, event, helper) {
        var fileName = 'No File Selected..';
        if (event.getSource().get("v.files").length > 0) {
            fileName = event.getSource().get("v.files")[0]['name'];
        }
        component.set("v.fileName", fileName);
    },
    
    handleCancel: function(component, event, helper) {
        $A.get("e.force:closeQuickAction").fire();
    }
})

File Upload JavaScript Controller Helper:
Now create below JavaScript FileUploadHelper.js helper for above FileUploadController.js component.

({
    MAX_FILE_SIZE: 4500000, //Max file size 4.5 MB 
    CHUNK_SIZE: 750000,      //Chunk Max size 750Kb 
    
    uploadHelper: function(component, event) {
        // get the selected files using aura:id [return array of files]
        var fileInput = component.find("fuploader").get("v.files");
        // get the first file using array index[0]  
        var file = fileInput[0];
        var self = this;
        // 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) {
            component.set("v.fileName", 'Alert : File size cannot exceed ' + self.MAX_FILE_SIZE + ' bytes.\n' + ' Selected file size: ' + file.size);
            return;
        }
        
        // create a FileReader object 
        var objFileReader = new FileReader();
        // set onload function of FileReader object   
        objFileReader.onload = $A.getCallback(function() {
            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);
        });
        
        objFileReader.readAsDataURL(file);
    },
    
    uploadProcess: function(component, file, fileContents) {
        // 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, '');
    },
    
    
    uploadInChunk: function(component, file, fileContents, startPosition, endPosition, attachId) {
        // call the apex method 'SaveFile'
        var getchunk = fileContents.substring(startPosition, endPosition);
        var action = component.get("c.SaveFile");
        action.setParams({
            parentId: component.get("v.parentId"),
            fileName: file.name,
            base64Data: encodeURIComponent(getchunk),
            contentType: file.type,
            fileId: attachId
        });
        
        // set call back 
        action.setCallback(this, function(response) {
            // store the response / Attachment Id   
            attachId = response.getReturnValue();
            var state = response.getState();
            if (state === "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);
                } else {
                    alert('File has been uploaded successfully');
                }
                // 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);
    }
})

File Upload Apex Controller:
Create below apex controller to use it in FileUpload.cmp component.

public with sharing class FileUploadController {
    
    @AuraEnabled
    public static Id SaveFile(Id parentId, String fileName, String base64Data, String contentType) {
        base64Data = EncodingUtil.urlDecode(base64Data, 'UTF-8');
        Attachment attach = new Attachment();
        attach.parentId = parentId;
        attach.Body = EncodingUtil.base64Decode(base64Data);
        attach.Name = fileName;
        attach.ContentType = contentType;
        Insert attach;
        return attach.Id;
    }
}

File Upload App:

<!--FileUploadApp.app-->
<aura:application extends="force:slds">
    <c:FileUpload />    
</aura:application>

Output:

Salesforce Lightning Data Service

Salesforce introduced Lightning Data Service(LDS) in Winter 17, It is to serve as the data layer for Lightning. It is similar like Standard Controllers in Visualforce Page.

Advantages of Lightning Data Service:

  • Lightning Data Service allows to load, create, edit, or delete a record without requiring Apex code and SOQL query.
  • Lightning Data Service handles sharing rules and field-level security.
  • Records loaded in Lightning Data Service are cashed and shared across all components.
  • When one component updates a record, the other component using it are notified.
  • Lightning Data Service supports offline in Salesforce1.

Consideration of Lightning Data Service:

  • Lightning Data Service is only available in Lightning Experience and Salesforce1.
  • Lightning Data Service does not support bulk operations.
  • Lightning Data Service in other containers, such as Lightning Components for Visualforce, Lightning Out, or Communities isn’t supported.
  • Lightning Data Service notifies listeners about data changes only if the changed fields are the same as in the listener’s fields or layout.

We can use force:recordData Lightning component to declare Lightning Data Service. To use Lightning Data Service, we need specify the following attributes:

  • recordId specifies the record to load. Records can’t be loaded without a recordId.
  • mode can be set to either EDIT or VIEW, which determines the behavior of notifications and what operations are available to perform with the record. If you’re using force:recordData to change the record in any way, set the mode to EDIT.
  • layoutType specifies the layout (FULL or COMPACT) that is used to display the record.
  • fields specifies which fields in the record to query. The fields or layoutType attribute (or both) must be provided.
  • target* attributes starting with target indicates values to be returned from Data Service. It can return record or error.
  • recordUpdated edit mode does not updates record by default, to update target* attributes, use this method handler.
  • targetRecord will contain only the fields relevant to the requested layoutType and/or fields atributes.
  • targetFields is populated with the simplified view of the loaded record.
  • targetError is to the localized error message if the record can’t be provided.

Here is an example of Lightning Data Service: In below example I’ve created a Lightning Quick Action to edit Contact Email and Phone using Lightning Data Service force:recordData.

ContactEditDataService.cmp :

<!--ContactEditDataService.cmp-->
<aura:component implements="force:appHostable,force:lightningQuickActionWithoutHeader,flexipage:availableForRecordHome,force:hasRecordId" access="global" >
    <!--Declare Atrribute-->
    <aura:attribute name="contact" type="Object"/>
    <aura:attribute name="contactRecord" type="Object"/>
    <aura:attribute name="recordSaveError" type="String"/>
    
    <force:recordData aura:id="conRec" 
                      layoutType="FULL" 
                      recordId="{!v.recordId}"  
                      targetError="{!v.recordSaveError}"
                      targetRecord="{!v.contact}"
                      targetFields="{!v.contactRecord}"
                      mode="EDIT"
                      recordUpdated="{!c.handaleRecordUpdated}"/>
    
    <div class="slds-box"> 
        <div class="slds-text-heading_medium">
            {!v.contactRecord.Name}
        </div> 
        
        <aura:if isTrue="{!not(empty(v.recordSaveError))}">
            <br />
            <div class="error slds-box">
                {!v.recordSaveError}
            </div> 
        </aura:if>
        
        <div class="slds-form--stacked slds-tile"> 
            <div class="slds-form-element">
                <label class="slds-form-element__label">Phone: </label>
                <div class="slds-form-element__control">
                    <lightning:input type="tel" value="{!v.contactRecord.Phone}" name="phone" required="true"/>
                </div>
            </div>
            <div class="slds-form-element">
                <label class="slds-form-element__label">Email: </label>
                <div class="slds-form-element__control">
                    <lightning:input type="email" aura:id="email" value="{!v.contactRecord.Email}" required="true"/>
                </div>
            </div>
            <div class="slds-form-element">
                <lightning:button variant="brand" title="Save" label="Save" onclick="{!c.handleSaveContact}"/>
                <lightning:button title="Cancel" label="Cancel" onclick="{!c.handaleCancel}"/>
            </div>
        </div>
    </div>
</aura:component>

ContactEditDataService JS Controller :

({
    handleSaveContact : function(component, event, helper) {
        helper.saveContact(component, event, helper);
    },
    
    handaleRecordUpdated : function(component, event, helper) {
        helper.recordUpdated(component, event, helper);
    },
    
    handaleCancel : function(component, event, helper) {
        $A.get("e.force:closeQuickAction").fire();
    }
})

ContactEditDataService Helper :

({
    //Save contact record
    saveContact : function(component, event, helper) {
        component.find("conRec").saveRecord($A.getCallback(function(saveResult) {
            if (saveResult.state === "SUCCESS" || saveResult.state === "DRAFT") {
                //To close the component with success message
                var toastEvent = $A.get("e.force:showToast");
                toastEvent.setParams({
                    "title": "Success!",
                    "message": "The record has been updated successfully."
                });
                toastEvent.fire();
                $A.get("e.force:closeQuickAction").fire();
                $A.get('e.force:refreshView').fire();
            } else if (saveResult.state === "INCOMPLETE") {
                //Show data saved incomplete message
                component.set("v.recordSaveError","Data saved incomplete.");
            } else if (saveResult.state === "ERROR") {
                //Show error message
                var errMsg = "";
                for (var i = 0; i < saveResult.error.length; i++) {
                    errMsg += saveResult.error[i].message + "\n";
                }
                component.set("v.recordSaveError", errMsg);
            } 
        }));
    },
    
    //Refresh record after update
    recordUpdated : function(component, event, helper){
        var changeType = event.getParams().changeType;
        if (changeType === "CHANGED") {
            component.find("conRec").reloadRecord();
        }
    }
})

Output: