Custom Reusable Lookup Component In Salesforce LWC
Hey guys, today in this article we are going to create a custom lookup lightning web component [LWC]. In Salesforce, generally we use lookup field to connect 2 objects. Sometimes we have to create a custom input form where we need to provide lookup type field to select a related record. till today there is no base lightning component available either in AURA or LWC for lookup field.
View As Aura Component : Custom Reusable lookup In Salesforce Aura
Custom Lookup Key Highlights :
- Reusable component
- Smart Delay Implementation to avoid a very large number of Apex method calls
- Fully dynamic & manageable code
- We can set predefined Values
- Following lookup properties are configurable :
- Field Label
- Object API Name [Standard/Custom]
- Place Holder
- Default Selected Record
Let’s Start….
Step 1 : Create Apex Class for Lookup :
In the APEX class, CustomLookupLwcController.apxc
, copy and paste the following code :
/* API : 50 Source : lwcFactory.com */ public class CustomLookupLwcController { // Method to fetch lookup search result @AuraEnabled(cacheable=true) public static list<sObject> fetchLookupData(string searchKey , string sObjectApiName) { List < sObject > returnList = new List < sObject > (); string sWildCardText = '%' + searchKey + '%'; string sQuery = 'Select Id,Name From ' + sObjectApiName + ' Where Name Like : sWildCardText order by createdDate DESC LIMIT 5'; for (sObject obj: database.query(sQuery)) { returnList.add(obj); } return returnList; } // Method to fetch lookup default value @AuraEnabled public static sObject fetchDefaultRecord(string recordId , string sObjectApiName) { string sRecId = recordId; string sQuery = 'Select Id,Name From ' + sObjectApiName + ' Where Id = : sRecId LIMIT 1'; for (sObject obj: database.query(sQuery)) { return obj; } return null; } }
Step2 : Create Lightning Web Component :
Copy and paste the following code in HTML and JavaScript files:
HTML File :
<!-- API : 50 Source : lwcFactory.com --> <template> <div class="slds-form-element" onmouseleave={toggleResult} data-source="lookupContainer"> <div class="slds-combobox_container slds-has-selection"> <label class="slds-form-element__label" for="combobox-id-1">{label}</label> <div class="lookupInputContainer slds-combobox slds-dropdown-trigger slds-dropdown-trigger_click" aria-expanded="false" aria-haspopup="listbox" role="combobox"> <div class="slds-combobox__form-element slds-input-has-icon slds-input-has-icon_left-right" role="none"> <div class="searchBoxWrapper slds-show"> <!--Lookup Input Field--> <lightning-input type="search" data-source="searchInputField" onclick={toggleResult} onchange={handleKeyChange} is-loading={isSearchLoading} value={searchKey} variant="label-hidden" placeholder={placeholder} ></lightning-input> </div> <!--Lookup Selected record pill container start--> <div class="pillDiv slds-hide"> <span class="slds-icon_container slds-combobox__input-entity-icon"> <lightning-icon icon-name={iconName} size="x-small" alternative-text="icon"></lightning-icon> </span> <input type="text" id="combobox-id-1" value={selectedRecord.Name} class="slds-input slds-combobox__input slds-combobox__input-value" readonly /> <button class="slds-button slds-button_icon slds-input__icon slds-input__icon_right" title="Remove selected option"> <lightning-icon icon-name="utility:close" size="x-small" alternative-text="close icon" onclick={handleRemove}></lightning-icon> </button> </div> </div> <!-- lookup search result part start--> <div style="margin-top:0px" id="listbox-id-5" class="slds-dropdown slds-dropdown_length-with-icon-7 slds-dropdown_fluid" role="listbox"> <ul class="slds-listbox slds-listbox_vertical" role="presentation"> <template for:each={lstResult} for:item="obj"> <li key={obj.Id} role="presentation" class="slds-listbox__item"> <div data-recid={obj.Id} onclick={handelSelectedRecord} class="slds-media slds-listbox__option slds-listbox__option_entity slds-listbox__option_has-meta" role="option"> <span style="pointer-events: none;" class="slds-media__figure slds-listbox__option-icon" > <span class="slds-icon_container" > <lightning-icon icon-name={iconName} size="small" alternative-text="icon" ></lightning-icon> </span> </span> <span style="pointer-events: none;" class="slds-media__body" > <span class="slds-listbox__option-text slds-listbox__option-text_entity">{obj.Name}</span> </span> </div> </li> </template> <!--ERROR msg, if there is no records..--> <template if:false={hasRecords}> <li class="slds-listbox__item" style="text-align: center; font-weight: bold;">No Records Found....</li> </template> </ul> </div> </div> </div> </div> </template>
JavaScript File :
/* API : 50 Source : lwcFactory.com */ import { LightningElement,api,wire} from 'lwc'; // import apex method from salesforce module import fetchLookupData from '@salesforce/apex/CustomLookupLwcController.fetchLookupData'; import fetchDefaultRecord from '@salesforce/apex/CustomLookupLwcController.fetchDefaultRecord'; const DELAY = 300; // dealy apex callout timing in miliseconds export default class CustomLookupLwc extends LightningElement { // public properties with initial default values @api label = 'custom lookup label'; @api placeholder = 'search...'; @api iconName = 'standard:account'; @api sObjectApiName = 'Account'; @api defaultRecordId = ''; // private properties lstResult = []; // to store list of returned records hasRecords = true; searchKey=''; // to store input field value isSearchLoading = false; // to control loading spinner delayTimeout; selectedRecord = {}; // to store selected lookup record in object formate // initial function to populate default selected lookup record if defaultRecordId provided connectedCallback(){ if(this.defaultRecordId != ''){ fetchDefaultRecord({ recordId: this.defaultRecordId , 'sObjectApiName' : this.sObjectApiName }) .then((result) => { if(result != null){ this.selectedRecord = result; this.handelSelectRecordHelper(); // helper function to show/hide lookup result container on UI } }) .catch((error) => { this.error = error; this.selectedRecord = {}; }); } } // wire function property to fetch search record based on user input @wire(fetchLookupData, { searchKey: '$searchKey' , sObjectApiName : '$sObjectApiName' }) searchResult(value) { const { data, error } = value; // destructure the provisioned value this.isSearchLoading = false; if (data) { this.hasRecords = data.length == 0 ? false : true; this.lstResult = JSON.parse(JSON.stringify(data)); } else if (error) { console.log('(error---> ' + JSON.stringify(error)); } }; // update searchKey property on input field change handleKeyChange(event) { // Debouncing this method: Do not update the reactive property as long as this function is // being called within a delay of DELAY. This is to avoid a very large number of Apex method calls. this.isSearchLoading = true; window.clearTimeout(this.delayTimeout); const searchKey = event.target.value; this.delayTimeout = setTimeout(() => { this.searchKey = searchKey; }, DELAY); } // method to toggle lookup result section on UI toggleResult(event){ const lookupInputContainer = this.template.querySelector('.lookupInputContainer'); const clsList = lookupInputContainer.classList; const whichEvent = event.target.getAttribute('data-source'); switch(whichEvent) { case 'searchInputField': clsList.add('slds-is-open'); break; case 'lookupContainer': clsList.remove('slds-is-open'); break; } } // method to clear selected lookup record handleRemove(){ this.searchKey = ''; this.selectedRecord = {}; this.lookupUpdatehandler(undefined); // update value on parent component as well from helper function // remove selected pill and display input field again const searchBoxWrapper = this.template.querySelector('.searchBoxWrapper'); searchBoxWrapper.classList.remove('slds-hide'); searchBoxWrapper.classList.add('slds-show'); const pillDiv = this.template.querySelector('.pillDiv'); pillDiv.classList.remove('slds-show'); pillDiv.classList.add('slds-hide'); } // method to update selected record from search result handelSelectedRecord(event){ var objId = event.target.getAttribute('data-recid'); // get selected record Id this.selectedRecord = this.lstResult.find(data => data.Id === objId); // find selected record from list this.lookupUpdatehandler(this.selectedRecord); // update value on parent component as well from helper function this.handelSelectRecordHelper(); // helper function to show/hide lookup result container on UI } /*COMMON HELPER METHOD STARTED*/ handelSelectRecordHelper(){ this.template.querySelector('.lookupInputContainer').classList.remove('slds-is-open'); const searchBoxWrapper = this.template.querySelector('.searchBoxWrapper'); searchBoxWrapper.classList.remove('slds-show'); searchBoxWrapper.classList.add('slds-hide'); const pillDiv = this.template.querySelector('.pillDiv'); pillDiv.classList.remove('slds-hide'); pillDiv.classList.add('slds-show'); } // send selected lookup record to parent component using custom event lookupUpdatehandler(value){ const oEvent = new CustomEvent('lookupupdate', { 'detail': {selectedRecord: value} } ); this.dispatchEvent(oEvent); } }
- Check code comments for more details…
- Deploy your component to connected Org.
How to use custom lookup component ?
Once we successfully create and deploy above component, now it’s time to see a demonstration of lookup component.
Let’s create a new web component where we will use custom lookup component tag.
HTML File : lookupDemo.html
Description :
In following HTML file we have added 2 custom lookup component for User and Account standard object [check highlighted code]. In first lookup component we provided user id to auto-populate field value on Initialization. Whenever a user select or remove lookup value, it will handle a event and invoke ‘lookupRecord‘ JS function to get updated values on parent component.
<template> <article class="slds-card" style="padding:20px"> <p style="color:blueviolet;font-size:24px">Custom Reusable Lookup Demo - LWC</p> <!--Custom Lookup with Pre-Populate Value for User Object--> <c-custom-lookup-lwc icon-name="standard:user" s-object-api-name="user" label="Owner [Pre-Populate Demo]" onlookupupdate={lookupRecord} default-record-id='0052y000001PmqDAAS' placeholder="type here..."></c-custom-lookup-lwc> <br/> <!--Custom Lookup without Pre-Populate Value for account Object--> <c-custom-lookup-lwc icon-name="standard:account" s-object-api-name="account" label="My Fev Account" onlookupupdate={lookupRecord} placeholder="Search Account here..."></c-custom-lookup-lwc> </article> </template>
JavaScript File : lookupDemo.js
import { LightningElement } from 'lwc'; export default class LookupDemo extends LightningElement { // handel custom lookup component event lookupRecord(event){ alert('Selected Record Value on Parent Component is ' + JSON.stringify(event.detail.selectedRecord)); } }
Meta XML file :
<?xml version="1.0" encoding="UTF-8"?> <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata"> <apiVersion>50.0</apiVersion> <isExposed>true</isExposed> <targets> <target>lightning__AppPage</target> <target>lightning__RecordPage</target> <target>lightning__HomePage</target> <target>lightningCommunity__Default</target> <target>lightningCommunity__Page</target> <target>lightning__Tab</target> <target>lightning__Inbox</target> </targets> </LightningComponentBundle>
Add Component to App in Lightning Experience
- From the App Launcher (), find and select Sales.
- Click then select Edit Page.
- Drag the
lookupDemo
Lightning web component from the Custom area of the Lightning Components list to the top of the Page Canvas. - Click Activate and Assign as Org Default and hit save.
- Refresh the page to view your new component.
Custom Lookup Output :
Resources :
For further reading:
- How To Use jQuery Data Table In Salesforce Lightning Web Component
- 2 Ways to Implement Salesforce Modal In Lightning Web Component
Happy coding!
A canada-based Salesforce Geek #lightning #lwc
12 comments
Hi Sir,
Could you please provide css file of the above lookup code. ?
The search bar is misbehaving in in different orgs
Hi Chandrakanth,
Very nice article.
Would you mind sharing the component CSS file, since there is a custom CSS class?
Thanks
Can u please provide Test class for CustomLookupLwcController.
public class CustomLookupLwcController {
// Method to fetch lookup search result
@AuraEnabled(cacheable=true)
public static list fetchLookupData(string searchKey , string sObjectApiName) {
List returnList = new List ();
string sWildCardText = ‘%’ + searchKey + ‘%’;
string sQuery = ‘Select Id,Name From ‘ + sObjectApiName + ‘ Where Name Like : sWildCardText order by createdDate DESC LIMIT 5’;
for (sObject obj: database.query(sQuery)) {
returnList.add(obj);
}
return returnList;
}
@AuraEnabled
public static sObject fetchDefaultRecord(string recordId , string sObjectApiName) {
string sRecId = recordId;
string sQuery = ‘Select Id,Name From ‘ + sObjectApiName + ‘ Where Id = : sRecId LIMIT 1’;
for (sObject obj: database.query(sQuery)) {
return obj;
}
return null;
}
}
Hi Raj, did u manage to get this?
was this provided?
No MODULE named markup://c:custom-lookup-lwcfound
Getting the same issue. How did you bypass this ?
If you are clicking on the remove text button (the X button to remove the selected item) and then all of the sudden you see the page refreshes, (usually happens if you use this in a pop up), then add these 2 lines to the end of the handleRemove(event) method:
event.preventDefault();
return false;
Hey guys, I just wanted to add this comment. If using this on a project you’ll want to add a few things for security. The first is you’ll want to use String.escapeSingleQuotes on all of the input to safe-guard against SOQL injections. This is a real security concern. You might think you’re good because how could someone use a lookup for an injection but bear in mind, someone trying to hack your code would go after the API endpoint that your lwc reaches out to and not the front-end form you’ve created. You’ll also want to use WITH SECURITY_ENFORCED to ensure that whoever is using the lookup cannot access any objects or fields disallowed by their permissions. You could also use Security.stripInaccessible, although if you want to modify this code to allow for filtering you will likely need WITH WITH SECURITY_ENFORCED.
An example of this with all three of these is provided below:
String sQuery = ‘SELECT Id, Name FROM ‘ +
String.escapeSingleQuotes(sObjectApiName) +
‘WHERE Name ‘ +
‘LIKE : sWildCardText ‘ +
‘WITH SECURITY_ENFORCED ‘ +
‘ORDER BY createdDate DESC ‘ +
‘LIMIT 5’;
try {
for (sObject obj:
Security.stripInaccessible(
accessType.READABLE,
database.query(sQuery)).getRecords()) {
returnList.add(obj);
}
} catch(Exception ex) {
throw AuraHandledException(‘Security Violation’);
}
The try-catch block is for WITH SECURITY_ENFORCED since it throws an exception when it tries to retrieve a field the current user doesn’t have access to. Bear in mind the Security.stripInaccessible would actually be redundant in that example because an exception would be thrown by WITH SECURITY_ENFORCED, but I wanted to provide an example of both.
Hi Chandrakanth,
Thank you for your post. Can we use this code in our Salesforce organization? Is there any licensing requirement to use it?
Regards,
Daymel