Create Single/Multi-Select Combobox / Picklist in LWC Salesforce

by Rijwan Mohmmed
how-to-create-single-/-multi-select-combobox-/-picklist-in-lwc-salesforce

Hello friends, today we will learn to create single/Multi-Select Combobox / picklist with the search bar in LWC Salesforce. We will customize the things and create this because standard combobox in LWC doesn’t have such type of functionality .

Key Heighlights :

  1. You can search the picklist values
  2. In multiselect we create pills so you can remove easily on pills.
  3. this component is reusable
  4. There are 2 components so child is main and parent where we get selected options.
  5. Multi Select Combobox component is advance then dual-listbox because this cover small space and dual cover big space.

MultiSelectPickList.Html :

<template>
    
    <!-- Start Header Label Passed from Parent -->
    <template if:true={label}>
        <label class="slds-form-element__label">{label}</label>
    </template>
    <!-- End Header Label Passed from Parent -->
    <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" onmouseleave={handleMouseOut} onmouseenter={handleMouseIn}>
            <!-- Search Input -->
            <div class="slds-combobox__form-element slds-input-has-icon slds-input-has-icon_right" role="none">
                <lightning-input disabled={disabled} class="inputBox" placeholder="Select an Option" onblur={handleBlur} onclick={showOptions} onkeyup={filterOptions} value={searchString}  variant="label-hidden" id="combobox-id-1" ></lightning-input>
                <lightning-icon class="slds-input__icon" icon-name="utility:down" size="x-small" alternative-text="downicon"></lightning-icon>
            </div>
            <!-- Dropdown List -->
            <template if:true={showDropdown}>
                <div id="listbox-id-1" class="slds-dropdown slds-dropdown_length-5 slds-dropdown_fluid">
                    <ul class="slds-listbox slds-listbox_vertical recordListBox" role="presentation">
                        <template if:false={noResultMessage} >
                            <template for:each={optionData} for:item="option">
                                <li key={option.value} data-id={option.value} onmousedown={selectItem} class="slds-listbox__item eachItem" if:true={option.isVisible}>
                                    <template if:true={option.selected}>
                                        <lightning-icon icon-name="utility:check" size="x-small" alternative-text="icon" ></lightning-icon>
                                    </template>
                                    <span class="slds-media slds-listbox__option_entity verticalAlign slds-truncate">{option.label}</span>
                                </li>
                            </template>
                        </template>
                        <template if:true={noResultMessage} >
                            <li class="slds-listbox__item">
                                <span class="slds-media slds-listbox__option_entity verticalAlign slds-truncate">{noResultMessage}</span>
                            </li>
                        </template>
                    </ul>
                </div>
            </template>
        </div>
    </div>
    <!-- Multi Select Pills -->
    <template for:each={optionData} for:item="option">
		<template if:true={option.selected}>
			<lightning-pill label={option.label} key={option.value} name={option.value} onremove={closePill}>
				<lightning-icon icon-name="custom:custom11" alternative-text="Account"></lightning-icon>
			</lightning-pill>
		</template>
	</template>

</template>

MultiSelectPickList.Js :

import { LightningElement, track, api } from 'lwc';
 
export default class MultiSelectPickList extends LightningElement {
    
    @api options;
    @api selectedValue;
    @api selectedValues = [];
    @api label;
    @api disabled = false;
    @api multiSelect = false;
    @track value;
    @track values = [];
    @track optionData;
    @track searchString;
    @track noResultMessage;
    @track showDropdown = false;
 
