How to create a multi-select and searchable component in LWC

This LWC component will help you create a multi-select dropdown in your main LWC component. I created this component to create a searchable dropdown of products by name and add them to the record. The dropdown is dynamic here i.e. it updates using the search term entered. Below is the visual representation of the multi-select, dynamic search child LWC component.

multi select component in lwc
Multi Select Component in LWC

multiSelectLookup HTML File

<template>
    <div class="slds-form-element">
     <label class='slds-m-around_x-small'><abbr class="slds-required">*</abbr>{labelName}</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">
                       <div class="slds-combobox slds-dropdown-trigger slds-dropdown-trigger_click slds-has-focus" 
                           role="none">
      <lightning-input id="input" 
       value={searchInput} onchange={onchangeSearchInput} variant="label-hidden" aria-autocomplete="list" role="textbox"
       autocomplete="off" placeholder="Search..." type="search">
      </lightning-input>
                       </div>
                       <div class="slds-form-element__control slds-input-has-icon slds-input-has-icon slds-input-has-icon_left-right" role="none">
                           <template for:each={globalSelectedItems} for:item="selectedItem">
                               <span key={selectedItem.value}>
                                   <lightning-pill label={selectedItem.label} name={selectedItem.value} data-item={selectedItem.value}
                                       onremove={handleRemoveRecord}>
                                       <lightning-icon icon-name={iconName} variant="circle" 
                                           alternative-text={selectedItem.label}></lightning-icon>
                                   </lightning-pill>
                               </span>
                           </template>                     
                       </div>
                       <template if:true={isDisplayMessage}>
                           <lightning-card>No records found.</lightning-card>
                       </template> 
                       <template if:false={isDisplayMessage}>
                           <template if:true={isDialogDisplay}>
                               <section aria-describedby="dialog-body-id-26" aria-label="Language Options" 
       class="slds-popover slds-popover_full-width" id="popover-unique-id-02" role="dialog">
                                   <div class="slds-popover__body slds-popover__body_small" id="dialog-body-id-26">
                                       <fieldset class="slds-form-element">   
                                           <lightning-checkbox-group name="Checkbox Group"
                                               label={objectAPIName}
                                               options={items}
                                               value={value}
                                               onchange={handleCheckboxChange}>
                                           </lightning-checkbox-group>
                                       </fieldset>
                                   </div>
                                   <footer class="slds-popover__footer slds-popover__footer_form">
                                       <lightning-button label="Cancel" title="Cancel" 
                                               onclick={handleCancelClick} class="slds-m-left_x-small"></lightning-button>
                                       <lightning-button variant="success" label="Done" title="Done"
                                               onclick={handleDoneClick} class="slds-m-left_x-small"></lightning-button>                                
                                   </footer>
                               </section>                                               
                           </template>
                       </template>
                  </div>
             </div>
         </div>
     </div>
   </template>

JS File

import { LightningElement, api, track } from 'lwc';
import retrieveRecords from '@salesforce/apex/MultiSelectLookupController.retrieveRecords';

let i=0;
export default class MultiSelectLookup extends LightningElement {

    @track globalSelectedItems = []; //holds all the selected checkbox items
    //start: following parameters to be passed from calling component
    @api labelName;
    @api objectApiName; // = 'Contact';
    @api fieldApiNames; // = 'Id,Name';
    @api filterFieldApiName;    // = 'Name';
    @api iconName;  // = 'standard:contact';
    //end---->
    @track items = []; //holds all records retrieving from database
    @track selectedItems = []; //holds only selected checkbox items that is being displayed based on search

    //since values on checkbox deselection is difficult to track, so workaround to store previous values.
    //clicking on Done button, first previousSelectedItems items to be deleted and then selectedItems to be added into globalSelectedItems
    @track previousSelectedItems = []; 
    @track value = []; //this holds checkbox values (Ids) which will be shown as selected
    searchInput ='';    //captures the text to be searched from user input
    isDialogDisplay = false; //based on this flag dialog box will be displayed with checkbox items
    isDisplayMessage = false; //to show 'No records found' message
    
