Being a developer, It is always helpful to have reusable components (code). The reusable components can save us some time, and reduce bugs in our code. We can reuse them when we got similar requirements for another client. It is like having a personal component library as salesforce provides us to build a lightning experience.
To build a custom user experience sometimes we need a Custom Lookup component for the lookup field or any customization. All projects have unique requirements and need custom development. From my experiences, I have created a reusable custom lookup component with the most commonly used features.
Key Features:
- It is an LWC component so we can use it on Aura and LWC framework.
- Reusable component.
- It is a dynamic component. You can change sObject as per requirement.
- Initial value
- Can add where condition for lookup field(will work as a lookup filter)
Apex Class: (CustomLookupCmpController)
/******************************************************************
Name : CustomAdvanceLookupCmpController
Author : Akhil Mandia
Date : 11/17/2022
Description : This Class is used as controller for CustomLookupCmp lWC.
*******************************************************************/
public with sharing class CustomLookupCmpController {
@AuraEnabled(cacheable=true)
public static List<sObject> getLookupValues( String searchKeyWord,
String objectAPIName,
String whereCondition,
String fieldNames,
Integer customLimit ) {
String searchKey = ‘%’+ searchKeyWord + ‘%’;
List<sObject> returnList = new List<sObject>();
String sQuery = ‘SELECT Id, Name’;
if(String.isNotBlank(fieldNames) && !fieldNames.equalsIgnoreCase(‘Name’)) {
sQuery += ‘,’+fieldNames;
}
if(String.isNotBlank(whereCondition) && whereCondition != null){
sQuery += ‘ FROM ‘ +objectAPIName + ‘ WHERE ‘ + fieldNames + ‘ LIKE \”+searchKey+’\’ AND ‘ + whereCondition + ‘ ORDER BY CreatedDate DESC limit ‘ + String.valueOf(customLimit);
} else {
sQuery += ‘ FROM ‘ +objectAPIName + ‘ WHERE ‘ + fieldNames + ‘ LIKE \”+searchKey+’\’ ORDER BY CreatedDate DESC limit ‘ + String.valueOf(customLimit);
}
system.debug(sQuery) ;
List<sObject> lstOfRecords = Database.query(sQuery);
for (sObject obj: lstOfRecords) { returnList.add(obj); }
System.debug(lstOfRecords) ;
return returnList;
}
@AuraEnabled(cacheable=true)
public static sObject getinitRecord(String recordId, String objectAPIName, String fieldNames) {
String sRecId = recordId;
String sQuery = ‘SELECT id, Name’;
if(String.isNotBlank(fieldNames)) {
sQuery += ‘,’+fieldNames;
}
sQuery += ‘ FROM ‘ + objectAPIName + ‘ WHERE Id = : sRecId LIMIT 1 ‘;
for (sObject obj: Database.query(sQuery)) {
return obj;
}
return null;
}
@AuraEnabled(cacheable=true)
public static List<sObject> gerRecentlyCreatedRecords( String objectAPIName,
String fieldNames,
String whereCondition,
Integer customLimit){
List<sObject> returnList = new List<sObject>();
String sQuery = ‘SELECT Id, Name’;
if(String.isNotBlank(fieldNames) && !fieldNames.equalsIgnoreCase(‘Name’)) {
sQuery += ‘,’+fieldNames;
}
if(String.isNotBlank(whereCondition) && whereCondition != null){
sQuery += ‘ FROM ‘ +objectAPIName + ‘ WHERE ‘ + whereCondition + ‘ ORDER BY CreatedDate DESC limit ‘ + String.valueOf(customLimit);
} else {
sQuery += ‘ FROM ‘ +objectAPIName + ‘ ORDER BY CreatedDate DESC limit ‘ + String.valueOf(customLimit);
}
System.debug(sQuery);
List<sObject> lstOfRecords = Database.query(sQuery);
for (sObject obj: lstOfRecords) { returnList.add(obj); }
return returnList;
}
}
LWC Component: (customLookupCmp)
customLookupCmp.html
<template>
<div class=“c-container” onmouseleave={handleOnblur}>
<div class=“custom-lookup slds-form-element”>
<template if:false={labelHidden}>
<label class=“slds-form-element__label”>{labelForComponent}</label>
</template>
<div class=“slds-form-element__control”>
<div class=“slds-combobox_container”>
<div
class=“custom-lookup-container 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”>
<div class=“search-input-container slds-show”>
<lightning-input type=“text” onclick={handleClickOnInputBox} onchange={handleKeyChange}
is-loading={isSearchLoading} value={searchKeyWord} variant=“label-hidden”
placeholder={placeholder}></lightning-input>
<span
class=“slds-icon_container slds-icon-utility-search slds-input__icon slds-input__icon_right”>
<lightning-icon class=“slds-icon slds-icon slds-icon_x-small slds-icon-text-default”
icon-name=“utility:search” size=“x-small” alternative-text=“icon”>
</lightning-icon>
</span>
</div>
<div class=“custom-lookup-pill slds-pill-container slds-hide” role=“none”>
<span
class=“slds-icon_container slds-icon-standard-account slds-combobox__input-entity-icon”
title=“Account”>
<lightning-icon class=“slds-icon slds-icon slds-icon_small” icon-name={iconName}
size=“small” alternative-text=“icon”>
</lightning-icon>
<span class=“slds-assistive-text”>Account</span>
</span>
<button type=“button”
class=“slds-input_faux slds-combobox__input slds-combobox__input-value”
aria-labelledby=“combobox-label-id-33 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-20”>{selectedRecordLabel}</span>
</button>
<button class=“slds-button slds-button_icon slds-input__icon slds-input__icon_right”
title=“Remove selected option” onclick={clearSelection}>
<lightning-icon class=“slds-icon slds-icon slds-icon_x-small slds-icon-text-default”
icon-name=“utility:close” size=“x-small” alternative-text=“icon”>
</lightning-icon>
<span class=“slds-assistive-text”>Remove selected option</span>
</button>
</div>
<!–This part is for Display typehead lookup result List–>
<div class=“slds-dropdown slds-dropdown_length-with-icon-7 slds-dropdown_fluid”
role=“listbox”>
<template if:true={spinnerShow}>
<div class=“spinner-holder”>
<lightning-spinner alternative-text=“Loading” size=“small”></lightning-spinner>
</div>
</template>
<ul style=“min-height:40px;margin-top:0px !important”
class=“slds-listbox slds-listbox_vertical” role=“presentation”>
<template for:each={searchRecordList} for:item=“rec”>
<li key={rec.Id} role=“presentation” class=“slds-listbox__item”>
<div id={rec.Id} data-recid={rec.Id} onclick={handleSelectionRecord}
class=“slds-media slds-listbox__option slds-listbox__option_entity”
role=“option” style=“align-items: center;”>
<span style=“pointer-events: none;”
class=“slds-media__figure slds-listbox__option-icon”>
<lightning-icon class=“slds-icon slds-icon slds-icon_small “
icon-name={iconName} size=“small” alternative-text=“icon”>
</lightning-icon>
</span>
<span style=“pointer-events: none;” class=“slds-media__body”>
<span class=“slds-listbox__option-text_entity”>{rec.Name}</span>
</span>
</div>
</li>
</template>
<template if:true={noRecordFound}>
<li role=“presentation” class=“slds-listbox__item”>
<div class=“slds-media slds-listbox__option slds-listbox__option_entity”
role=“option” style=“align-items: center;”>
<span style=“pointer-events: none;” class=“slds-media__body”>
<span class=“slds-listbox__option-text_entity”>No Records
Found….</span>
</span>
</div>
</li>
</template>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
customLookupCmp.js
import { api, wire, LightningElement } from ‘lwc’;
import getLookupValues from ‘@salesforce/apex/CustomLookupCmpController.getLookupValues’;
import getinitRecord from ‘@salesforce/apex/CustomLookupCmpController.getinitRecord’;
import gerRecentlyCreatedRecords from ‘@salesforce/apex/CustomLookupCmpController.gerRecentlyCreatedRecords’;
export default class CustomLookupCmp extends LightningElement {
//public properties
@api uniqueName = ‘Account’;
@api initialLookupValue = ”;
@api objectAPIName = ‘Account’;
displayLabelField = ‘Name’;
@api iconName = ‘standard:account’;
@api labelForComponent = ‘Account’
@api placeHolder = ‘Search Account’
@api recordLimit = 5;
@api labelHidden = false;
searchKeyWord = ”;
@api selectedRecord = {}; // Use, for store SELECTED sObject Record
@api where = ”;
// private properties
selectedRecordLabel = ”;
searchRecordList = []; // Use,for store the list of search records which returns from apex class
message = ”;
spinnerShow = false;
error = ”;
noRecordFound = false;
@wire(getLookupValues, { searchKeyWord: ‘$searchKeyWord’, objectAPIName: ‘$objectAPIName’, whereCondition: ‘$where’, fieldNames: ‘$displayLabelField’, customLimit: ‘$recordLimit’ })
wiredsearchRecordList({ error, data }) {
this.spinnerShow = true;
if (data) {
this.spinnerShow = false;
this.searchRecordList = JSON.parse(JSON.stringify(data));
this.error = undefined;
this.hasRecord();
} else if (error) {
console.log(‘getLookupValues Error 2 —> ‘ + JSON.stringify(error));
this.hasRecord();
this.error = error;
this.searchRecordList = undefined;
}
}
connectedCallback() {
if (this.initialLookupValue != ”) {
getinitRecord({ recordId: this.initialLookupValue, ‘objectAPIName’: this.objectAPIName, ‘fieldNames’: this.displayLabelField })
.then((data) => {
if (data != null) {
console.log(‘getinitRecord —> ‘, JSON.stringify(data));
this.selectedRecord = data;
this.selectedRecordLabel = data.Name; //data[this.displayLabelField];
this.selectionRecordHelper();
}
})
.catch((error) => {
console.log(‘getinitRecord Error —> ‘ + JSON.stringify(error));
this.error = error;
this.selectedRecord = {};
});
}
}
handleClickOnInputBox(event) {
let container = this.template.querySelector(‘.custom-lookup-container’);
container.classList.add(‘slds-is-open’);
this.spinnerShow = true;
console.log(this.where);
if (typeof this.searchKeyWord === ‘string’ && this.searchKeyWord.trim().length === 0) {
gerRecentlyCreatedRecords({ ‘objectAPIName’: this.objectAPIName, ‘fieldNames’: this.displayLabelField, ‘whereCondition’: this.where, ‘customLimit’: this.recordLimit })
.then((data) => {
if (data != null) {
try {
console.log(‘gerRecentlyCreatedRecords —> ‘, JSON.stringify(data));
this.spinnerShow = false;
this.searchRecordList = JSON.parse(JSON.stringify(data));
this.hasRecord();
} catch (error) {
console.log(error);
this.hasRecord();
}
}
})
.catch((error) => {
console.log(‘gerRecentlyCreatedRecords Error —> ‘ + JSON.stringify(error));
this.error = error;
});
} else if (typeof this.searchKeyWord === ‘string’ && this.searchKeyWord.trim().length > 0) {
let temp = this.searchKeyWord
this.searchKeyWord = temp;
getLookupValues({ ‘searchKeyWord’: this.searchKeyWord, ‘objectAPIName’: this.objectAPIName, ‘whereCondition’: this.where, ‘fieldNames’: this.displayLabelField, ‘customLimit’: this.recordLimit })
.then((data) => {
if (data != null) {
console.log(‘getLookupValues —> ‘, JSON.stringify(data));
this.spinnerShow = false;
this.searchRecordList = JSON.parse(JSON.stringify(data));
this.error = undefined;
this.hasRecord();
}
})
.catch((error) => {
console.log(‘getLookupValues Error —> ‘ + JSON.stringify(error));
this.error = error;
this.selectedRecord = {};
});
}
}
fireLookupUpdateEvent(value) {
const oEvent = new CustomEvent(‘customLookupUpdateEvent’,
{
‘detail’: {
‘name’: this.uniqueName,
‘selectedRecord’: value
}
}
);
this.dispatchEvent(oEvent);
}
handleKeyChange(event) {
this.searchKeyWord = event.detail.value;
console.log(this.searchKeyWord);
if (typeof this.searchKeyWord === ‘string’ && this.searchKeyWord.trim().length > 0) {
this.searchRecordList = [];
}
}
handleOnblur(event) {
let container = this.template.querySelector(‘.custom-lookup-container’);
container.classList.remove(‘slds-is-open’);
this.spinnerShow = false;
this.searchRecordList = [];
}
handleSelectionRecord(event) {
var recid = event.target.getAttribute(‘data-recid’);
console.log(‘recid : ‘, recid);
let container = this.template.querySelector(‘.custom-lookup-container’);
container.classList.remove(‘slds-is-open’);
this.selectedRecord = this.searchRecordList.find(data => data.Id === recid);
this.selectedRecordLabel = this.selectedRecord.Name;//this.selectedRecord[this.displayLabelField];
console.log(this.selectedRecord);
console.log(this.selectedRecordLabel);
this.fireLookupUpdateEvent(this.selectedRecord);
this.selectionRecordHelper();
}
selectionRecordHelper() {
let custom_lookup_pill_container = this.template.querySelector(‘.custom-lookup-pill’);
custom_lookup_pill_container.classList.remove(‘slds-hide’);
custom_lookup_pill_container.classList.add(‘slds-show’);
let search_input_container_container = this.template.querySelector(‘.search-input-container’);
search_input_container_container.classList.remove(‘slds-show’);
search_input_container_container.classList.add(‘slds-hide’);
}
clearSelection() {
let custom_lookup_pill_container = this.template.querySelector(‘.custom-lookup-pill’);
custom_lookup_pill_container.classList.remove(‘slds-show’);
custom_lookup_pill_container.classList.add(‘slds-hide’);
let search_input_container_container = this.template.querySelector(‘.search-input-container’);
search_input_container_container.classList.remove(‘slds-hide’);
search_input_container_container.classList.add(‘slds-show’);
this.fireLookupUpdateEvent(undefined);
this.clearSelectionHelper();
}
clearSelectionHelper() {
this.selectedRecord = {};
this.selectedRecordLabel = ”;
this.searchKeyWord = ”;
this.searchRecordList = [];
}
hasRecord() {
if (this.searchRecordList && this.searchRecordList.length > 0) {
this.noRecordFound = false;
} else {
this.noRecordFound = true;
}
}
}
customLookupCmp.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>
<target>lightningCommunity__Default</target>
<target>lightningCommunity__Page</target>
<target>lightning__Tab</target>
<target>lightning__Inbox</target>
</targets>
</LightningComponentBundle>
How to use the LWC component in Aura framework?
The following code can be used as reference for how we can use a LWC component in aura framework..
TestApp.app
<aura:application extends=”force:slds”>
<aura:attribute name=”selectedRecord” type=”object” access=”private” />
<lightning:card title=”Narrow Card Header”>
<p class=”slds-p-around_large”>
<c:customLookupCmp uniqueName=”MyUser”
objectAPIName=”User”
labelHidden=”true”
where=” isActive = true”
selectedRecord=”{!v.selectedRecord}”
oncustomLookupUpdateEvent=”{!c.handleSelection}”/>
</p>
</lightning:card>
{!v.selectedRecord}
</aura:application>
TestAppController.js
({
handleSelection : function(component, event, helper) {
try {
var name = event.getParam(‘name’);
var selectedRecord = event.getParam(‘selectedRecord’);
console.log(name);
console.log(selectedRecord.Id);
} catch (err) {
console.log(err);
}
}
})
Akhil Mandia
Salesforce Developer
Akhil, one of our sophisticated developers, holds a deep passion for coding and software development. He has a background in website development and has continued to grow his passion in the Salesforce Ohana as a Salesforce Developer.
About Roycon
We’re an Austin-based Salesforce Consulting Partner, with a passion and belief that the Salesforce platform’s capabilities can help businesses run more efficiently and effectively. Whether you are just getting started with Salesforce or looking to realize its full potential, Roycon specializes in Salesforce Implementations, Salesforce Ongoing Support, and Salesforce Integrations, and Development. We’re the certified partner to guide the way to increase Salesforce Adoption, make strategic decisions, and build your Salesforce Roadmap for success.