    connectedCallback() {
        this.showDropdown = false;
        var optionData = this.options ? (JSON.parse(JSON.stringify(this.options))) : null;
        var value = this.selectedValue ? (JSON.parse(JSON.stringify(this.selectedValue))) : null;
        var values = this.selectedValues ? (JSON.parse(JSON.stringify(this.selectedValues))) : null;
        if(value || values) {
            var searchString;
            var count = 0;
            for(var i = 0; i < optionData.length; i++) {
                if(this.multiSelect) {
                    if(values.includes(optionData[i].value)) {
                        optionData[i].selected = true;
                        count++;
                    }  
                } else {
                    if(optionData[i].value == value) {
                        searchString = optionData[i].label;
                    }
                }
            }
            if(this.multiSelect)
                this.searchString = count + ' Option(s) Selected';
            else
                this.searchString = searchString;
        }
        this.value = value;
        this.values = values;
        this.optionData = optionData;
    }
 
    filterOptions(event) {
        this.searchString = event.target.value;
        if( this.searchString && this.searchString.length > 0 ) {
            this.noResultMessage = '';
            if(this.searchString.length >= 2) {
                var flag = true;
                for(var i = 0; i < this.optionData.length; i++) {
                    if(this.optionData[i].label.toLowerCase().trim().startsWith(this.searchString.toLowerCase().trim())) {
                        this.optionData[i].isVisible = true;
                        flag = false;
                    } else {
                        this.optionData[i].isVisible = false;
                    }
                }
                if(flag) {
                    this.noResultMessage = "No results found for '" + this.searchString + "'";
                }
            }
            this.showDropdown = true;
        } else {
            this.showDropdown = false;
        }
    }
 
    selectItem(event) {
        var selectedVal = event.currentTarget.dataset.id;
        if(selectedVal) {
            var count = 0;
            var options = JSON.parse(JSON.stringify(this.optionData));
            for(var i = 0; i < options.length; i++) {
                if(options[i].value === selectedVal) {
                    if(this.multiSelect) {
                        if(this.values.includes(options[i].value)) {
                            this.values.splice(this.values.indexOf(options[i].value), 1);
                        } else {
                            this.values.push(options[i].value);
                        }
                        options[i].selected = options[i].selected ? false : true;   
                    } else {
                        this.value = options[i].value;
                        this.searchString = options[i].label;
                    }
                }
                if(options[i].selected) {
                    count++;
                }
            }
            this.optionData = options;
            if(this.multiSelect){
                this.searchString = count + ' Option(s) Selected';

                let ev = new CustomEvent('selectoption', {detail:this.values});
                this.dispatchEvent(ev);
            }
                

            if(!this.multiSelect){
                let ev = new CustomEvent('selectoption', {detail:this.value});
                this.dispatchEvent(ev);
            }

            if(this.multiSelect)
                event.preventDefault();
            else
                this.showDropdown = false;
        }
    }
 
    showOptions() {
        if(this.disabled == false && this.options) {
            this.noResultMessage = '';
            this.searchString = '';
            var options = JSON.parse(JSON.stringify(this.optionData));
            for(var i = 0; i < options.length; i++) {
                options[i].isVisible = true;
            }
            if(options.length > 0) {
                this.showDropdown = true;
            }
            this.optionData = options;
        }
    }

    @api clearAll() {
        this.values = [];
        var optionData = this.options ? (JSON.parse(JSON.stringify(this.options))) : null;
        for (var i = 0; i < optionData.length; i++) {
            if (this.multiSelect) {
                optionData[i].selected = false;
            }
        }
        this.searchString = 0 + ' Option(s) Selected';
        this.selectedValues = [];
        this.optionData = optionData;
    }
 
    closePill(event) {
        var value = event.currentTarget.name;
        var count = 0;
        var options = JSON.parse(JSON.stringify(this.optionData));
        for(var i = 0; i < options.length; i++) {
            if(options[i].value === value) {
                options[i].selected = false;
                this.values.splice(this.values.indexOf(options[i].value), 1);
            }
            if(options[i].selected) {
                count++;
            }
        }
        this.optionData = options;
        if(this.multiSelect){
            this.searchString = count + ' Option(s) Selected';
            
            let ev = new CustomEvent('selectoption', {detail:this.values});
            this.dispatchEvent(ev);
        }
    }
 