    //This method is called when user enters search input. It displays the data from database.
    onchangeSearchInput(event){

        this.searchInput = event.target.value;
        if(this.searchInput.trim().length>0){
            //retrieve records based on search input
            retrieveRecords({objectName: this.objectApiName,
                            fieldAPINames: this.fieldApiNames,
                            filterFieldAPIName: this.filterFieldApiName,
                            strInput: this.searchInput
                            })
            .then(result=>{ 
                console.log('result'+JSON.stringify(result));
                this.items = []; //initialize the array before assigning values coming from apex
                this.value = [];
                this.previousSelectedItems = [];

                if(result.length>0){
                    result.map(resElement=>{
                        console.log(resElement);
                        //prepare items array using spread operator which will be used as checkbox options
                        this.items = [...this.items,{value:resElement.recordId, 
                                                    label:resElement.recordName}];
                        
                        /*since previously choosen items to be retained, so create value array for checkbox group.
                            This value will be directly assigned as checkbox value & will be displayed as checked.
                        */
                        this.globalSelectedItems.map(element =>{
                            if(element.value == resElement.recordId){
                                this.value.push(element.value);
                                this.previousSelectedItems.push(element);                      
                            }
                        });
                    });
                    this.isDialogDisplay = true; //display dialog
                    this.isDisplayMessage = false;
                }
                else{
                    //display No records found message
                    this.isDialogDisplay = false;
                    this.isDisplayMessage = true;                    
                }
            })
            .catch(error=>{
                this.error = error;
                this.items = undefined;
                this.isDialogDisplay = false;
            })
        }else{
            this.isDialogDisplay = false;
        }                
    }

    //This method is called during checkbox value change
    handleCheckboxChange(event){
        let selectItemTemp = event.detail.value;
        
        //all the chosen checkbox items will come as follows: selectItemTemp=0032v00002x7UE9AAM,0032v00002x7UEHAA2
        console.log(' handleCheckboxChange  value=', event.detail.value);        
        this.selectedItems = []; //it will hold only newly selected checkbox items.        
        
        /* find the value in items array which has been prepared during database call
           and push the key/value inside selectedItems array           
        */
        selectItemTemp.map(p=>{            
            let arr = this.items.find(element => element.value == p);
            if(arr != undefined){
                this.selectedItems.push(arr);
            }  
        });     
    }

    //this method removes the pill item
    handleRemoveRecord(event){        
        const removeItem = event.target.dataset.item; 
        
        //this will prepare globalSelectedItems array excluding the item to be removed.
        this.globalSelectedItems = this.globalSelectedItems.filter(item => item.value  != removeItem);
        const arrItems = this.globalSelectedItems;

        //initialize values again
        this.initializeValues();
        this.value =[]; 

        //propagate event to parent component
        const evtCustomEvent = new CustomEvent('remove', {   
            detail: {removeItem,arrItems}
            });
        this.dispatchEvent(evtCustomEvent);
    }

    //Done dialog button click event prepares globalSelectedItems which is used to display pills
    handleDoneClick(event){
        //remove previous selected items first as there could be changes in checkbox selection
        this.previousSelectedItems.map(p=>{
            this.globalSelectedItems = this.globalSelectedItems.filter(item => item.value != p.value);
        });
        
        //now add newly selected items to the globalSelectedItems
        this.globalSelectedItems.push(...this.selectedItems);        
        const arrItems = this.globalSelectedItems;
        console.log('test54'+JSON.stringify(arrItems));
        //store current selection as previousSelectionItems
        this.previousSelectedItems = this.selectedItems;
        this.initializeValues();
        
        //propagate event to parent component
        const evtCustomEvent = new CustomEvent('retrieve', { 
            detail: {arrItems}
            });
        this.dispatchEvent(evtCustomEvent);
    }

    //Cancel button click hides the dialog
    handleCancelClick(event){
        this.initializeValues();
    }

    //this method initializes values after performing operations
    initializeValues(){
        this.searchInput = '';        
        this.isDialogDisplay = false;
    }
}

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__HomePage</target>
        <target>lightning__RecordPage</target>
        <target>lightning__AppPage</target>
    </targets>
