custom lookup in salesforce lwc

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
custom lookup in salesforce lightning web component output

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

  1. From the App Launcher (App Launcher), find and select Sales.
  2. Click Setup gear then select Edit Page.
  3. Drag the lookupDemo Lightning web component from the Custom area of the Lightning Components list to the top of the Page Canvas.
  4. Click Activate and Assign as Org Default and hit save.
  5. Refresh the page to view your new component.

Custom Lookup Output :

custom lookup output

Resources :

For further reading:

Happy coding!

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;
    }

    }

  • 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

Leave a Reply