    handleBlur() {
        var previousLabel;
        var count = 0;

        for(var i = 0; i < this.optionData.length; i++) {
            if(this.optionData[i].value === this.value) {
                previousLabel = this.optionData[i].label;
            }
            if(this.optionData[i].selected) {
                count++;
            }
        }

        if(this.multiSelect){
            this.searchString = count + ' Option(s) Selected';
        }else{
            this.searchString = previousLabel;
        }

        this.showDropdown = false;
    }

    handleMouseOut(){
        this.showDropdown = false;
    }

    handleMouseIn(){
        this.showDropdown = true;
    }
}

MultiSelectPickList.css :

.verticalAlign {
 cursor: pointer;
    padding: 0px 5px !important;
}
.slds-dropdown {
    padding:0px !important;
}
.recordListBox {
 margin-top:0px !important;
 overflow-y: scroll;
}
.slds-listbox li {
    padding: .45rem 0.7rem !important;
    display: flex;
}
.inputBox input {
 padding-left: 10px;
}
.eachItem:hover {
    background-color: #F1F1F1;
    cursor: pointer;
}
 
/* For Scrolling */
::-webkit-scrollbar {
    width: 7px;
    height: 7px;
}
::-webkit-scrollbar-track {
   display: none !important;
}
::-webkit-scrollbar-thumb {
    border-radius: 10px;
    background: rgba(0,0,0,0.4);
}
.slds-dropdown_fluid{
	margin-top: 0px;
}

We will put this component any parent component so we can reuse this

MultiSelectPickListParent.Html :

<template>
    <div class="slds-tabs_card">
        <div class="slds-page-header">
            <div class="slds-page-header__row">
                <div class="slds-page-header__col-title">
                    <div class="slds-media">
                        <div class="slds-media__figure">
                            <span class="slds-icon_container slds-icon-standard-opportunity">
                                 <lightning-icon icon-name="standard:recipe" alternative-text="recipe" title="recipe"></lightning-icon>
                            </span>
                        </div>
                        <div class="slds-media__body">
                            <div class="slds-page-header__name">
                                <div class="slds-page-header__name-title">
                                    <h1>
                                        <span>LWC Combobox</span>
                                        <span class="slds-page-header__title slds-truncate" title="TechDicer">TechDicer</span>
                                    </h1>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div> <br/>
     <lightning-card  variant="Narrow"  title="Pick List" if:true={options}>
        <div class="slds-p-horizontal_small"> 
            <c-multi-select-pick-list onselectoption={handleSelectOption} options={options} selected-value={selectedValue} label="Single Select Pick List"></c-multi-select-pick-list>
        </div>
        <div class="slds-p-horizontal_small"> 
            <c-multi-select-pick-list multi-select="true" onselectoption={handleSelectOptionList} options={options} selected-values={selectedValueList} label="multiSelect Pick List"></c-multi-select-pick-list>
        </div>
   </lightning-card>
</template>

MultiSelectPickListParent.Js :

import { LightningElement, track, wire } from 'lwc';
import ACCOUNT_OBJECT from '@salesforce/schema/Account';
import TYPE_FIELD from '@salesforce/schema/Account.Type';
import { getPicklistValues, getObjectInfo } from 'lightning/uiObjectInfoApi';
const options = [
                    {'label':'India','value':'India'},
                    {'label':'USA','value':'USA'},
                    {'label':'China','value':'China'},
                    {'label':'Rusia','value':'Rusia'}
                ];
 
export default class MultiSelectPickListParent extends LightningElement {
    @track selectedValue = 'Customer - Direct';//selected values
    @track selectedValueList = ['Customer - Direct'];//selected values
    @track options; //= options;

    @wire(getObjectInfo, { objectApiName: ACCOUNT_OBJECT })
    objectInfo;
 
    //fetch picklist options
    @wire(getPicklistValues, {
        recordTypeId: "$objectInfo.data.defaultRecordTypeId",
        fieldApiName: TYPE_FIELD
    })
    wirePickList({ error, data }) {
        if (data) {
            this.options = data.values;
        } else if (error) {
            console.log(error);
        }
    }
     
