Salesforce Generic sObject Lightning Lookup Component

In Salesforce Lightning we don’t have any lookup field component like Salesforce classic. So, here is a generic lookup Lightning Component, which can be used for any sObject lookup present in our org without changing any major code.

sObjectLookup.cmp:
Create below Lightning Component, which will be used for sObject lookup.

<!--sObjectLookup.cmp-->
<aura:component description="Lookup. Lightning component for lookup fields. Can be used standalone or with other lightning component" controller="sObjectLookupController">
    
    <aura:attribute name="objectAPIName" type="String" required="true" description="Object API name used for searching records"/>
    
    <aura:attribute name="placeholder" type="String" default="Search..." description="Placeholder text for input search filed"/>
    
    <aura:attribute name="fieldLabel" type="String" required="true" description="input search field Label"/>
    
    <aura:attribute name="filter" type="String[]" default="[]" description="Array of filter for SOSL query. All the filters should be given in this field separated by comma(,) Example: AccountId='00128000002KuXU' "/>
    
    <aura:attribute name="selectedRecordId" type="String" description="Used to store the selected record id. While calling this component from other component, set this attribute to the lookup field API name"/>
    
    <aura:attribute name="selectedRecordLabel" type="String" description="This is used to show the selected record Name in search input"/>
    
    <aura:attribute name="subHeadingFieldsAPI" type="String[]" description="Field API for the fields to be shown under the record Name. Must be comma separated. Example: Email,Phone"/>
    
    <aura:attribute name="matchingRecords" type="Object[]" access="private" description="List of records returned from server side call."/>
    
    <aura:handler name="lookupSelect" event="c:sObjectLookupSelectEvent" action="{!c.handleLookupSelectEvent}" description="Event handler to get the selected record Id and Name from LookupItem component"/>
    
    
<div class="slds-form-element__control">
        
<div class="slds-combobox_container slds-has-inline-listbox">
            
<div aura:id="lookupdiv" class="slds-combobox slds-dropdown-trigger slds-dropdown-trigger_click slds-combobox-lookup" aria-expanded="false" aria-haspopup="listbox" role="combobox">
                
                
<div class="slds-combobox__form-element">
                    <!-- using input type "search" to place the search icon input field-->
                    <lightning:input type="search" aura:id="searchinput" label="{!v.fieldLabel}" name="{!v.fieldLabel}" value="{!v.selectedRecordLabel}" onchange="{!c.searchRecords}" isLoading="false" placeholder="{!v.placeholder}" onfocus="{!c.searchRecords}" onblur="{!c.hideList}"/>
                </div>

                
<div id="listbox-unique-id" role="listbox">
                    
<ul class="slds-listbox slds-listbox_vertical slds-dropdown slds-dropdown_fluid" role="presentation">
                        <!-- LookupItem component for creating record list -->
                        <aura:iteration var="rec" items="{!v.matchingRecords}">
                            <c:sObjectLookupItem record="{!rec}" subHeadingFieldsAPI="{!v.subHeadingFieldsAPI}" iconCategoryName="standard:contact"/>
                        </aura:iteration>
                    </ul>

                </div>

            </div>

        </div>

    </div>

</aura:component>

sObjectLookupController.js:
Now create below JavaScript controller for above sObjectLookup.cmp component.

({
    //Function to handle the sObjectLookupSelectEvent. Sets the chosen record Id and Name
    handleLookupSelectEvent : function (component,event,helper) {
        component.set("v.selectedRecordId", event.getParam("recordId"));
        component.set("v.selectedRecordLabel",event.getParam("recordLabel"));
        helper.toggleLookupList(component,
                                false,
                                'slds-combobox-lookup',
                                'slds-is-open');
    },
    
    //Function for finding the records as for given search input
    searchRecords : function (component,event,helper) {
        var searchText = component.find("searchinput").get("v.value");
        
        if(searchText){
            helper.searchSOSLHelper(component,searchText);
        }else{
            helper.searchSOQLHelper(component);
        }
    },
    
    //function to hide the list on onblur event.
    hideList :function (component,event,helper) {
        //Using timeout and $A.getCallback() to avoid conflict between LookupChooseEvent and onblur
        window.setTimeout(
            $A.getCallback(function() {
                if (component.isValid()) {
                    helper.toggleLookupList(component,
                                            false,
                                            'slds-combobox-lookup',
                                            'slds-is-open'
                                           );
                }
            }), 200
        );
    }
})

sObjectLookupHelper.js:
Now create below JavaScript helper for above sObjectLookupController.js JavaScript controller.

