How to create LWC Multi-select combobox
During web app development multiselect combobox is a common scenario. Salesforce lightning combobox component allows single selection from options. Salesforce provides solution to multiselect combobox with Dual Listbox however this option is not feasible when we have to deal with large number options and dual listbox takes comparatively more space on UI screen. SLDS includes blueprint of multiselect combobox however complete implementation solution and ready to use component is missing.
In this blog post, provided solution is feasible for large number of options because user can search particular option from combobox and by using scroll bar user can navigate through options.
Even if we consider single select lightning combobox, same solution can be used where we can take advantage of search feature within combobox when number of options to be shown are large.
- User can select multiple options from dropdown
- Same component can be used as single select or multi select with help of 'multiselect' attribute
- Combobox can be rendered with pre selected values from available options
Parent LWC component
test.html
<template>
<lightning-card>
<lightning-layout multiplerows="true">
<lightning-layout-item size="12" small-device-size="4">
<div if:true={colorsList} class="slds-m-right_x-large slds-m-left_x-large">
<c-multiselect-combobox onselect= {single_select_Color} options={colorsList} label="Single select picklist"></c-multiselect-combobox>
</div><br>
</lightning-layout-item>
<lightning-layout-item size="12" small-device-size="4">
<div if:true={colorsList} class="slds-m-right_x-large slds-m-left_x-large">
<c-multiselect-combobox onselect= {multi_select_Colors} multiselect="true" options={colorsList} label="Multi select picklist"></c-multiselect-combobox>
</div>
</lightning-layout-item>
<lightning-layout-item size="12" small-device-size="4">
<div if:true={pre_selected_colors} class="slds-m-right_x-large slds-m-left_x-large">
<c-multiselect-combobox onselect= {preselect_Colors} multiselect="true" options={colorsList} selectedvalues={pre_selected_colors} label="preselected multiselect values"></c-multiselect-combobox>
</div>
</lightning-layout-item>
</lightning-layout>
</lightning-card>
</template>
test.js
import { LightningElement, track } from 'lwc';
export default class Test extends LightningElement {
@track colorsList =[];
@track pre_selected_colors=[];
@track single_selected_color;
@track multi_selected_colors=[];
@track preselected_colors_list=[]
connectedCallback() {
this.colorsList.push({value:'Red', label:'Red'});
this.colorsList.push({value:'Orange', label:'Orange'});
this.colorsList.push({value:'Yellow', label:'Yellow'});
this.colorsList.push({value:'Lavender', label:'Lavender'});
this.colorsList.push({value:'Orchid', label:'Orchid'});
this.colorsList.push({value:'Magenta', label:'Magenta'});
this.colorsList.push({value:'Purple', label:'Purple'});
this.colorsList.push({value:'Indigo', label:'Indigo'});
this.colorsList.push({value:'LimeGreen', label:'LimeGreen'});
this.colorsList.push({value:'Cyan', label:'Cyan'});
this.colorsList.push({value:'RoyalBlue', label:'RoyalBlue'});
this.colorsList.push({value:'Maroon', label:'Maroon'});
this.colorsList.push({value:'GhostWhite', label:'GhostWhite'});
this.colorsList.push({value:'Gray', label:'Gray'});
this.colorsList.push({value:'Black', label:'Black'});
this.colorsList.push({value:'Pink', label:'Pink'});
this.pre_selected_colors.push('Orange');
this.pre_selected_colors.push('Indigo');
this.template
.querySelectorAll('c-multiselect-combobox').forEach(element => {
if (element.label === 'Single select picklist') {
element.ReloadComponent(this.colorsList);
}
});
this.template
.querySelectorAll('c-multiselect-combobox').forEach(element => {
if (element.label === 'Multi select picklist') {
element.ReloadComponent(this.colorsList);
}
});
this.template
.querySelectorAll('c-multiselect-combobox').forEach(element => {
if (element.label === 'preselected multiselect values') {
element.ReloadComponentwith_preselectedvalues(this.colorsList,this.pre_selected_colors,true);
}
});
}
single_select_Color(event){
this.single_selected_color = event.detail.payload.value;
console.log('selected colors..',this.single_selected_color);
}
multi_select_Colors(event){
this.multi_selected_colors = event.detail.payload.values;
console.log('selected colors..'+this.multi_selected_colors);
}
preselect_Colors(event){
this.preselected_colors_list = event.detail.payload.values;
console.log('selected colors..'+this.preselected_colors_list);
}
}
Child LWC component 'multiselectcombobox'
multiselectcombobox.html
<template>
<template if:true={label}>
<label class="slds-form-element__label">{label}</label>
</template>
<div onmouseleave={handleMouseOut}>
<div tabindex="-1">
<div class="slds-combobox_container" id="divboxcomponentid">
<div class="slds-combobox slds-dropdown-trigger slds-dropdown-trigger_click slds-is-open" aria-expanded="true" aria-haspopup="listbox" role="combobox">
<!-- Search Input -->
<div class="slds-combobox__form-element slds-input-has-icon slds-input-has-icon_right" role="none">
<lightning-input dropdown-alignment="auto" disabled={disabled} class="inputBox" placeholder="Select an Option" onblur={blurEvent} onclick={showOptions} onkeyup={filterOptions} value={searchString} auto-complete="off" variant="label-hidden" id="combobox-id-1" ></lightning-input>
<lightning-icon class="slds-input__icon" icon-name="utility:down" size="x-small" alternative-text="search"></lightning-icon>
</div>
<!-- Dropdown List -->
<template if:true={showDropdown}>
<div id="listbox-id-1" class="slds-dropdown slds-dropdown_length-5 slds-dropdown_fluid" onscroll={handlescroll}><!--style="{! 'max-height:' + (8 + (v.recordCount * 40)) + 'px' }""-->
<ul class="slds-listbox slds-listbox_vertical recordListBox" role="presentation">
<template if:false={message} >
<template for:each={optionData} for:item="option">
<template if:true={option.isVisible}>
<li key={option.value} data-id={option.value} onmousedown={selectItem} class="slds-listbox__item eachItem">
<template if:true={option.selected}>
<lightning-icon icon-name="utility:check" size="x-small" alternative-text="icon" ></lightning-icon>
</template>
<span title={option.label} class="slds-media slds-listbox__option_entity verticalAlign slds-truncate">{option.label}</span>
</li>
</template>
</template>
</template>
<template if:true={message} >
<li class="slds-listbox__item">
<span class="slds-media slds-listbox__option_entity verticalAlign slds-truncate">{message}</span>
</li>
</template>
</ul>
</div>
</template>
</div>
</div>
</div>
</div>
<!-- Multi Select Pills -->
<template for:each={optionData} for:item="option">
<template if:true={option.selected}>
<lightning-pill key={option.value} class="slds-m-around_xx-small" name={option.value} label={option.label} onremove={removePill}></lightning-pill>
</template>
</template>
</template>
multiselectcombobox.js
import { LightningElement, track, api } from 'lwc';
export default class MultiselectCombobox extends LightningElement {
@api options;
@api selectedValue;
@api selectedvalues = [];
@api label;
@api minChar = 2;
@api disabled = false;
@api multiselect = false;
@track value;
@track values = [];
@track optionData;
@track searchString;
@track message;
@track showDropdown = false;
connectedCallback() {
this.showDropdown = false;
let optionData = this.options ? (JSON.parse(JSON.stringify(this.options))) : null;
this.value = this.selectedValue ? (JSON.parse(JSON.stringify(this.selectedValue))) : null;
this.values = this.selectedvalues ? (JSON.parse(JSON.stringify(this.selectedvalues))) : null;
if(this.value || this.values) {
this.connectedcallback_extension(optionData);
}
}
connectedcallback_extension(optionData){
let count = 0;
let searchString;
console.log('multiselectcombobox.........',optionData[0]);
for(let i = 0; i < optionData.length; i++) {
if(this.multiselect) {
if(this.values.includes(optionData[i].value)) {
optionData[i].selected = true;
count++;
}
} else {
if(optionData[i].value == this.value) {
searchString = optionData[i].label;
}
}
}
if(this.multiselect)
this.searchString = count + ' Option(s) Selected';
else
this.searchString = searchString;
this.optionData = optionData;
}
filterOptions(event) {
this.searchString = event.target.value;
if( this.searchString && this.searchString.length > 0 ) {
this.message = '';
if(this.searchString.length >= this.minChar) {
let flag = true;
for(let 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.message = "No results found for '" + this.searchString + "'";
}
}
this.showDropdown = true;
} else {
this.showDropdown = false;
}
}
selectItem(event) {
let selectedVal = event.currentTarget.dataset.id;
if(event.currentTarget.dataset.id) {
let options = JSON.parse(JSON.stringify(this.optionData));
let count = this.selectItem_extension(options,selectedVal);
if(this.multiselect)
this.searchString = count + ' Option(s) Selected';
if(this.multiselect)
event.preventDefault();
else{
this.dispatchEvent(new CustomEvent('select', {
detail: {
'payloadType' : 'multi-select',
'payload' : {
'value' : this.value,
'values' : this.values
}
}
}));
this.showDropdown = false;
}
}
}
selectItem_extension(options,selectedVal){
let count = 0;
for(let i = 0; i < options.length; i++) {
options = this.selectItem_extension1(options,selectedVal,i);
if(options[i].selected) {
count = count+1;
}
}
this.optionData = options;
return count;
}
selectItem_extension1(options,selectedVal,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;
}
}
return options;
}
showOptions() {
if(this.disabled == false && this.options) {
this.message = '';
this.searchString = '';
let options = JSON.parse(JSON.stringify(this.optionData));
for(let i = 0; i < options.length; i++) {
options[i].isVisible = true;
}
if(options.length > 0) {
this.showDropdown = true;
}
this.optionData = options;
}
}
removePill(event) {
let value = event.currentTarget.name;
let count = 0;
let options = JSON.parse(JSON.stringify(this.optionData));
for(let 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';
}
this.dispatchEvent(new CustomEvent('select', {
detail: {
'payloadType' : 'multi-select',
'payload' : {
'value' : this.value,
'values' : this.values
}
}
}));
}
blurEvent() {
let previousLabel;
let count = 0;
for(let 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;
if(this.multiselect){
this.dispatchEvent(new CustomEvent('select', {
detail: {
'payloadType' : 'multi-select',
'payload' : {
'value' : this.value,
'values' : this.values
}
}
}));
}
}
handleMouseOut(){
this.showDropdown = false;
}
handlescroll(){
this.showDropdown = true;
}
@api
ReloadComponent(newoptions) {
this.options = newoptions;
this.showDropdown = false;
let optionData = this.options ? (JSON.parse(JSON.stringify(this.options))) : null;
this.value = this.selectedValue ? (JSON.parse(JSON.stringify(this.selectedValue))) : null;
this.values = this.selectedvalues ? (JSON.parse(JSON.stringify(this.selectedvalues))) : null;
if(this.value || this.values) {
this.connectedcallback_extension(optionData);
}
}
@api
ReloadComponentwith_preselectedvalues(newoptions, selectedvalues, multiselect) {
this.options = newoptions;
if(this.selectedvalues.length>0){
this.selectedvalues = selectedvalues;
}
this.showDropdown = false;
let optionData = this.options ? (JSON.parse(JSON.stringify(this.options))) : null;
if(multiselect==false){
this.value = this.selectedvalue ? (JSON.parse(JSON.stringify(this.selectedvalues))) : null;
}
if(multiselect==true){
this.values = this.selectedvalues ? (JSON.parse(JSON.stringify(this.selectedvalues))) : null;
}
if(this.value || this.values || multiselect==false) {
this.ReloadComponentwith_preselectedvalues_extension(optionData,selectedvalues,multiselect);
}
}
ReloadComponentwith_preselectedvalues_extension(optionData,selectedvalues,multiselect){
let count = 0;
for(let i = 0; i < optionData.length; i++) {
if(multiselect && this.values) {
if(this.values.includes(optionData[i].value)) {
optionData[i].selected = true;
count++;
}
}
else {
this.value = selectedvalues[0];
if(optionData[i].value == this.value) {
this.value = optionData[i].value;
this.searchString = optionData[i].label;
}
}
}
this.optionData = optionData;
if(multiselect && this.values)
this.searchString = count + ' Option(s) Selected';
}
}
multiselectcombobox.css
.verticalAlign {
cursor: pointer;
padding: 0px 5px !important;
}
.slds-dropdown {
padding:20px !important;
}
.recordListBox {
margin-top:0px !important;
}
.slds-listbox li {
padding: .05rem 0.7rem !important;
display: flex;
}
.inputBox input {
padding-left: 10px;
}
.eachItem:hover {
background-color: #F1F1F1;
cursor: pointer;
}
You can find all code in detail on Github
.gif)


No comments:
Post a Comment