    //for single select picklist
    handleSelectOption(event){
        console.log(event.detail);
        this.selectedValue = event.detail;
    }
 
    //for multiselect picklist
    handleSelectOptionList(event){
        console.log(event.detail);
        this.selectedValueList = event.detail;
        console.log(this.selectedValueList);
    }
}

What’s your Reaction?
+1
2
+1
2
+1
2
+1
0
+1
2
+1
2

You may also like

54 comments

Mark June 10, 2022 - 5:43 am

How to get the exact selected values from here? I want to pass it to Apex Controller in a string format with “;” as separator. Please help. Thank you.

Reply
Rijwan Mohmmed June 20, 2022 - 3:32 pm

You can check the parent component (MultiSelectPickListParent.Js) I am passing picklist values.

Reply
Fred June 20, 2022 - 3:22 pm

Hi, this version is not searchable. Can we somehow change the picklist values and use it within a flow? I am only able to install the static and fixed component containing both picklist fields. How can I customize it?
Thanks a lot for your help!

Reply
Rijwan Mohmmed June 20, 2022 - 3:30 pm

yupp You can change the picklist values from parent components

Reply
Uttam Singh July 29, 2022 - 4:34 am

vertical scroll bar getting closed whenever user click on scrollbar.

Reply
Rijwan Mohmmed July 31, 2022 - 4:43 pm

Hi @Uttam, add below CSS class in LWC CSS file
.slds-dropdown_fluid{
margin-top: 0px;
}

Reply
Anjana April 25, 2023 - 2:24 pm

Hi RIJWAN MOHMMED, I tried this. Still the scroll bar is not working. Can you please help me on this.

Reply
Rijwan Mohmmed April 27, 2023 - 12:52 pm

Schedule a call on sunday

Reply
Giacomo July 17, 2023 - 12:16 pm

hi Rijwan, clicking the vertical scroll bar closes the dropdown menu, causing frustration on users not using the mouse wheel for scrolling. I tried using an onscroll event and preventDefault, however doesn’t look like the scroll event is getting fired when scrolling occurs by clicking the scrollbar. Is there a way to prevent this from happening? It appears that the lightning-combobox somehow managed to do it, as clicking the scrollbar there does not close the dropdown of options. Thanks

Rijwan Mohmmed July 18, 2023 - 3:49 am

you can commented that code which fire on ommouseHover.

Priyanka August 3, 2022 - 12:01 pm

Rijwan, I am trying to get the picklist values from object using apex controller method. But the connectedcallback of the child component gets called first and then apex controller is getting called. this results into empty array in the options attribute of the multiselect picklist.

Reply
Rijwan Mohmmed August 3, 2022 - 4:59 pm

Hi @Priyanka
Pass the picklist values from the parent component to the child component. In my component, you just need to assign the picklist values to the child component.

Just put the values in options field

Reply
Carlos February 1, 2023 - 10:45 am

Hello,
I found the same issue. I am passing the values from Apex, but the child component gets called first.
There is a way to refresh the child component after that?
Thanks

Reply
Carlos February 1, 2023 - 11:04 am

Just found the fix.

I had to add is before the adding the child component, so every thing that I change options it will refresh it.

Reply
Thaigo June 22, 2023 - 2:38 pm

I’m running on the same issue, I can get a hardcoded option list to show up, but the options are not updating once I change the value of the option variable

Mohsin August 7, 2022 - 1:43 pm

How can I get an Array of multi-selected values?

Reply
Rijwan Mohmmed August 7, 2022 - 4:03 pm

Please check my parent component , I am getting array list of selected options.

Reply
Mohsin August 7, 2022 - 2:05 pm

handleMultiSelectValues(event){
try {
this.fields = event.detail.payload.values;

} catch (error) {
console.log(error);
}
}

This is how I am trying to get all multi-selected values But I am getting an empty array.

for reference;

Reply
Rijwan Mohmmed August 7, 2022 - 4:04 pm

