Building Generic Lightning Component (Custom Clone Functionality)


In this blog I am going to tell you about writing generic Lightning Component which helps to reduce your effort.
It is definitely going to take more effort to write generic component but just think about what will happen once you will finish your Lightning Component. It will save lot of your effort in future. So the question is why generic component takes more effort than specific Lightning Component. The answer is we need to think about every possible scenarios.
Let's take one simple requirement.
Requirement: Build a generic Clone functionality on Account Object which copies only specific field's data of a record instead of all fields.

Step 1: So the requirement is specific to Account Object so lets start building a Custom Lightning Component. To implement clone functionality first I will create a Lightning Component in which I will use force:createRecord method to create new record and in force:CreateRecord we have attribute which can auto-populate fields. So e will use that attribute which will populate specific fields on Account new Record.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!--CustomCloneComponent-->
<aura:component controller="CloneActionController" implements="force:lightningQuickAction,force:hasRecordId" >
    <!--ATTRIBUTE-->
    <aura:attribute name="loaded" type="Boolean" default="true"/>
    
    <!--EVENT-->
    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
    
    <!--BODY-->
    <aura:if isTrue="{!v.loaded}">
        <lightning:spinner alternativeText="Loading" />
    </aura:if>
</aura:component>

As you see above lightning component doesn't have much UI part. That is because we don't want anything as UI but I have added controller and force:hasRecordId interface which i will use later. Let's create Controller Part of this Lightning Component:

1
2
3
4
5
({
 doInit : function(component, event, helper) {
  helper.createAccountRecordHelper(component, event, helper);
 }
})

So, we are not doing much in Controller Part also. So lets go to Helper of this Lightning Component:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
({
 createAccountRecordHelper : function(component, event, helper) {
  let action = component.get('c.getCurrentRecordDetails');
        action.setParams({
            objectRecordId : component.get('v.recordId') 
        });
        action.setCallback(this, function(response){
            var finalResponse = response.getReturnValue();
            var createRecordEvent = $A.get('e.force:createRecord');
            createRecordEvent.setParams({
                'entityApiName' : 'Account',
                'defaultFieldValues' : {
                    'Name' : finalResponse.Name,
                    'AccountNumber' : finalResponse.AccountNumber,
                    'Description' : finalResponse.Description,
                    'Site' : finalResponse.Site
                }
            })
            createRecordEvent.fire();
        });
        $A.enqueueAction(action);
 }
})

As you know that we are currently trying to build custom Clone functionality so we will use this Lightning Component as a Quick Action on any particular Account record. So when User will click on Clone Quick Action then we want to open another Account creation page which has pre-populated values based on the record open. So with the help of force:hasRecordId we will get the record Id and pass it to Apex Class and Apex Class will return the default values to lightning component and that is why there is Server Side call in Helper method.
Now lets see the Apex Class Logic:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/*
* Class Name: CloneActionController
* @Author: FlightToSalesforce
* @Date: 15th March 2020
* @Description: Handler class for Lightning Component of Clone functionality.
*/
public class CloneActionController{
    @auraEnabled
    public static Account getCurrentRecordDetails(String objectRecordId){
        return [select Id, Name, AccountNumber, Description, Site from Account where Id = :objectRecordId];
    }
}

Now we need to create theQuick Action and add it to Account Page Layout. Which I am sure everyone can do.

So now look into this component and think if we get another requirement that user want to copy another field then again we need to update Apex Class and Lightning Component both.
Or if user want same custom Clone functionality on any other standard/custom object then again we need to write another Lightning Component.
So let's build a lightning Component which will work for any Custom Object. But before doing that what other things we need to consider:

1. Generic Clone Functionality should work with minimum Limitations.
2. It should work for all custom/standard Objects.
3. Component can handle Record Types also.
4. Object & Field Level Access should be consider.
5. In future if any new field need to be cloned then no need of enhancement should be required.

To implement a generic Clone Functionality first I need to create a Custom Metadata Object which will hold the list of Field for all objects in Salesforce which needs to be copied:

So this Metadata will help in future to customize Clone Functionality based on User need. Now lets see what changes required in Apex Class


 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
