Synopsis
Relationship of Lookup in Salesforce
Establishes a connection between two records to enable their mutual association. The parent record gains the ability to exhibit a related list, showcasing all linked records. These lookup relationship fields can be established to connect users, standard objects, or custom objects. If the record referenced by a lookup field gets deleted, Salesforce will typically erase the lookup field’s value. Conversely, the option exists to prohibit record deletions if they are part of a lookup relationship.
For standard or custom objects, a lookup relationship generates a field that enables users to click on a lookup icon and pick another record from a pop-up window.
Lookup Functionality in Custom Lightning Components
When incorporating look-up functionality within Salesforce Lightning components, there isn’t a dedicated input tag provided by Salesforce that directly generates the look-up field user interface.
The <lightning:inputField />
tag, which can automate the rendering of the look-up field, is exclusively applicable within the context of:
<lightning:recordEditForm/>
<lightning:recordForm />
<lightning:recordViewForm/>
Hence, to address this limitation, we need to construct a customized look-up component.
The envisioned look-up component we’re about to create is designed to be versatile, making it reusable across various scenarios. It is not limited to a specific object type and has the capability to facilitate both multi-select and single-select functionalities. Additionally, the component will possess the ability to populate values in advance.
Flow
- Phase 1: Formulate an Apex class that generates a collection of records (up to a maximum of 5) dependent on the search phrase, while excluding any chosen records (if applicable).
- Phase 2: Develop a primary Lightning component featuring a search input, which triggers the Apex class upon keystroke adjustments, armed with two parameters (”, ”).
- Phase 3: Establish a subsidiary component responsible for showcasing the compiled record list returned to the parent component.
- Phase 4: Present the selected record as distinct tokens (pills).
- Controller: ContactController
- Components: CustomLookUpComp (Parent), childLookUp (Child)
- Events: CustomLookUpFinalValueEvent, LookupEventToParent
- ContactController.apxc
public class ContactController { @AuraEnabled public static List getContacts(String searchTerm, List selectedOptions) { List Ids = new List(); for(Contact c : selectedOptions){ Ids.add(c.Id); } system.debug('selectedOptions:::'+selectedOptions); system.debug('Ids:::'+Ids); if(selectedOptions != NULL){ List conList = [Select Id, Name from Contact Where Name Like : ('%'+searchTerm+'%') AND (Id != : Ids) LIMIT 5]; system.debug('conList:::'+conList); return conList; }else{ List conList = [Select Id, Name from Contact Where Name Like : ('%'+searchTerm+'%') LIMIT 5]; system.debug('conList:::'+conList); return conList; } } @AuraEnabled public static List getUsers(String searchTerm, List selectedOptions) { List Ids = new List(); for(User c : selectedOptions){ Ids.add(c.Id); } system.debug('selectedOptions:::'+selectedOptions); system.debug('Ids:::'+Ids); if(selectedOptions != NULL){ List userList = [Select Id, Name from User Where Name Like : ('%'+searchTerm+'%') AND (Id != : Ids) LIMIT 5]; system.debug('userList:::'+userList); return userList; }else{ List userList = [Select Id, Name from User Where Name Like : ('%'+searchTerm+'%') LIMIT 5]; system.debug('userList:::'+userList); return userList; } } }
CustomLookupComponent.cmp
<!-- CustomLookUpComp --> <aura:component controller="ContactController" implements="force:appHostable,flexipage:availableForAllPageTypes,flexipage:availableForRecordHome,force:hasRecordId,forceCommunity:availableForAllPageTypes,force:lightningQuickAction" access="global" > <!-- Main attributes Start --> <aura:attribute name="uniqueName" type="String" required="true" default="lookup1"/><!--If not entered it takes default value--> <aura:attribute name="iconName" type="String" default="standard:contact" required="true"/> <aura:attribute name="methodName" type="String" default="c.getContacts" required="true"/> <aura:attribute name="singleSelect" type="Boolean" default="true"/> <aura:attribute name="sObject" type="String" default="Contact" required="true"/> <aura:attribute name="selected" type="Map[]"/><!--Optional If you want any pre-population can use this to pass that record Id value--> <!-- Main attributes End --> <aura:attribute name="sTerm" type="string" default=""/> <aura:attribute name="conList" type="Map[]"/> <aura:attribute name="blurTimeout" type="Integer"/> <!-- Register Final value Event --> <aura:registerEvent name="CustomLookUpFinalValueEvent" type="c:CustomLookUpFinalValueEvent"/> <!-- Handlers --> <aura:handler name="init" value="{!this}" action="{!c.doInit}"/> <aura:handler name="LookupEventToParent" event="c:LookupEventToParent" action="{!c.handleEvent}"/> <!-- Method to send pre-populate lookUp value to the requested component: parent to lookup--> <aura:method name="prePopulateMethod" action="{!c.render}"> <aura:attribute name="populatedRecord" type="Map[]"/> </aura:method> <aura:if isTrue="{!v.singleSelect}"> <div class="slds-form-element"> <div class="slds-form-element__control"> <div class="slds-combobox_container slds-has-selection"> <div aura:id="toOpen" class="slds-combobox slds-dropdown-trigger slds-dropdown-trigger_click" aria-expanded="true" aria-haspopup="listbox" role="combobox"> <div class=" slds-input-has-icon slds-input-has-icon_right" role="none"> <div> <div aura:id="input" class="slds-p-top_none" > <lightning:input class="slds-p-top_none" variant="label-hidden" name="Search" value="{!v.sTerm}" onblur="{!c.onblur}" onclick="{!c.onfocus}" onchange="{!c.onchange}" placeholder="{!'Select ' + (v.sObject)}"/> <span class="slds-icon_container slds-icon-utility-search slds-input__icon slds-input__icon_right "> <span class="slds-icon slds-icon slds-icon_x-small slds-icon-text-default" aria-hidden="true"> <lightning:icon class="" iconName="utility:search" size="x-small" alternativeText="Search" /> </span> </span> </div> <aura:if isTrue="{!v.selected}"> <div aura:id="lookup-pill" class="slds-pill-container slds-hide"> <aura:iteration items="{!v.selected}" var="l"> <lightning:pill class="pillSize" label="{!l.Name}" name="{!l}" onremove="{! c.handleRemoveOnly }" href="{!'/'+(l.Id)}"> <aura:set attribute="media"> <lightning:icon iconName="{!v.iconName}" alternativeText="{!v.iconName}"/> </aura:set> </lightning:pill> </aura:iteration> </div> </aura:if> </div> </div> <div onfocus="{!c.onfocus}" id="listbox-id-1" class="slds-dropdown slds-dropdown_length-with-icon-7 slds-dropdown_fluid" role="listbox"> <ul class="slds-listbox slds-listbox_vertical" role="presentation"> <aura:iteration items="{!v.conList}" var="listItem"> <span ><c:childLookUp myContact="{!listItem}" icon="{!v.iconName}" object="{!v.sObject}"/></span> </aura:iteration> </ul> </div> </div> </div> </div> </div> <!-- Multi Select Start--> <aura:set attribute="else"> <div class="slds-form-element"> <div class="slds-form-element__control"> <div class="slds-combobox_container slds-has-selection"> <div aura:id="toOpen" class="slds-combobox slds-dropdown-trigger slds-dropdown-trigger_click" aria-expanded="true" aria-haspopup="listbox" role="combobox"> <div class=" slds-input-has-icon slds-input-has-icon_right" role="none"> <div> <lightning:input class="slds-p-top_none" variant="label-hidden" name="Search" value="{!v.sTerm}" onblur="{!c.onblur}" onclick="{!c.onfocus}" onchange="{!c.onchange}" placeholder="{!'Select ' + (v.sObject)}"/> <span class="slds-icon_container slds-icon-utility-search slds-input__icon slds-input__icon_right"> <span class="slds-icon slds-icon slds-icon_x-small slds-icon-text-default" aria-hidden="true"> <lightning:icon iconName="utility:search" size="x-small" alternativeText="Search" /> </span> </span> </div> </div> <div onfocus="{!c.onfocus}" id="listbox-id-1" class="slds-dropdown slds-dropdown_length-with-icon-7 slds-dropdown_fluid" role="listbox"> <ul class="slds-listbox slds-listbox_vertical" role="presentation"> <aura:iteration items="{!v.conList}" var="listItem"> <span ><c:childLookUp myContact="{!listItem}" icon="{!v.iconName}" object="{!v.sObject}"/></span> </aura:iteration> </ul> </div> </div> </div> </div> <!--div class="slds-listbox_selection-group slds-listbox-toggle"--> <ul class="slds-listbox slds-listbox_horizontal" role="listbox" aria-label="Selected Options:" aria-orientation="horizontal"> <aura:iteration items="{!v.selected}" var="l"> <li class="slds-listbox-item slds-truncate" role="presentation"> <aura:if isTrue="{! v.selected}"> <lightning:pill class="" label="{!l.Name}" name="{!l}" onremove="{! c.handleRemoveOnly }" href="{!'/'+(l.Id)}"> <aura:set attribute="media"> <lightning:icon iconName="{!v.iconName}" alternativeText="{!v.iconName}"/> </aura:set> </lightning:pill> </aura:if> </li> </aura:iteration> </ul> <!--/div--> </div> </aura:set> <!-- Multi Select End--> </aura:if> </aura:component>
CustomLookupComponentController.js
({ doInit : function(component, event, helper) { helper.doInit(component, event, helper); }, render : function(component, event, helper) { helper.render(component, event, helper); }, onchange : function(component, event, helper) { helper.onchange(component, event, helper); }, onblur : function(component, event, helper) { helper.onblur(component, event, helper); }, onfocus : function(component, event, helper) { helper.onfocus(component, event, helper); }, handleRemoveOnly : function(component, event, helper) { helper.handleRemoveOnly(component, event, helper); }, onOptionClick : function(component, event, helper) { helper.onOptionClick(component, event, helper); }, handleEvent : function(component, event, helper) { helper.handleEvent(component, event, helper); }, })
CustomLookupComponentHelper.js
({ doInit : function(component, event, helper) { //As of now does nothing }, render : function(component, event, helper) { //This is to send the prepopulated value through event to parent console.log("In render"); var args = event.getParam("arguments"); console.log(JSON.stringify(args)); console.log(JSON.stringify(args.populatedRecord)); if(args){ // Firing Final value event var finalEvent = component.getEvent("CustomLookUpFinalValueEvent"); var uName = component.get("v.uniqueName"); console.log("Final Event fired:: "+uName); finalEvent.setParams({ uniqueName : uName, finalValue : args.populatedRecord }); finalEvent.fire(); }else{ console.log("Didnt get any record to pre-populate"); } }, onchange : function(component, event, helper) { console.log("Onchange"); var icon = component.get("v.iconName"); var Object = component.get("v.sObject"); console.log("icon:: "+icon); console.log("Object:: "+Object); // Setting method name dynamically var methodName = component.get("v.methodName"); console.log(methodName); /*Send this value to server to get values other than in this list*/ var selected = component.get("v.selected"); console.log("selected"); console.log(JSON.stringify(selected)); var action = component.get(methodName); var term = component.get("v.sTerm"); action.setParams({ "searchTerm" : term, "selectedOptions" : selected }); if(term.length > 0){ action.setCallback(this, function(response){ var state = response.getState(); console.log(state); if(state === "SUCCESS") { var result = response.getReturnValue(); console.log(JSON.stringify(result)); console.log(result); component.set("v.conList", result); if(term != "" && result.length > 0){ var ToOpen = component.find("toOpen"); $A.util.addClass(ToOpen, "slds-is-open"); }else{ var ToOpen = component.find("toOpen"); $A.util.removeClass(ToOpen, "slds-is-open"); } } }); $A.enqueueAction(action); } }, onblur : function(component, event, helper) { //Setting timeout so that we can capture the value onclick const blurTimeout = window.setTimeout( $A.getCallback(() => { var ToOpen = component.find("toOpen"); $A.util.removeClass(ToOpen, "slds-is-open"); }), 300 ); component.set('v.blurTimeout', blurTimeout); }, onfocus : function(component, event, helper) { var term = component.get("v.sTerm"); var returnedResults = component.get("v.conList"); console.log("in onfocus"); console.log(term); if(term && returnedResults.length > 0){ var ToOpen = component.find("toOpen"); $A.util.addClass(ToOpen, "slds-is-open"); } }, handleRemoveOnly : function(component, event, helper) { var singleSel = component.get("v.singleSelect"); if(singleSel){ console.log("in remove"); var sel = event.getSource().get("v.name"); console.log(JSON.stringify(sel)); var lis = component.get("v.selected"); console.log(JSON.stringify(lis)); for(var i = 0; i < lis.length; i++){ console.log(JSON.stringify(lis[i])); console.log(lis[i].Id == sel.Id); if(lis[i].Id == sel.Id){ lis.splice(i,1); } } component.set("v.selected", lis); console.log(JSON.stringify(lis)); var Input = component.find("input"); $A.util.removeClass(Input, "slds-hide"); var lookupPill = component.find("lookup-pill"); $A.util.addClass(lookupPill, "slds-hide"); // Firing Final value event var finalEvent = component.getEvent("CustomLookUpFinalValueEvent"); var uName = component.get("v.uniqueName"); console.log("Final Event fired "+uName); finalEvent.setParams({ uniqueName : uName, finalValue : lis }); finalEvent.fire(); }else{ console.log("in remove"); var sel = event.getSource().get("v.name"); console.log(JSON.stringify(sel)); var lis = component.get("v.selected"); console.log(JSON.stringify(lis)); for(var i = 0; i < lis.length; i++){ console.log(JSON.stringify(lis[i])); console.log(lis[i].Id); console.log(sel.Id); console.log(lis[i].Id == sel.Id); if(lis[i].Id == sel.Id){ lis.splice(i,1); } } component.set("v.selected", lis); console.log(JSON.stringify(lis)); // Firing Final value event var finalEvent = component.getEvent("CustomLookUpFinalValueEvent"); var uName = component.get("v.uniqueName"); console.log("Final Event fired "+uName); finalEvent.setParams({ uniqueName : uName, finalValue : lis }); finalEvent.fire(); } }, handleEvent : function(component, event, helper) { var lookupEventToParent = event.getParam("selectedItem"); /* selectedValue attribute is used to de duplicate the list box options in dropdown after selecting a value. so that the value does not repeat in box after selecting once*/ console.log("In event handler"); console.log(JSON.stringify(lookupEventToParent)); var singleSel = component.get("v.singleSelect"); if(!singleSel){ var selectedList = []; var existing = component.get("v.selected"); if(existing.length > 0){ for(var i = 0; i < existing.length; i++){ selectedList.push(existing[i]); } } selectedList.push(lookupEventToParent); console.log(JSON.stringify(selectedList)); component.set("v.selected", selectedList); const blurTimeout = component.get('v.blurTimeout'); if (blurTimeout) { clearTimeout(blurTimeout); } var ToOpen = component.find("toOpen"); $A.util.removeClass(ToOpen, "slds-is-open"); //Empty Search string component.set("v.sTerm", ""); // Firing Final value event var finalEvent = component.getEvent("CustomLookUpFinalValueEvent"); var uName = component.get("v.uniqueName"); console.log("Final Event fired "+uName); finalEvent.setParams({ uniqueName : uName, finalValue : selectedList }); finalEvent.fire(); }else{ var selectedList = []; //var existing = component.get("v.selected"); selectedList.push(lookupEventToParent); console.log(JSON.stringify(selectedList)); component.set("v.selected", selectedList); var Input = component.find("input"); $A.util.addClass(Input, "slds-hide"); var lookupPill = component.find("lookup-pill"); $A.util.removeClass(lookupPill, "slds-hide"); const blurTimeout = component.get('v.blurTimeout'); if (blurTimeout) { clearTimeout(blurTimeout); } var ToOpen = component.find("toOpen"); $A.util.removeClass(ToOpen, "slds-is-open"); //Empty Search string component.set("v.sTerm", ""); // Firing Final value event var finalEvent = component.getEvent("CustomLookUpFinalValueEvent"); var uName = component.get("v.uniqueName"); console.log("Final Event fired "+uName); finalEvent.setParams({ uniqueName : uName, finalValue : selectedList }); finalEvent.fire(); } } })
childLookUp.cmp
<aura:component controller="ContactController" implements="force:appHostable,flexipage:availableForAllPageTypes,flexipage:availableForRecordHome,force:hasRecordId,forceCommunity:availableForAllPageTypes,force:lightningQuickAction" access="global" > <aura:attribute name="myContact" type="Map"/> <aura:attribute name="icon" type="String"/> <aura:attribute name="object" type="String"/> <aura:handler name="init" value="{!this}" action="{!c.doinit}"/> <aura:registerEvent name="LookupEventToParent" type="c:LookupEventToParent"/> <li role="presentation" class="slds-listbox__item " onclick ="{!c.onOptionClick}"> <div id="option1" class="slds-media slds-listbox__option slds-listbox__option_entity slds-media_center" role="option"> <span class="slds-media__figure slds-listbox__option-icon"> <span class="slds-icon_container slds-icon-standard-account"> <div class="slds-icon slds-icon_small" aria-hidden="true"> <lightning:icon iconName="{!v.icon}" alternativeText="{!v.object}" size="small"/> </div> </span> </span> <span class="slds-media__body"> <span class="slds-listbox"> {!v.myContact.Name} </span> </span> </div> </li> </aura:component>
ChildLookupController.js
({ doinit : function(component, event, helper) { console.log("In Child lookup Init"); var icon = component.get("v.iconName"); var Object = component.get("v.sObject"); console.log("icon:: "+icon); console.log("Object:: "+Object); }, onOptionClick : function(component, event, helper) { console.log("In child init"); var selVal = component.get("v.myContact"); console.log(selVal); var evt = component.getEvent("LookupEventToParent"); evt.setParams({ selectedItem : selVal }); evt.fire(); } })
CustomLookupFinalValueEvent.evt
<!-- CustomLookUpFinalValueEvent --> <aura:event type="COMPONENT" description="Event template"> <aura:attribute name="uniqueName" type="String"/> <aura:attribute name="finalValue" type="Map[]"/> </aura:event>
LookupEventToParent.evt
<!-- LookupEventToParent --> <aura:event type="COMPONENT" description="Event template"> <aura:attribute name="selectedItem" type="Map"/> </aura:event>
UPDATE
You can now employ this lookup component for any object, utilizing just a single Apex method with the provided code. We’ve implemented a dynamic query approach to eliminate the necessity of crafting methods for each individual object. Please refer to the alterations below.
To the existing ContactController.apxc Apex class, incorporate the following lookUp() method.
@AuraEnabled public static List lookUp(String searchTerm, List selectedOptions, string myObject) { List Ids = new List(); for(sObject c : selectedOptions){ Ids.add(c.Id); } if(selectedOptions != NULL){ String myQuery = 'Select Id, Name from '+myObject+' Where Name Like \'%' + searchTerm + '%\' AND (Id != : Ids) LIMIT 5'; List lookUpList = database.query(myQuery); system.debug('lookUpList:::'+lookUpList); return lookUpList; }else{ String myQuery = 'Select Id, Name from '+myObject+' Where Name Like \'%' + searchTerm + '%\' AND (Id != : Ids) LIMIT 5'; List lookUpList = database.query(myQuery); system.debug('lookUpList:::'+lookUpList); return lookUpList; } }
Incorporate the line “myObject”: Object into CustomLookupComponentHelper.js as demonstrated below.
action.setParams({ "searchTerm" : term, "selectedOptions" : selected, "myObject" : Object //Updated Add this line in your code });
I’ll ensure you’re informed of any updates 😃 Are there any additional ideas you’d like to contribute to this list that I might not have covered? What types of content are you interested in seeing more of on this blog? Feel free to share your thoughts in the comment section below…
A Quick Favor: If you found value in this blog post, consider sharing it with a friend!
Also, remember to subscribe to receive the latest updates directly in your inbox.