Hi @Mohsin, below is code for get select options array
//for multiselect picklist
handleSelectOptionList(event){
console.log(event.detail);
this.selectedValueList = event.detail;
console.log(this.selectedValueList);
}

Reply
Mohsin August 7, 2022 - 2:12 pm

”handleMultiSelectValues(event){
try {
this.fieldObjectValues = event.detail.payload.values;
});

console.log( this.fieldObjectValues);

} catch (error) {
console.log(error);
}
}”


This is how I am trying to get multi-selected values but I am getting an empty array.

Reply
Hil September 25, 2022 - 9:58 pm

How we can make picklist values darkest and bolder?

Reply
Rijwan Mohmmed September 27, 2022 - 5:34 pm

apply CSS on picklist options

Reply
Ravi September 27, 2022 - 3:20 pm

How can we default the picklist value by default coming from Apex controller?

Reply
Rijwan Mohmmed September 27, 2022 - 5:38 pm

pass the value as selected values from the parent component

Reply
Miggy November 17, 2022 - 6:58 am

HI, I need to clear selected muti-picklist and the pills when parent was trigger change by another field.

I found the problem when I send blank value from parent to clear picklist, the text in combobox was clear to 0 selected but in the dropdown it still show selected icon and below is show selected pill from latest.

Reply
Rijwan Mohmmed November 18, 2022 - 3:29 pm

create a child component method so you can call from the parent and clear the selected picklist values.

Reply
shab November 17, 2022 - 11:16 pm

I cant find this component in lightning app builder under custom components. Am I missing something?

Reply
shab November 17, 2022 - 11:43 pm

I fixed the previous issue by adding targets in XML File.
Another question: Instead of static options(like India, USA etc), can we fetch the list of all objects in salesforce?

Reply
Rijwan Mohmmed November 18, 2022 - 3:24 pm

Hi, Shab,
you can use a picklist of objects, you need to fetch that picklist from apex.

Reply
Alex December 2, 2022 - 9:03 pm

hello Rijwan,
In search string, it doesn’t find the items in the list or sort them?However, it returns the right message that the list doesn’t have the records. How can I do it ?

Reply
Rijwan Mohmmed December 3, 2022 - 5:22 am

Hi Alex, I have fixed the MultiSelectPickList.Html file now. now search option working.
Thanks for the posted bug.

Reply
Juan December 14, 2022 - 8:11 am

Hi Rijwan,
is it possibe to put clear all button at the bottom of the multi-select picklist so that it clear all the selected values and removes all the pills? If yes, how to achieve it?

Reply
Rijwan Mohmmed December 14, 2022 - 8:24 am

Hi @Juan,
Yupp that is possible. Below is the code


closeAllPill(event) {

var count = 0;
var options = JSON.parse(JSON.stringify(this.optionData));
for(var i = 0; i < options.length; i++) { if(options[i].selected) { options[i].selected = false; this.values.splice(this.values.indexOf(options[i].value), 1); } if(options[i].selected) { count++; } } this.optionData = options; if(this.multiSelect){ this.searchString = count + ' Option(s) Selected'; let ev = new CustomEvent('selectoption', {detail:this.values}); this.dispatchEvent(ev); } }

Reply
Juan December 14, 2022 - 8:32 am

Thanks for your quick reply, Rijwan.
When will closeAllPIll() will be called? I want it as a button. Where should I put in MultiselectPicklist.html?
you can put in the parent LWC component and call from there.

Reply
Rijwan Mohmmed December 15, 2022 - 5:07 pm

above code should be put in: MultiSelectPickList.JS
and create a button in the parent component: MultiSelectPickListParent.HTML
Call this child component method from parent .
https://techdicer.com/call-child-method-from-parent-component-in-lightning-web-component-lwc-salesforce/

Reply
Farooq March 16, 2023 - 3:03 pm

Hi Rijwan,

First of all thanks a lot for posting this solution.

