Reusable Lookup in LWC are one of the important element in custom development. Generally, Lookup is an autocomplete combobox that will search against a database object. We are going to build a custom dynamic reusable Lookup field in Lightning Web Component. Some of the key features are:-
1. Dynamic Lookup Component shows two fields for each item in search panel
2. You can pre-populate the lookup field by just passing the id of the selected record
3. It shows the Icon of the related SObject and can be set dynamically
4. Custom Lookup also shows you the object label in search panel for each item
5. This Reusable Lookup can be extended with other LWC as it fires a custom event whenever a value is selected so that consumer can handle it
6. It accepts parent record id and parent field api name so that you can build dependent lookups.
Code for Single Select Reusable Lookup
Let’s create a Lightning Web Component in VS Code with name reusableLookup.
reusableLookup.html
The html file of the reusable lookup component has code derived from slds-lookup. It has three main parts.

1. When lookup is in selected state
2. When lookup is not in selected state
3. When Lookup shows result in serach panel
<template>
<div class="slds-form-element">
<div class="slds-form-element__control">
<div class="slds-combobox_container" if:false={isValueSelected}>
<div class="slds-combobox slds-dropdown-trigger slds-dropdown-trigger_click slds-is-open">
<div class="slds-combobox__form-element slds-input-has-icon slds-input-has-icon_right" role="none">
<lightning-input onchange={handleChange} type="search" autocomplete="off" label={label}
required={required} field-level-help={helpText} placeholder={placeholder}
onblur={handleInputBlur}></lightning-input>
</div>
</div>
</div>
<template if:true={isValueSelected}>
<label class="slds-form-element__label" for="combobox-id-5" id="combobox-label-id-35">{label}</label>
<template if:true={required}>
<span style="color:red">*</span>
</template>
<div tabindex="0" class="slds-combobox_container slds-has-selection">
<div class="slds-combobox slds-dropdown-trigger slds-dropdown-trigger_click">
<div class="slds-combobox__form-element slds-input-has-icon slds-input-has-icon_left-right"
role="none">
<span
class="slds-icon_container slds-icon-standard-account slds-combobox__input-entity-icon"
title="Account">
<lightning-icon icon-name={selectedIconName} alternative-text={selectedIconName}
size="x-small"></lightning-icon>
</span>
<button type="button"
class="slds-input_faux slds-combobox__input slds-combobox__input-value"
aria-labelledby="combobox-label-id-34 combobox-id-5-selected-value"
id="combobox-id-5-selected-value" aria-controls="listbox-id-5" aria-expanded="false"
aria-haspopup="listbox">
<span class="slds-truncate" id="combobox-value-id-19">{selectedRecordName}</span>
</button>
<button class="slds-button slds-button_icon slds-input__icon slds-input__icon_right"
title="Remove selected option" onclick={handleCommit}>
<lightning-icon icon-name="utility:close" alternative-text="Remove selected option"
size="x-small"></lightning-icon>
</button>
</div>
</div>
</div>
</template>
<template if:true={showRecentRecords}>
<div id="listbox-id-4" tabindex="0" onblur={handleBlur} onmousedown={handleDivClick}
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={recordsList} for:item="rec">
<li role="presentation" key={rec.id} class="slds-listbox__item">
<div onclick={handleSelect} data-id={rec.id} data-mainfield={rec.mainField}
data-subfield={rec.subField}
class="slds-media slds-listbox__option slds-listbox__option_entity slds-listbox__option_has-meta"
role="option">
<span class="slds-media__figure slds-listbox__option-icon">
<lightning-icon icon-name={selectedIconName} alternative-text={selectedIconName}
size="small"></lightning-icon>
</span>
<span class="slds-media__body">
<span class="slds-listbox__option-text slds-listbox__option-text_entity">
<span>
<mark>{rec.mainField}</mark>
</span>
</span>
<span class="slds-listbox__option-meta slds-listbox__option-meta_entity">
{objectLabel} • {rec.subField}
</span>
</span>
</div>
</li>
</template>
</ul>
</div>
</template>
</div>
</div>
</template>
reusableLookup.js
The js file of the lwc has exposed required parameters as public so that they can be set. It also has several private methods to handle the various events on input element.
import { LightningElement, api } from 'lwc';
import fetchRecords from '@salesforce/apex/ReusableLookupController.fetchRecords';
/** The delay used when debouncing event handlers before invoking Apex. */
const DELAY = 500;
export default class ReusableLookup extends LightningElement {
@api helpText = "custom search lookup";
@api label = "Parent Account";
@api required;
@api selectedIconName = "standard:account";
@api objectLabel = "Account";
recordsList = [];
selectedRecordName;
@api objectApiName = "Account";
@api fieldApiName = "Name";
@api otherFieldApiName = "Industry";
@api searchString = "";
@api selectedRecordId = "";
@api parentRecordId;
@api parentFieldApiName;
preventClosingOfSerachPanel = false;
get methodInput() {
return {
objectApiName: this.objectApiName,
fieldApiName: this.fieldApiName,
otherFieldApiName: this.otherFieldApiName,
searchString: this.searchString,
selectedRecordId: this.selectedRecordId,
parentRecordId: this.parentRecordId,
parentFieldApiName: this.parentFieldApiName
};
}
get showRecentRecords() {
if (!this.recordsList) {
return false;
}
return this.recordsList.length > 0;
}
//getting the default selected record
connectedCallback() {
if (this.selectedRecordId) {
this.fetchSobjectRecords(true);
}
}
//call the apex method
fetchSobjectRecords(loadEvent) {
fetchRecords({
inputWrapper: this.methodInput
}).then(result => {
if (loadEvent && result) {
this.selectedRecordName = result[0].mainField;
} else if (result) {
this.recordsList = JSON.parse(JSON.stringify(result));
} else {
this.recordsList = [];
}
}).catch(error => {
console.log(error);
})
}
get isValueSelected() {
return this.selectedRecordId;
}
//handler for calling apex when user change the value in lookup
handleChange(event) {
this.searchString = event.target.value;
this.fetchSobjectRecords(false);
}
//handler for clicking outside the selection panel
handleBlur() {
this.recordsList = [];
this.preventClosingOfSerachPanel = false;
}
//handle the click inside the search panel to prevent it getting closed
handleDivClick() {
this.preventClosingOfSerachPanel = true;
}
//handler for deselection of the selected item
handleCommit() {
this.selectedRecordId = "";
this.selectedRecordName = "";
}
//handler for selection of records from lookup result list
handleSelect(event) {
let selectedRecord = {
mainField: event.currentTarget.dataset.mainfield,
subField: event.currentTarget.dataset.subfield,
id: event.currentTarget.dataset.id
};
this.selectedRecordId = selectedRecord.id;
this.selectedRecordName = selectedRecord.mainField;
this.recordsList = [];
// Creates the event
const selectedEvent = new CustomEvent('valueselected', {
detail: selectedRecord
});
//dispatching the custom event
this.dispatchEvent(selectedEvent);
}
//to close the search panel when clicked outside of search input
handleInputBlur(event) {
// Debouncing this method: Do not actually invoke the Apex call 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.
window.clearTimeout(this.delayTimeout);
// eslint-disable-next-line @lwc/lwc/no-async-operation
this.delayTimeout = setTimeout(() => {
if (!this.preventClosingOfSerachPanel) {
this.recordsList = [];
}
this.preventClosingOfSerachPanel = false;
}, DELAY);
}
}
reusableLookup.js-meta.xml
We have exposed to home, app and record page.
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>55.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__AppPage</target>
<target>lightning__RecordPage</target>
<target>lightning__HomePage</target>
</targets>
</LightningComponentBundle>
Apex Controller of Reusable Lookup – ReusableLookupController
public with sharing class ReusableLookupController {
@AuraEnabled
public static List<ResultWrapper> fetchRecords(SearchWrapper inputWrapper) {
try {
if(inputWrapper != null){
String fieldsToQuery = 'SELECT Id, ';
if(string.isNotBlank(inputWrapper.fieldApiName)){
fieldsToQuery = fieldsToQuery + inputWrapper.fieldApiName;
}
if(string.isNotBlank(inputWrapper.otherFieldApiName)){
fieldsToQuery = fieldsToQuery + ', ' + inputWrapper.otherFieldApiName;
}
String query = fieldsToQuery + ' FROM '+ inputWrapper.objectApiName;
String filterCriteria = inputWrapper.fieldApiName + ' LIKE ' + '\'' + String.escapeSingleQuotes(inputWrapper.searchString.trim()) + '%\' LIMIT 10';
if(String.isNotBlank(inputWrapper.selectedRecordId)) {
query += ' WHERE Id = \''+ inputWrapper.selectedRecordId + '\'';
}else if(String.isNotBlank(inputWrapper.parentFieldApiName) && String.isNotBlank(inputWrapper.parentRecordId)){
query += ' WHERE '+ inputWrapper.parentFieldApiName+ ' = \''+ inputWrapper.parentRecordId + '\'';
query += ' AND ' + filterCriteria;
}
else {
query += ' WHERE '+ filterCriteria;
}
List<ResultWrapper> returnWrapperList = new List<ResultWrapper>();
for(SObject s : Database.query(query)) {
ResultWrapper wrap = new ResultWrapper();
wrap.mainField = (String)s.get(inputWrapper.fieldApiName);
wrap.subField = (String)s.get(inputWrapper.otherFieldApiName);
wrap.id = (String)s.get('id');
returnWrapperList.add(wrap);
}
return returnWrapperList;
}
return null;
} catch (Exception err) {
throw new AuraHandledException(err.getMessage());
}
}
public class ResultWrapper{
@AuraEnabled public String mainField{get;set;}
@AuraEnabled public String subField{get;set;}
@AuraEnabled public String id{get;set;}
}
public class SearchWrapper {
@AuraEnabled public String objectApiName{get;set;}
@AuraEnabled public String fieldApiName{get;set;}
@AuraEnabled public String otherFieldApiName{get;set;}
@AuraEnabled public String searchString{get;set;}
@AuraEnabled public String selectedRecordId{get;set;}
@AuraEnabled public String parentRecordId{get;set;}
@AuraEnabled public String parentFieldApiName{get;set;}
}
}
Demo – Reusable Lookup In LWC
You can call the reusable lwc lookup in any other component as below. Let’s create a LWC with name demoLookup.
demoLookup.html
<template>
<c-reusable-lookup label="Parent Account" selected-icon-name="standard:account" object-label="Account"
object-api-name="Account" field-api-name="Name" other-field-api-name="Industry"
onvalueselected={handleValueSelectedOnAccount}>
</c-reusable-lookup>
</template>
demoLookup.js
handleValueSelectedOnAccount method handles the valueselected event fired from reusable search lookup component. parentAccountSelectedRecord holds the selected record information. It will have id of the record, main field value and sub field value.
import { LightningElement } from 'lwc';
export default class DemoLookup extends LightningElement {
parentAccountSelectedRecord;
handleValueSelectedOnAccount(event) {
this.parentAccountSelectedRecord = event.detail;
}
}
demoLookup.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>55.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__AppPage</target>
<target>lightning__RecordPage</target>
<target>lightning__HomePage</target>
</targets>
</LightningComponentBundle>
We have placed the component to home page under a tab component. You can see the demo below:
Do you need help?
Are you stuck while working on this requirement? Do you want to get review of your solution? Now, you can book dedicated 1:1 with me on Lightning Web Component completely free.
GET IN TOUCH
Schedule a 1:1 Meeting with me
Also check out https://salesforcediaries.com/category/lightning-web-component/ to learn more on LWC.
I am truing to use this component in Experience Builder Site, componnet is loading but connectedCallBack method of the reusable component is not getting called and so data is not getting fetched