({
    //Function to toggle the record list drop-down
    toggleLookupList : function (component, ariaexpanded, classadd, classremove) {
        component.find("lookupdiv").set("v.aria-expanded", true);
        $A.util.addClass(component.find("lookupdiv"), classadd);
        $A.util.removeClass(component.find("lookupdiv"), classremove);
    },
    
    //function to call SOSL apex method.
    searchSOSLHelper : function (component,searchText) {
        //validate the input length. Must be greater then 3.
        //This check also manages the SOSL exception. Search text must be greater then 2.
        if(searchText && searchText.length > 3){
            //show the loading icon for search input field
            component.find("searchinput").set("v.isLoading", true);
            
            //server side callout. returns the list of record in JSON string
            var action = component.get("c.search");
            action.setParams({
                "objectAPIName": component.get("v.objectAPIName"),
                "searchText": searchText,
                "whereClause" : component.get("v.filter"),
                "extrafields":component.get("v.subHeadingFieldsAPI")
            });
            
            action.setCallback(this, function(a){
                var state = a.getState();
                
                if(component.isValid() && state === "SUCCESS") {
                    //parsing JSON return to Object[]
                    var result = [].concat.apply([], JSON.parse(a.getReturnValue()));
                    component.set("v.matchingRecords", result);
                    console.log( component.get("v.matchingRecords"));
                    
                    //Visible the list if record list has values
                    if(a.getReturnValue() && a.getReturnValue().length > 0){
                        this.toggleLookupList(component,
                                              true,
                                              'slds-is-open',
                                              'slds-combobox-lookup');
                        
                        //hide the loading icon for search input field
                        component.find("searchinput").set("v.isLoading", false);
                    }else{
                        this.toggleLookupList(component, false,
                                              'slds-combobox-lookup',
                                              'slds-is-open');
                    }
                }else if(state === "ERROR") {
                    console.log('error in searchRecords');
                }
            });
            $A.enqueueAction(action);
        }else{
            //hide the drop down list if input length less then 3
            this.toggleLookupList(component,
                                  false,
                                  'slds-combobox-lookup',
                                  'slds-is-open'
                                 );
        }
    },
    
    //function to call SOQL apex method.
    searchSOQLHelper : function (component) {
        component.find("searchinput").set("v.isLoading", true);
        //var searchText = component.find("searchinput").get("v.value");
        
        var action = component.get("c.getRecentlyViewed");
        action.setParams({
            "objectAPIName": component.get("v.objectAPIName"),
            "whereClause" : component.get("v.filter"),
            "extrafields":component.get("v.subHeadingFieldsAPI")
        });
        
        //console.log(searchText);
        
        // Configure response handler
        action.setCallback(this, function(response) {
            var state = response.getState();
            if(component.isValid() && state === "SUCCESS") {
                if(response.getReturnValue()){
                    component.set("v.matchingRecords", response.getReturnValue());
                    console.log( component.get("v.matchingRecords"));
                    if(response.getReturnValue().length>0){
                        this.toggleLookupList(component,
                                              true,
                                              'slds-is-open',
                                              'slds-combobox-lookup');
                    }
                    component.find("searchinput").set("v.isLoading", false);
                }
            } else {
                console.log('Error in loadRecentlyViewed: ' + state);
            }
        });
        $A.enqueueAction(action);
    }
})

sObjectLookupSelectEvent.evt:
Create below Lightning Event, which is used to store and fill the input field with selected record Id and Name. Fired from sObjectLookupItem.cmp component, handled at Lookup component.

<aura:event type="COMPONENT" description="sObjectLookupSelectEvent">
    <aura:attribute name="recordId" type="String" required="true" description="Used to send selected record Id"/>
    
    <aura:attribute name="recordLabel" type="String" description="Used to send selected record Name" required="true"/>
</aura:event>

sObjectLookupItem.cmp:
Create below lightning component, which will be used for creating list elements for records in sObjectLookup.cmp component.

<!--sObjectLookupItem.cmp-->
<aura:component description="sObjectLookupItem. Component used for creating list elements for records. Used in Lookup component">
    
    <!-- Component attributes-->
    <aura:attribute name="record" type="Object" description="Holds the single record instance" required="true"/>
    
    <aura:attribute name="subHeadingFieldsAPI" type="String[]" description="Holds the field API names to show as meta entity in list"/>
    
    <aura:attribute name="subHeadingFieldValues" type="String" description="Used to construct the meta entity value. Works as subheading in record option"/>
    
    <aura:attribute name="iconCategoryName" type="String" description="Lightning icon category and icon name to show with each record element"/>
    
    <!-- Component event registers-->
    <aura:registerEvent name="lookupSelect" type="c:sObjectLookupSelectEvent" description="Event used to send the selected record Id and Name to Lookup component"/>
    
    <!-- Component event handlers-->
    <aura:handler name="init" value="{!this}" action="{!c.loadValues}" description="standard init event to prepare the sub heading mete entity value"/>
    
    <!-- Component markup-->
    
<li role="presentation" class="slds-listbox__item" onclick="{!c.choose}">
        <span class="slds-media slds-listbox__option slds-listbox__option_entity slds-listbox__option_has-meta" role="option">
            <!-- lightning icon -->
            <span class="slds-media__figure">
                <lightning:icon iconName="{!v.iconCategoryName}" size="small" alternativeText="{!v.record.Name}"/>
            </span>
            <!-- option Name-->
            <span class="slds-media__body">
                <span class="slds-listbox__option-text slds-listbox__option-text_entity">
                    {!v.record.Name}
                </span>
                <!-- option sub heading. Also known as meta entity as per SLDS combobox component-->
                <span class="slds-listbox__option-meta slds-listbox__option-meta_entity">
                    {!v.subHeadingFieldValues}
                </span>
            </span>
        </span>
    </li>