</LightningComponentBundle>

Apex Controller


public with sharing class MultiSelectLookupController {
    //This method retrieves the data from database table. It search input is '*', then retrieve all records
    @AuraEnabled 
    public static List<SObjectQueryResult> retrieveRecords(String objectName, 
                                                           String fieldAPINames,
                                                           String filterFieldAPIName,
                                                           String strInput){
                                                               List<SObject> lstResult= new List<SObject>();
                                                               List<SObjectQueryResult> lstReturnResult = new List<SObjectQueryResult>();
                                                               if(strInput.equals('*')){
                                                                   strInput = '';
                                                               }
                                                               boolean userActive=true;
                                                               String str = strInput + '%';
                                                               String recName = 'Person Account';
                                                               String strQueryField = '';
                                                               List<String> fieldList = fieldAPINames.split(',');
                                                               
                                                               //check if Id is already been passed
                                                               if(!fieldList.contains('Id')){
                                                                   fieldList.add('Id');
                                                                   strQueryField = String.join(fieldList, ',');
                                                               }else {
                                                                   strQueryField = fieldAPINames;
                                                               }
                                                               if(objectName=='Product__c'){
                                                                   String strQuery = 'SELECT ' + String.escapeSingleQuotes(strQueryField) 
                                                                       + ' FROM ' 
                                                                       + String.escapeSingleQuotes(objectName) 
                                                                       + ' WHERE recordtype.name=\'Sample\' AND ' + filterFieldAPIName + '  LIKE \'' + str + '%\'' 
                                                                       + ' ORDER BY ' + filterFieldAPIName
                                                                       + ' LIMIT 50'; 
                                                                   System.debug('strQuery=' + strQuery);
                                                                    lstResult = database.query(strQuery);
                                                                   }   
                                                               else{
                                                                   String strQuery = 'SELECT ' + String.escapeSingleQuotes(strQueryField) 
                                                                       + ' FROM ' 
                                                                       + String.escapeSingleQuotes(objectName) 
                                                                       + ' WHERE ' + filterFieldAPIName + '  LIKE \'' + str + '%\'' 
                                                                       + ' ORDER BY ' + filterFieldAPIName
                                                                       + ' LIMIT 50'; 
                                                                   System.debug('strQuery=' + strQuery);
                                                                    lstResult = database.query(strQuery);
                                                               }
                                                               
                                                               
                                                               return lstReturnResult;
                                                           }
    
    public class SObjectQueryResult {
        @AuraEnabled
        public String recordId;
        
        @AuraEnabled
        public String recordName;
    }
}

Parent LWC HTML to add the component

In this component, we have used the above multi-select lookup as a child component to add the products. “<c-multi-select-lookup” to embed the component. Understand that the ‘-‘ is used to segregate the capital letters in the name of the component “multiSelectLookup”.

<c-multi-select-lookup 
                label-name="Add Product"
                object-api-name= "Product__c"
                field-api-names="Id,Name"
                filter-field-api-name="Name"
                onvaluechange={handleChange}
                onretrieve={selectItemEventHandler} 
                onremove={deleteItemEventHandler}
                required="true">
            </c-multi-select-lookup>

Parent JS to add the component

handleChange(event){
  this.selectedBrand = event.detail; 
  var val=JSON.stringify(this.selectedBrand);
  console.log('sval'+val);


 
}
 selectItemEventHandler(event){
        let args = JSON.parse(JSON.stringify(event.detail.arrItems));
        this.displayItem(args);
        if(event.detail.arrItems.length>5){
          this.countBrandNames=true;
          
         console.log('true'+ this.countBrandNames);
       }
       else {
         this.countBrandNames=false;
           console.log('false'+ this.countBrandNames);
       }                    
    }

    displayItem(args){
     
        this.values = []; //initialize first
        args.map(element=>{
            this.values.push(element.value);
        });

        this.isItemExists = (args.length>0);
        this.selectedItemsToDisplay = this.values.join(',');
        console.log('test123'+this.selectedItemsToDisplay);
    }

    //captures the remove event propagated from lookup component
    deleteItemEventHandler(event){
        let args = JSON.parse(JSON.stringify(event.detail.arrItems));
        this.displayItem(args);
        if(event.detail.arrItems.length>5){
          this.countBrandNames=true;
          
         console.log('true'+ this.countBrandNames);
       }
       else {
         this.countBrandNames=false;
           console.log('false'+ this.countBrandNames);
       }         
    }