/*
* Class Name: CloneActionController
* @Author: FlightToSalesforce
* @Date: 15th March 2020
* @Description: Handler class for Lightning Component of Clone functionality.
*/
public class CloneActionController{
    @auraEnabled
    public static DataWrapper fetchDefaultValues(String objectRecordId){
        DataWrapper valuesWrapper = new DataWrapper();
        try{
            String query;
            List<String> objAccessFields = new List<String>();
            Id recordId = Id.valueOf(objectRecordId);
            String objectName = String.valueOf(recordId.getSObjectType());
            Schema.DescribeSObjectResult schemaResult = Schema.getGlobalDescribe().get(objectName).getDescribe();
            if(schemaResult.isAccessible()){
                Map<String, Schema.SObjectField> fieldMap = schemaResult.fields.getMap();
                Map<String,Schema.RecordTypeInfo> recordTypeInfoList = schemaResult.getRecordTypeInfosByName();
                List<Clone_Field_Mapping__mdt> defaultsobjectFieldList = [Select Id, Field_API_Name__c from Clone_Field_Mapping__mdt where Object_API_Name__c = :objectName LIMIT 1000];
                if(defaultsobjectFieldList.size() > 0){
                    if(recordTypeInfoList.size() > 0){
                        query = 'Select Id, RecordTypeId';
                    }else{
                        query = 'Select Id';
                    }
                    for(Clone_Field_Mapping__mdt field : defaultsobjectFieldList){
                        query+= ', '+ field.Field_API_Name__c;
                    }
                    system.debug('query'+query);
                    query+= ' from '+ objectName +' where Id =:objectRecordId LIMIT 1';
                    sObject exisitingRecord = Database.query(query);
                    if(recordTypeInfoList.size() > 0){
                        valuesWrapper.recordTypeId = exisitingRecord.get('RecordTypeId');
                    }
                    Map<Object,Object> fieldToValueMap = new Map<Object, Object>();
                    for(Clone_Field_Mapping__mdt field : defaultsobjectFieldList){
                        Boolean hasEditAccess = fieldMap.get(field.Field_API_Name__c).getDescribe().isCreateable();
                        if(hasEditAccess){
                            fieldToValueMap.put(field.Field_API_Name__c, exisitingRecord.get(field.Field_API_Name__c));
                        }
                    }
                    Object defaultValueJSON = JSON.serialize(fieldToValueMap);
                    valuesWrapper.fieldDefaultValues = defaultValueJSON;
                    valuesWrapper.sObjectName = objectName;
                    return valuesWrapper;
                }else{
                    valuesWrapper.isError = true;
                    valuesWrapper.errorMessage = 'No field has been added for cloning record. Please contact your system administrator.';
                    return valuesWrapper;
                }
            }else{
                valuesWrapper.isError = true;
                valuesWrapper.errorMessage = 'You do not have sufficient access to create new record. Please contact your system administrator.';
                return valuesWrapper;
            }
        }catch(Exception ex){
            valuesWrapper.isError = true;
            valuesWrapper.errorMessage = 'Please contact your system administrator.';
            return valuesWrapper;
        }
    }
    public class DataWrapper{
        @auraEnabled public String sObjectName {get; set;}
        @auraEnabled public Object fieldDefaultValues {get; set;}
        @auraEnabled public Object recordTypeId {get; set;}
        @auraEnabled public boolean isError {get; set;}
        @auraEnabled public String errorMessage {get; set;}
        
        public DataWrapper(){
            isError = false;
        }
    }
}

So if you check above class, it is a generic class which can handle any object of Salesforce for Custom Clone Functionality. Now let's take a look into Lightning Component helper method logic:


 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
({
    createAccountRecordHelper : function(component, event, helper) {
        let action = component.get('c.fetchDefaultValues');
        action.setParams({
            objectRecordId : component.get('v.recordId')
        });
        action.setCallback(this,function(response){
            component.set("v.loaded", false);
            var finalResponse = response.getReturnValue();
            var cloneRecorTypeId = finalResponse.recordTypeId;
            var defaultValuesString = JSON.parse(finalResponse.fieldDefaultValues);
            var objectName = finalResponse.sObjectName;
            var createRecordEvent = $A.get("e.force:createRecord");
            if(!finalResponse.isError){
                if(cloneRecorTypeId !== ''){
                    createRecordEvent.setParams({
                        'entityApiName': objectName,
                        'recordTypeId' : cloneRecorTypeId,
                        'defaultFieldValues': defaultValuesString
                    });
                }else{
                    createRecordEvent.setParams({
                        'entityApiName': objectName,
                        'defaultFieldValues': defaultValuesString
                    });
                }
            }else{
                var toastEvent = $A.get("e.force:showToast");
                toastEvent.setParams({
                    "title": "Error!",
                    "type": "error",
                    "mode": "pester",
                    "message": finalResponse.errorMessage
                });
                toastEvent.fire();
            }
            
            createRecordEvent.fire();
        });
        $A.enqueueAction(action);
    }
})

Now if you see the helper method above, instead of hard coding of field pre-population logic we have created a dynamic lightning component.

Advantage: Now the advantage of this generic Lightning component is that you can manage fields clone functionality from Custom Metadata in Production directly and if user want clone functionality on any different object then we just need new Quick Action and link this lightning component to that Quick Action.

Comments

Post a Comment

Followers

Popular posts from this blog

Salesforce LWC: Multi-Record Creation using Lightning Web Component

Salesforce LWC: Basic Drag and Drop Functionality

Salesforce Lightning Web Component: Dynamic Column Selection in Lightning Datatable