</aura:component>

sObjectLookupItemController.js:
Now create below JavaScript controller for above sObjectLookupItem.cmp component.

({
    loadValues : function (component) {
        var record = component.get("v.record");
        var subheading = '';
        for(var i=0; i<component.get("v.subHeadingFieldsAPI").length ;i++ ){
            if(record[component.get("v.subHeadingFieldsAPI")[i]]){
                subheading = subheading + record[component.get("v.subHeadingFieldsAPI")[i]] + ' • ';
            }
        }
        subheading = subheading.substring(0,subheading.lastIndexOf('•'));
        component.set("v.subHeadingFieldValues", subheading);
    },
    
    choose : function (component,event) {
        var chooseEvent = component.getEvent("lookupSelect");
        chooseEvent.setParams({
            "recordId" : component.get("v.record").Id,
            "recordLabel":component.get("v.record").Name
        });
        chooseEvent.fire();
        console.log('event fired');
    }
})

sObjectLookupController:
Create below apex controller to use it in sObjectLookup.cmp component.

public with sharing class sObjectLookupController {
    
    /* Method to query records using SOSL*/
    @AuraEnabled
    public static String search(String objectAPIName, String searchText,
                                List<String> whereClause, List<String> extrafields){
                                    
                                    objectAPIName = String.escapeSingleQuotes(objectAPIName);
                                    searchText = String.escapeSingleQuotes(searchText);
                                    String searchQuery = 'FIND \'' + searchText + '*\' IN ALL FIELDS RETURNING ' + objectAPIName + '(Id,Name' ;
                                    if(!extrafields.isEmpty()){
                                        searchQuery = searchQuery + ',' + String.join(extrafields, ',') ;
                                    }
                                    system.debug(whereClause);
                                    if(!whereClause.isEmpty()){
                                        searchQuery = searchQuery + ' WHERE ' ;
                                        searchQuery = searchQuery + String.join(whereClause, 'AND') ;
                                    }
                                    searchQuery = searchQuery + ' LIMIT 10 ) ';
                                    system.debug(searchQuery);
                                    return JSON.serializePretty(search.query(searchQuery)) ;
                                }
    
    /* Method to query records using SOQL*/
    @AuraEnabled
    public static List<SObject> getRecentlyViewed(
        String objectAPIName,
        List<String> whereClause,
        List<String> extrafields){
            
            String searchQuery = 'SELECT Id, Name';
            if(!extrafields.isEmpty()){
                searchQuery = searchQuery + ',' + String.join(extrafields, ',') ;
            }
            searchQuery = searchQuery + ' FROM ' + objectAPIName + ' WHERE LastViewedDate != NULL ';
            if(!whereClause.isEmpty()){
                searchQuery = searchQuery + ' AND ' ;
                searchQuery = searchQuery + String.join(whereClause, 'AND') ;
                system.debug(searchQuery);
            }
            searchQuery = searchQuery + ' ORDER BY LastViewedDate DESC' ;
            List<SObject> objectList =  new List<SObject>();
            objectList = Database.query(searchQuery);
            return objectList;
        }
}

Implementation Notes:

  • On focusing the input field it shows the recent items viewed for given Object. This replcates the standard behavior noticed in standard lightning lookup fields. If filter attribute is given then recent items will respect the filter condition.
  • SOSL and SOQL both are utilizied to search for records. SOQL is used to fetch the recently viewed records because SOSL cannoot accept blank text. SOSL is used if input field has text. Also, because searching can be done on non-index fields SOSL gives better performance in terms of speed and record list. With SOSL text fields are considered as indexed.
  • Input text must be greater than 3 in size. Noticed that SOSL throws an error with less than 3 search string length.
  • To show and hide the record drop-down list SLDS “slds-combobox-lookup”, “slds-is-open” classes and “v.aria-expanded” attribute is used.
  • $A.getCallback() is used to hide the record list on “onblur” of input field. Because we have an event firing on click of list item, onblur conflict is noticed and doesn’t allows to fire the sObjectLookupSelectEvent.evt event.
  • Maintains the record sharing security.

Usage:

<!--sObjectLookupApp.app-->
<aura:application implements="force:appHostable" extends="force:slds" description="sObjectLookupApp">
    <!-- with Filter -->
    <c:sObjectLookup fieldLabel="Contact" objectAPIName="Contact" subHeadingFieldsAPI="Email,Phone" placeholder="Search Contact" filter="AccountId='00128000002KuXU'"/>
    
    <!-- without Filter -->
    <c:sObjectLookup fieldLabel="Contact" objectAPIName="Contact" subHeadingFieldsAPI="Email,Phone" placeholder="Search Contact"/>
</aura:application>

Output: