How to create a single select and searchable dropdown component in LWC

This component can help you create a single select dropdown by just passing the required data. You can add this as a child component to your main component. I created this component to create a searchable dropdown of State, City, Town subselection to create a dynamic searchable option for the end-user to add location details.

Behind this component, I have created a custom object named “City Master” that has State, City, Town data saved. So when we search and select the State from dropdown, city data from the records having the selected state will be populated in the City dropdown. Similarly, when the city is selected, the town list is populated. Later adding the data we can create a new address record on the Address custom object.

Single select dropdown component in LWC
LWC Component in a glance

SingleSelectLookup HTML File

<template>  
    <div class="slds-form-element">  
      <label class="slds-form-element__label" for="combobox-id-2"><abbr class="slds-required">*</abbr>{lookupLabel}</label>  
      <div class="slds-form-element__control">  
        <div class="slds-combobox_container">  
          <div class="slds-combobox slds-dropdown-trigger slds-dropdown-trigger_click slds-is-open" aria-expanded="true" aria-haspopup="listbox" role="combobox">  
            <template if:true={selectedValue}>  
            <div data-key="pilldiv" 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="object">  
               <div class="slds-icon slds-icon_small" aria-hidden="true">  
                <lightning-icon icon-name={iconName} size="small"></lightning-icon>  
               </div>  
               <span class="slds-assistive-text">Record</span>  
              </span>  
              <input type="text" class="slds-input slds-combobox__input slds-combobox__input-value" id="combobox-id-5" aria-controls="listbox-id-5" role="textbox" placeholder="Select an Option" readonly value={selectedValue} />  
              <button class="slds-button slds-button_icon slds-input__icon slds-input__icon_right" onclick={removeRecordOnLookup}  
              title="Remove selected option">  
               <span class="slds-button__icon" aria-hidden="true" >  
                <lightning-icon icon-name="utility:close"   
                 size="xx-Small" class="slds-icon slds-icon slds-icon_x-small slds-icon-text-default" aria-hidden="true"></lightning-icon>  
               </span>  
               <span class="slds-assistive-text">Remove selected record</span>  
              </button>  
             </div>  
            </template>  
    
            <template if:false={selectedValue}>  
            <div data-key="searchdiv" class="slds-combobox__form-element slds-input-has-icon slds-input-has-icon_right" role="none">  
              <input type="text" onfocusout={onLeave} value={searchKey} onkeyup={handleKeyChange} onchange={handleKeyChange} class="slds-input slds-combobox__input slds-has-focus" id="combobox-id-2" aria-autocomplete="list" aria-controls="listbox-id-2" role="textbox" placeholder="Search..." />  
              <span class="slds-icon_container slds-icon-utility-search slds-input__icon slds-input__icon_right">  
                <lightning-icon icon-name="utility:search" size="xx-Small" class="slds-icon slds-icon slds-icon_x-small slds-icon-text-default" aria-hidden="true"></lightning-icon>  
              </span>  
            </div>  
    
            <template if:true={recordsList}>  
            <div id="listbox-id-2-venu" data-key="dropdownresult" class="slds-show slds-dropdown slds-dropdown_length-with-icon-7 slds-dropdown_fluid" role="listbox">  
              <ul class="slds-listbox slds-listbox_vertical" role="presentation" >  
                <template if:true={message}>  
                 {message}</center>  
                </template>  
                <template for:each={recordsList} for:item="record">  
                  <li id={record.Id} key={record.Id} onclick={onRecordSelection} role="presentation" class="slds-listbox__item">  
                  <div data-key={record.Id} data-name={record.Name} 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">  
                      <span class="slds-icon_container">  
                        <lightning-icon icon-name={iconName} size="small"></lightning-icon>  
                      </span>  
                    </span>  
                    <span class="slds-media__body">  
                      <span data-key={record.Id} data-name={record.Name} class="slds-listbox__option-text slds-listbox__option-text_entity">{record.Name} </span>  
                    </span>  
                  </div>  
                </li>  
                </template>  
              </ul>  
            </div>  
            </template>  
            </template>  
          </div>  
        </div>  
      </div>  
    </div>  
  </template>

JS File

import { LightningElement, track, api } from "lwc";  
import findRecords from "@salesforce/apex/SingleLookupController.findRecords";  
export default class LwcLookup extends LightningElement {  
 @track recordsList;  
 @track searchKey = "";  
 @api selectedValue;  
 @api selectedRecordId;  
 @api objectApiName;  
 @api fieldApiName;  
 @api filterCond;  
 @api filterCity;
 @api iconName;  
 @api lookupLabel;  
 @track message;  
   
 onLeave(event) {  
  setTimeout(() => {  
   this.searchKey = "";  
   this.recordsList = null;  
  }, 300);  
 }  
   
 onRecordSelection(event) {  
  this.selectedRecordId = event.target.dataset.key;  
  this.selectedValue = event.target.dataset.name;  
  this.searchKey = "";  
  this.onSeletedRecordUpdate();  
 }  
  
 handleKeyChange(event) {  
   
  const searchKey = event.target.value;  
  this.searchKey = searchKey;  
  this.getLookupResult();  

 }  
  
 removeRecordOnLookup(event) {  
  this.searchKey = "";  
  this.selectedValue = null;  
  this.selectedRecordId = null;  
  this.recordsList = null;  
  this.onSeletedRecordUpdate();  
} 
getLookupResult() {  
findRecords({ searchKey: this.searchKey, objectName : this.objectApiName ,fieldAPI : this.fieldApiName, filterCond : this.filterCond, filterCity : this.filterCity})  
     .then((result) => {  
      if (result.length===0) {  
        this.recordsList = [];  
        this.message = "No Records Found";  
       } else {  
        this.recordsList = result;  
        this.message = "";  
       }  
       this.error = undefined; 
     })  
     .catch((error) => {  
      this.error = error; 
      this.recordsList = undefined;  
     });  
   }  
    
   onSeletedRecordUpdate(){  
     
    const passEventr = new CustomEvent('recordselection', {  
      detail: { selectedRecordId: this.selectedRecordId, selectedValue: this.selectedValue }  
     });  
     this.dispatchEvent(passEventr);  
   }  
  }

Meta XML File

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>52.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordPage</target>
        <target>lightning__RecordAction</target>



    </targets>
</LightningComponentBundle>

Apex Controller

public class SingleLookupController {
    
@AuraEnabled(cacheable=true)  
public static List<sobject> findRecords(String searchKey,String objectName,String fieldAPI,String filterCond){  
        List<City_Master__c> returnList= new List<City_Master__c>();
        string searchText = '\'' + String.escapeSingleQuotes(searchKey) + '%\'';  
        if(objectName=='City_Master__c'){
            if(fieldAPI=='State__c'){
                List<City_Master__c> stateList= new List<City_Master__c>();
                string query = 'SELECT Id,City__c,State__c,Town__c FROM ' +objectName+ ' WHERE State__c LIKE '+searchText+' LIMIT 6';  
                List<City_Master__c> addrList= Database.query(query);  
                Set<String> stateSet = new Set<String>();
                List<id> idSet = new List<id>();
                
                for(City_Master__c addr:addrList) {
                    stateSet.add(addr.State__c);
                    idSet.add(addr.Id);
                }
                
                for(String state:stateSet) {
                    City_Master__c newAddr = new City_Master__c();
                    newAddr.Id = idSet[0];
                    newAddr.Name = state;
                    stateList.add(newAddr);
                }
                returnList=stateList;
            }
            else if(fieldAPI=='City__c'){
                List<City_Master__c> cityList= new List<City_Master__c>();
                string query = 'SELECT Id,City__c,State__c,Town__c FROM ' +objectName+' WHERE State__c = :filterCond AND City__c LIKE '+searchText+'  LIMIT 6 ';  
                
                List<City_Master__c> addrList= Database.query(query);  
                System.debug('addrList'+addrList);
                Set<String> CitySet = new Set<String>();
                List<id> idSet = new List<id>();
                
                for(City_Master__c addr:addrList) {
                    CitySet.add(addr.City__c);
                    idSet.add(addr.Id);
                }
                
                for(String city:CitySet) {
                    City_Master__c newAddr = new City_Master__c();
                    newAddr.Id = idSet[0];
                    newAddr.Name = city;
                    cityList.add(newAddr);
                }
                
                returnList=cityList;
                
            }
            else if(fieldAPI=='Town__c'){
                List<City_Master__c> townList= new List<City_Master__c>();
                string query = 'SELECT Id,City__c,State__c,Town__c FROM ' +objectName+  ' WHERE City__c = :filterCond AND Town__c LIKE '+searchText+'  LIMIT 6 ';                  List<City_Master__c> addrList= Database.query(query);  
                Set<String> TownSet = new Set<String>();
                List<id> idSet = new List<id>();
                
                for(City_Master__c addr:addrList) {
                    TownSet.add(addr.Town__c);
                    idSet.add(addr.Id);
                }
                
                for(String town:TownSet) {
                    City_Master__c newAddr = new City_Master__c();
                    newAddr.Id = idSet[0];
                    newAddr.Name = town;
                    townList.add(newAddr);
                }
                
                returnList=townList;
                
            }
            return returnList;
        }
        else{
            string query = 'SELECT Id, Name FROM ' +objectName+ ' WHERE Name LIKE '+searchText+' LIMIT 6';  
            return Database.query(query);  
        }
    }
 	
}

Parent LWC HTML to add the component

In this component, we have used the above single select lookup as a child component to populate state, city, and town. “<c-single-select-lookup” to embed the component. Understand the ‘-‘ used to segregate the capital letters in the name of the component “singleSelectLookup”.

Parent HTML

<div class="slds-p-top_x-small">
    <lightning-button class="slds-p-top_x-small"  label="Add New Address" onclick={handleNewAdd}>
    </lightning-button>

	<template if:true={newaddress}>
	<lightning-input type="text" label="Address" onchange={typedAddress} disabled={disable} >
        </lightning-input>

	<template if:true={typeaddress}>   
	<c-single-select-lookup lookup-label="State" object-api-name="City_Master__c"  field-api-name ="State__c" filter-cond="" onrecordselection={onStateSelection} onselect={handleChangeRealted} disabled={disable} aria-required="true"></c-single-select-lookup>    
	</template>

	<template if:true={newselectedstate}> 
		<c-single-select-lookup lookup-label="City" object-api-name="City_Master__c"  field-api-name ="City__c" filter-cond={statevalue}
		onrecordselection={onCitySelection} onselect={handleChangeRealtedCity} disabled={disable} aria-required="true"></c-single-select-lookup>
	</template>

	<template if:true={newselectedcity}> 
	<c-single-select-lookup lookup-label="Town" object-api-name="City_Master__c"  field-api-name ="Town__c" filter-cond={cityvalue}
		onrecordselection={ontownSelection} onselect={handleChangeRealtedCity} disabled={disable} aria-required="true"></c-single-select-lookup>
	</template>

	<template if:true={newselectedtown}> 
	   <lightning-input type="number" value={pincode} onchange={typedPin} label="Pincode"  disabled={disable}></lightning-input>
	   <lightning-button variant="brand" label="Create Address" title="Create Address" onclick={handlenew} disabled={disable}></lightning-button>

	</template>
            </template>
            </div>

Parent JS

import { LightningElement,track,api,wire} from 'lwc';
import getAddresses from '@salesforce/apex/AddressCardController.getAddresses';
import Address__c from "@salesforce/schema/Address__c";
import State__c from "@salesforce/schema/Address__c.State__c";
import City__c from "@salesforce/schema/Address__c.City__c";
import Town__c from "@salesforce/schema/Address__c.Town__c";
import Address__c from "@salesforce/schema/Address__c.Address__c";
import Pincode__c from "@salesforce/schema/Address__c.Pincode__c";
import _c from "@salesforce/schema/Address__c._c";
import { createRecord } from "lightning/uiRecordApi";
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
export default class Address extends LightningElement {
    @api recordId;
    @track options = [];
    @track adds = [];
    error;
    @track newaddress=false;
    @track typeaddress=false;
    @track oldaddress=true;
    @track disable = false;
    @track value;


    @track newselectedstate=false; 
    @track newselectedcity=false;
    @track newselectedtown=false;

    @track statevalue;
    @track cityvalue;
    @track townvalue;

    stateid;
    cityid;
    townid;
    add;
    pin;
    @track newrecid;
    oldrecid;


//add new address button  
handleNewAdd(){  
    this.newaddress = true; 

}


onStateSelection(event){
    this.statevalue = event.detail.selectedValue;  
    if(this.statevalue!=null){
        this.newselectedstate = true;  
    }
    this.stateid = event.detail.selectedRecordId; 
}

onCitySelection(event){
    this.cityvalue = event.detail.selectedValue;  
    if(this.cityvalue!=null){
        this.newselectedcity = true;  
    }
    this.cityid = event.detail.selectedRecordId; 
}
ontownSelection(event){
    this.townvalue = event.detail.selectedValue;
    if(this.townvalue!=null){
        this.newselectedtown = true;  
    }
    this.townid = event.detail.selectedRecordId; 
}

typedAddress(event){
    this.typeaddress=true;
    var add=event.target.value;    
    this.add = add;
}

typedPin(event){
    var pin=event.target.value;    
    console.log(pin);
    this.pin = pin;
}

handlenew() {
    this.disable=true;
try{
 const fields = {};
 fields[State__c.fieldApiName] = this.statevalue;
 fields[City__c.fieldApiName] = this.cityvalue;
 fields[Town__c.fieldApiName] = this.townvalue;
 fields[Address__c.fieldApiName] = this.add;
 fields[Pincode__c.fieldApiName] = this.pin;
 fields[_c.fieldApiName] = this.recordId;

     //Prepare config object with object and field API names 
 const recordInput = {
   apiName: Address__c.objectApiName,
   fields: fields
 };
  

     // Invoke createRecord by passing the config object
 createRecord(recordInput).then((record) => {
    console.log('rec id'+record.id);
   this.newrecid = record.id;
  console.log('newrecid'+this.newrecid);
 if( this.newrecid!= null){    
    this.oldrecid=undefined;
    const evt = new ShowToastEvent({
        title: '',
        message: 'New Address saved successfully',
        variant: 'success',
        mode: 'dismissable'
    });
    this.dispatchEvent(evt);
    this.lookupAdd();
    }
    });

}catch(err) {
    console.log(err.message);
  }
 
}

//**this method is passing the created address record id to another component not part of this lesson //
lookupAdd(){ 
        if (this.newrecid!=null){
            const selectedEvent = new CustomEvent('selectedaddress', {
                detail: this.newrecid
            });
      // Dispatches the event.
            this.dispatchEvent(selectedEvent);
        }
    } 
   
}

Parent Meta XML

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>52.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordAction</target>
    </targets>
     <targetConfigs>
    <targetConfig targets="lightning__RecordAction">
      <actionType>ScreenAction</actionType>
    </targetConfig>
  </targetConfigs>
  </LightningComponentBundle>
Address Record save with Toast Message on LWC
Address Record save with Toast Message on LWC

Check This Post: Dynamic Show and Hide Button in LWC

Also Refer: How to create a multi-select dropdown in LWC

References here

Hope this post makes your day a tad bit easier. Lemme know in the comments below.

1 thought on “How to create a single select and searchable dropdown component in LWC”

Leave a Reply

error: Content is protected !!