
A reusable lookup in LWC is a crucial component in custom development. Typically, a lookup functions as an autocomplete combobox that searches against a specific database object. In this guide, we will build a dynamic and reusable custom lookup field in Lightning Web Component (LWC).
Key Features of the Custom Reusable Lookup:
- Dynamic Lookup Display – Shows two fields for each item in the search panel.
- Pre-Population Support – Allows pre-populating the lookup field by passing the ID of the selected record.
- Dynamic Object Icon – Displays the related SObject icon, which can be set dynamically.
- Enhanced Search Panel – Shows the object label for each item in the search results.
- Custom Event Handling – Fires a custom event when a value is selected, enabling seamless integration with other LWCs.
- Dependent Lookup Support – Accepts a parent record ID and parent field API name, allowing the creation of dependent lookups.
- Configurable Search Criteria – Enables filtering records based on conditions or additional field values.
- Debounce Mechanism – Implements a delay in search execution to optimize performance and reduce server calls.
- Keyboard Navigation – Supports arrow key navigation and selection within the search results.
- Custom Styling Support – Allows customization of UI elements to match design requirements.
- Multi-Object Lookup Capability – Can be configured to search across multiple objects dynamically.
- User-Friendly Experience – Provides intuitive interactions and real-time search feedback.
This custom reusable lookup is designed for flexibility and can be easily extended or modified based on specific business needs.
Process & Code :
To create a single-select reusable lookup, we will build a Lightning Web Component in VS Code named reusableLookup.
In the reusableLookup.html file, the structure is based on the SLDS lookup component. It consists of three key sections: the selected state, where a record is already chosen; the unselected state, where no record is selected yet; and the search panel, which displays the results dynamically as the user types.
reusableLookup.html
<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 JavaScript file of the LWC includes the following key elements:
- Public Parameters – Exposes required properties as public to allow dynamic configuration.
- Event Handlers – Implements private methods to manage various user interactions with the input field.
- Search Logic – Handles querying and filtering results based on user input.
- Selection Management – Manages selection and deselection of lookup values.
- Custom Event Dispatching – Fires events to communicate selected values to parent components.
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
<?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 – Implementing the Reusable Lookup in LWC
The reusable lookup component can be integrated into any other LWC. To demonstrate its usage, we will create a new Lightning Web Component named demoLookup and call the reusable lookup within it.
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
The handleValueSelectedOnAccount method is responsible for managing the valueselected event triggered by the reusable lookup component. Key functionalities include:
- Event Handling – Captures the selected value event from the reusable lookup.
- Record Storage – Stores the selected record details in parentAccountSelectedRecord.
- Data Structure – Holds essential record information, including the ID, main field value, and sub-field value.
import { LightningElement } from ‘lwc’;
export default class DemoLookup extends LightningElement {
parentAccountSelectedRecord;
handleValueSelectedOnAccount(event) {
this.parentAccountSelectedRecord = event.detail;
}
}
The component has been added to the home page within a tab component. A demo screenshot is shown below.
See how FieldAx can transform your Field Operations.
Try it today! Book Demo
You are one click away from your customized FieldAx Demo!
Conclusion :
A reusable lookup in LWC enhances search efficiency with dynamic display and intuitive user interactions. It supports pre-population, dependent lookups, and multi-object searches for greater flexibility. Performance optimization is ensured through debounce mechanisms and seamless keyboard navigation. Custom styling and event handling allow easy integration into various business processes. This powerful component improves data accuracy and enhances the overall Salesforce user experience.