Check This Post: Dynamic Show and Hide Button in LWC

Also Refer: How to create a single select dropdown in LWC

References here

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

Share with friends and colleagues ?

3 thoughts on “How to create a multi-select and searchable component in LWC”

  1. You Have to add This Line of code in your multiSelectLookupController class file
    /*
    for (SObject record : lstResult) {
    SObjectQueryResult resultItem = new SObjectQueryResult();
    resultItem.recordId = String.valueOf(record.get(‘Id’));
    resultItem.recordName = String.valueOf(record.get(filterFieldAPIName));
    lstReturnResult.add(resultItem);
    }
    */
    because you are not convert the record in wrapper class so first you need to do first , If any other alternative solution please mention me .

    Reply
  2. public with sharing class multiSelectLookupController {
    public multiSelectLookupController() {

    }

    //This method retrieves the data from database table. It search input is ‘*’, then retrieve all records
    @AuraEnabled
    public static List retrieveRecords(String objectName, String fieldAPINames, String filterFieldAPIName, String strInput){
    List lstResult= new List();
    List lstReturnResult = new List();
    if(strInput.equals(‘*’)){
    strInput = ”;
    }
    boolean userActive=true;
    String str = strInput + ‘%’;
    String recName = ‘Person Account’;
    String strQueryField = ”;
    List fieldList = fieldAPINames.split(‘,’);
    //check if Id is already been passed
    if(!fieldList.contains(‘Id’)){
    fieldList.add(‘Id’);
    strQueryField = String.join(fieldList, ‘,’);
    }else {
    strQueryField = fieldAPINames;
    }
    if(objectName==’Product__c’){
    String strQuery = ‘SELECT ‘ + String.escapeSingleQuotes(strQueryField)
    + ‘ FROM ‘
    + String.escapeSingleQuotes(objectName)
    + ‘ WHERE recordtype.name=\’Sample\’ AND ‘ + filterFieldAPIName + ‘ LIKE \” + str + ‘%\”
    + ‘ ORDER BY ‘ + filterFieldAPIName
    + ‘ LIMIT 50’;
    System.debug(‘strQuery=’ + strQuery);
    lstResult = database.query(strQuery);
    }
    else{
    String strQuery = ‘SELECT ‘ + String.escapeSingleQuotes(strQueryField)
    + ‘ FROM ‘
    + String.escapeSingleQuotes(objectName)
    + ‘ WHERE ‘ + filterFieldAPIName + ‘ LIKE \” + str + ‘%\”
    + ‘ ORDER BY ‘ + filterFieldAPIName
    + ‘ LIMIT 50’;
    System.debug(‘strQuery=’ + strQuery);
    lstResult = database.query(strQuery);
    }
    System.debug(‘lstResult ==== ‘ + lstResult);
    System.debug(‘Before lstReturnResult === ‘ + lstReturnResult);
    for (SObject record : lstResult) {
    SObjectQueryResult resultItem = new SObjectQueryResult();
    resultItem.recordId = String.valueOf(record.get(‘Id’));
    resultItem.recordName = String.valueOf(record.get(filterFieldAPIName));
    lstReturnResult.add(resultItem);
    }

    System.debug(‘After lstReturnResult === ‘ + lstReturnResult);
    return lstReturnResult;
    }
    public class SObjectQueryResult {
    @AuraEnabled
    public String recordId;
    @AuraEnabled
    public String recordName;
    }
    }

    //Please Check this code compare to your code , basically This code is all yours But there is missing block of code in your blog Apex code?
    Thanks

    Reply

Leave a comment

error: Content is protected !!