Create Reusable Custom Lookup in (LWC) Salesforce

by Rijwan Mohmmed
create-reusable-custom-lookup-in-lwc-salesforce

We are going to create Reusable Custom Lookup Component in Lightning Web Component. if you need to bypass the current user’s sharing and security or you need more flexibility then use this custom approach.

Also check this: Upload files in LWC Salesforce

Key Highlights :

  1. Reusable in any component.
  2. Fully Dynamic
  3. We can set default values
  4. All methods are very simple

Process & Code :

CustomLookup.cls : This class is used for search records and return a list of sobject records. Here we create two methods one for fetching the value on the lookup search and the other is getting records with default values.

public without sharing class CustomLookup {
    
    @AuraEnabled(cacheable=true)
    public static list<sObject> searchLookupData(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 searchDefaultRecord(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;
    }
}


customLookup.Html :

<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" if:false={isValueSelected}>
                <!--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" if:true={isValueSelected}>        
              <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>

customLookup.JS :

import { LightningElement,api,wire} from 'lwc';
import searchLookupData from '@salesforce/apex/CustomLookup.searchLookupData';
import searchDefaultRecord from '@salesforce/apex/CustomLookup.searchDefaultRecord';

export default class CustomLookup extends LightningElement {
    // public properties with initial default values 
    @api label = 'label';
    @api placeholder = 'search...'; 
    @api iconName = 'standard:contact';
    @api sObjectApiName = 'Contact';
    @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;
    isValueSelected;
    selectedRecord = {}; // to store selected lookup record in object formate 
   // initial function to populate default selected lookup record if defaultRecordId provided  
    connectedCallback(){
         if(this.defaultRecordId != ''){
            searchDefaultRecord({ 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(searchLookupData, { 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) {
        // Do not update the reactive property as long as this function is
        this.isSearchLoading = true;
        window.clearTimeout(this.delayTimeout);
        const searchKey = event.target.value;
        this.delayTimeout = setTimeout(() => {
        this.searchKey = searchKey;
        }, 300);
    }
    // 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.lookupUpdateParenthandler(undefined); // update value on parent component as well from helper function
        this.isValueSelected = false; 
    }
    // 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.lookupUpdateParenthandler(this.selectedRecord); // update value on parent component
        this.handelSelectRecordHelper(); // helper function to show/hide lookup result container on UI
    }
    //handle select record
    handelSelectRecordHelper(){
        this.template.querySelector('.lookupInputContainer').classList.remove('slds-is-open');
        this.isValueSelected = true; 
    }
    // send selected lookup record to parent component using custom event
    lookupUpdateParenthandler(value){
        console.log(value);
        const oEvent = new CustomEvent('lookupupdate',
                                    {
                                        'detail': {selectedRecord: value}
                                    }
                        );
        this.dispatchEvent(oEvent);
    }
}

customLookupParent.html :

<template>
	<div class="slds-tabs_card">
		<div class="slds-page-header">
			<div class="slds-page-header__row">
				<div class="slds-page-header__col-title">
					<div class="slds-media">
						<div class="slds-media__figure">
							<span class="slds-icon_container slds-icon-standard-opportunity">
								 <lightning-icon icon-name="standard:event" alternative-text="Event" title="Event"></lightning-icon>
                            </span>
						</div>
						<div class="slds-media__body">
							<div class="slds-page-header__name">
								<div class="slds-page-header__name-title">
									<h1>
										<span>Custom Reusable Lookup LWC</span>
										<span class="slds-page-header__title slds-truncate" title="Recently Viewed">Lookup LWC</span>
									</h1>
								</div>
							</div>
						</div>
					</div>
				</div>
			</div>
		</div>
	</div> <br/>

	<lightning-card  variant="Narrow"  title="Lookup" icon-name="standard:apps">
		<div class="slds-p-horizontal_small">
			<!--Custom Lookup without Pre-Populate Value for Contact Object-->
			<c-custom-lookup icon-name="standard:contact" s-object-api-name="contact" label="Contact"
				onlookupupdate={handleLookupSelection} placeholder="Search Contact...">
			</c-custom-lookup>
			<br/>

			<!--Custom Lookup with Pre-Populate Value for User Object-->
			<c-custom-lookup icon-name="standard:user" s-object-api-name="user" label="User"
				onlookupupdate={handleLookupSelection} placeholder="Search User here..." default-record-id={userId}>
			</c-custom-lookup>
		</div>
	</lightning-card>
</template>

customLookupParent.JS : In this we get lookup values in handleLookupSelection method.

import { LightningElement } from 'lwc';
import strUserId from '@salesforce/user/Id';
export default class CustomLookupParent extends LightningElement {
    userId = strUserId;

    handleLookupSelection(event){
        if(event.detail.selectedRecord != undefined){
            console.log('Selected Record Value on Parent Component is ' +  
            JSON.stringify(event.detail.selectedRecord));
            alert(event.detail.selectedRecord.Id + ' '+ event.detail.selectedRecord.Name);
        }
    }
}

Output :

What’s your Reaction?
+1
0
+1
2
+1
0
+1
0
+1
0
+1
0

You may also like

Leave a Comment