It seems when there is a duplicate in the list, and if we try to use keyword in the search which is present in the duplicated entry, then an exception is occurring and causing the abnormality in the functionality. By any chance, have you noticed this in your tests?

“Uncaught (in promise) TypeError: Failed to execute ‘removeChild’ on ‘Node’: parameter 1 is not of type ‘Node’.
at HTMLUListElement.removeChild”

Reply
Rijwan Mohmmed March 16, 2023 - 3:05 pm

Hi Farooq, thanks for caught the issue I will check and fix this

Reply
Deekshant April 3, 2023 - 8:38 am

Hey is there any video available of the above code with the explanation??

Reply
Rijwan Mohmmed April 6, 2023 - 6:17 am

You can arrange a meeting by clicking on book a meeting in right side button, I can explain you.

Reply
Shreya April 17, 2023 - 11:38 am

Hey!!
this is not working if I am using it in a same template for 2 times. why? I need it 2 times but it is not working 2 times?

Reply
Shreya April 17, 2023 - 11:46 am

ALso I am giving new values to all the params::

this is working fine, I am thankful for providing such a generic code.

but I am using twice in same template.

and here, it is not working. all the options in {optionsCityUnique} apperas in JS but not visible in html. I had applied @track optionsCityUnique = []; also. Please mention if I have to do alternate solutuion for getting it right.

Reply
Rijwan Mohmmed April 17, 2023 - 5:51 pm

I am not getting your questing, Can you please explain more

Reply
Rijwan Mohmmed April 17, 2023 - 5:54 pm

In HTML I am checking option have or not for this I am using If:true
so you can change this condition then it will work

Reply
shreya April 19, 2023 - 4:49 pm

here is the code:: 1st layout item is working, but 2nd one is not.

Reply
shreya April 19, 2023 - 4:50 pm

<!–

–>

here is the code:: 1st layout item is working, but 2nd one is not.

Reply
Deepa April 26, 2023 - 6:57 pm

Hi Rijwan,

I am using ur child component same as it is and calling it from my parent html file. In my JS, I am fetching the options using getPicklistValues() and fetching the status picklist values.The UI works exactly fine. When selected each option , it shows the no of selcted options. But these selected values which are stored in this.values are passed from child to Parent through custom event and I am able to retrieve the values in the parent and I am trying to send the same to the apex controller as a parmaeter using $. but here my wire method doesnt work at all as the values passed from JS are not fetched in the controller. All the other fields that are passed from parent are being queried using wire. But the param which is sent from the child to parent and then to the controller is not working. Could yuou please help me. Have you ever faced this issue?

Appreciate your help.

Thanks.

Reply
Prateek Shekhawat June 20, 2023 - 11:09 am

I want to know how can we make the search functionality here. Thanks in advance

Reply
Rijwan Mohmmed June 20, 2023 - 11:13 am

Hi @Prateek, Search functionality already there.

Reply
Chirag September 8, 2023 - 6:25 am

HI is it possible to change the name of options[] to any other lets say NumberOfFloors[];
I tried Doing that but my component just keeps rendering I think as you have used Options[] in your child component it only takes that vales not the updated NumberOfFloors[] value

Reply
Rijwan Mohmmed September 8, 2023 - 7:18 am

Hi Chirag, you can use any variable. Just check the reference of that field.Also check the HTML files , I am passing the values as a options variable.

Reply
Oscar December 13, 2023 - 1:04 pm

You are awesome, I’ve seen so many half assed/broken implementations of this one didn’t require much changes at all. The only weird thing was the mouse hover opening the drop down but that was easily disabled.

Reply
Rijwan Mohmmed December 13, 2023 - 5:27 pm

Thanks you Oscar

Reply
Alex January 4, 2024 - 2:31 pm

Hey Rijwan, What if I want use the component two or more time in a parent component for different option?

Reply
Rijwan Mohmmed January 4, 2024 - 3:39 pm

You can use , you only need to change in parent component that create different-2 method (handleSelectOptionList)

Reply

Leave a Comment