lightning-combobox doesn’t support multiple selection. So, we can make use of the following reusable Lightning Web Component for multi-select picklist.
Sample Code:
Reusable multi-select Lightning Web Component:
HTML:
<template>
<template if:true={label}>
<label class="slds-form-element__label">
{label}
</label>
</template>
<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__form-element slds-input-has-icon slds-input-has-icon_right"
role="none">
<lightning-input
class="inputBox"
placeholder={placeHolder}
onfocusout={handleBlur}
onclick={showOptions}
onkeyup={filterOptions}
value={searchString}
variant="label-hidden">
</lightning-input>
<lightning-icon
class="slds-input__icon"
icon-name="utility:down"
size="x-small"
alternative-text="downicon">
</lightning-icon>
</div>
<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" role="presentation">
<template if:false={noResultMessage} >
<template for:each={optionData} for:item="option" for:index="index">
<li
key={option.value}
data-id={option.value}
data-index={index}
onmousedown={selectItem}
class="slds-listbox__item"
if:true={option.isVisible}>
<template if:true={option.selected}>
<lightning-icon
icon-name="utility:check"
size="x-small"
alternative-text="icon" >
</lightning-icon>
{option.label}
</template>
<template if:false={option.selected}>
<span class="slds-media slds-listbox__option_entity slds-truncate">
{option.label}
</span>
</template>
</li>
</template>
</template>
<template if:true={noResultMessage} >
<li class="slds-listbox__item">
<span class="slds-media slds-listbox__option_entity slds-truncate">
{noResultMessage}
</span>
</li>
</template>
</ul>
</div>
</template>
</div>
</div>
</template>
JavaScript:
import { LightningElement, track, api } from 'lwc';
export default class MultiSelectPicklist extends LightningElement {
@api options = [];
@api selectedValues = [];
@api label;
@track values = new Array();
@track optionData;
@track searchString;
@track noResultMessage;
@track showDropdown = false;
@api placeHolder = 'Select';
@api countLabel;
connectedCallback() {
this.initLoad();
}
initLoad() {
this.showDropdown = false;
let optionData = this.options ? (JSON.parse(JSON.stringify(this.options))) : new Array();
let values = this.selectedValues ? (JSON.parse(JSON.stringify(this.selectedValues))) : null;
this.searchString = '';
let count = 0;
if ( values ) {
for ( let i = 0; i < optionData.length; i++ ) {
if ( values.includes( optionData[i].value ) ) {
optionData[i].selected = true;
count++;
}
}
}
this.values = values;
this.optionData = optionData;
if ( count == 1 ) {
this.placeHolder = values[ 0 ];
} else {
this.placeHolder = count + ' ' + this.countLabel + ' Selected';
}
}
filterOptions( event ) {
this.searchString = event.target.value;
if ( this.searchString && this.searchString.length > 0 ) {
this.noResultMessage = '';
if ( this.searchString.length >= 2 ) {
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.noResultMessage = "No results found for '" + this.searchString + "'";
}
}
this.showDropdown = true;
} else {
this.showDropdown = false;
}
}
selectItem( event ) {
let selectedVal = event.currentTarget.dataset.id;
let count = 0;
if ( selectedVal ) {
let options = JSON.parse(
JSON.stringify( this.optionData )
);
for ( let i = 0; i < options.length; i++) {
if (options[i].value === selectedVal ) {
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;
}
if ( options[i].selected ) {
count++;
}
}
this.optionData = options;
if ( count == 1 ) {
this.placeHolder = this.values[ 0 ];
} else {
this.placeHolder = count + ' ' + this.countLabel + ' Selected';
}
let ev = new CustomEvent('selectoption', { detail: this.values });
this.dispatchEvent(ev);
event.preventDefault();
}
}
showOptions(event) {
if ( this.options ) {
this.noResultMessage = '';
this.searchString = event.target.value;
let options = JSON.parse(JSON.stringify( this.optionData ) );
for ( let i = 0; i < options.length; i++ ) {
options[i].isVisible = true;
}
this.showDropdown = ( this.optionData.length > 0 ? true : false );
this.optionData = options;
}
}
handleBlur() {
let count = 0;
for ( let i = 0; i < this.optionData.length; i++ ) {
if ( this.optionData[ i ].selected ) {
count++;
}
}
if ( count == 1 ) {
this.placeHolder = this.values[ 0 ];
} else {
this.placeHolder = count + ' ' + this.countLabel + ' Selected';
}
this.showDropdown = false;
this.searchString = '';
}
@api resetValues( defaultValues ) {
console.log(
'defaultValues is',
JSON.stringify( defaultValues )
);
this.selectedValues = defaultValues;
this.initLoad();
}
}
js-meta.xml:
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>62.0</apiVersion>
<isExposed>false</isExposed>
</LightningComponentBundle>
Sample Lightning Web Component to test:
HTML:
<template>
<div class="slds-box slds-theme--default">
<lightning-layout vertical-align="center">
<lightning-layout-item padding="around-small">
<lightning-input
type="text" label="Account Name"
value={accountName} onchange={handleChange}
class="slds-p-around_medium">
</lightning-input>
</lightning-layout-item>
<lightning-layout-item padding="around-small">
<template lwc:if={optionsForIndustry}>
<c-multi-select-picklist
options={optionsForIndustry}
place-holder="Select Industry"
label="Industry"
onselectoption={handleIndustryChange}
count-label="Industries"
selected-values={defaultIndustries}>
</c-multi-select-picklist>
</template>
</lightning-layout-item>
<lightning-layout-item padding="around-small">
<template lwc:if={optionsForType}>
<c-multi-select-picklist
options={optionsForType}
place-holder="Select Type"
label="Type"
onselectoption={handleTypeChange}
count-label="Types">
</c-multi-select-picklist>
</template>
</lightning-layout-item>
<lightning-layout-item padding="around-small">
<br/>
<lightning-button
variant="brand"
icon-name="utility:record_lookup"
label="Search Accounts"
onclick={handleAccountsSearch}
class="slds-p-around_medium">
</lightning-button>
</lightning-layout-item>
<lightning-layout-item padding="around-small">
<br/>
<lightning-button
variant="destructive"
icon-name="action:refresh"
label="Reset Filters"
onclick={handleFilterReset}
class="slds-p-around_medium">
</lightning-button>
</lightning-layout-item>
</lightning-layout>
</div>
<template lwc:if={listAccounts}>
<lightning-card>
<lightning-datatable
key-field="Id"
data={listAccounts}
columns={columns}
hide-checkbox-column
show-row-number-column>
</lightning-datatable>
</lightning-card>
</template>
</template>
JavaScript:
import { LightningElement , wire} from 'lwc';
import ACCOUNT_OBJECT from '@salesforce/schema/Account';
import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry';
import TYPE_FIELD from '@salesforce/schema/Account.Type';
import { getPicklistValues, getObjectInfo } from 'lightning/uiObjectInfoApi';
import fetchAccounts from '@salesforce/apex/AccountSearchController.fetchAccounts';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
export default class SampleLightningWebComponent extends LightningElement {
accountName;
listAccounts;
selectedTypes;
optionsForType;
selectedIndustries;
optionsForIndustry;
defaultIndustries = [ 'Technology' ];
columns = [
{ label: 'Name', fieldName: 'Name' },
{ label: 'Industry', fieldName: 'Industry' },
{ label: 'Type', fieldName: 'Type' }
];
connectedCallback() {
this.selectedIndustries = this.defaultIndustries;
}
@wire( getObjectInfo, {objectApiName: ACCOUNT_OBJECT} )
objectInfo;
@wire(getPicklistValues, {
recordTypeId: '$objectInfo.data.defaultRecordTypeId',
fieldApiName: INDUSTRY_FIELD
} )
wiredIndustryData( { error, data } ) {
if ( data ) {
this.optionsForIndustry = data.values;
} else if ( error ) {
this.optionsForIndustry = [];
console.error(
error
);
}
}
@wire(getPicklistValues, {
recordTypeId: '$objectInfo.data.defaultRecordTypeId',
fieldApiName: TYPE_FIELD
} )
wiredTypeData( { error, data } ) {
if ( data ) {
this.optionsForType = data.values;
} else if ( error ) {
this.optionsForType = [];
console.error(
error
);
}
}
handleChange( event ) {
if ( event.target.label == 'Account Name' ) {
this.accountName = event.target.value;
}
}
handleIndustryChange( event ){
console.log(
'Selected Industries',
JSON.stringify( event.detail )
);
this.selectedIndustries = event.detail;
}
handleTypeChange( event ){
console.log(
'Selected Types',
JSON.stringify( event.detail )
);
this.selectedTypes = event.detail;
}
handleAccountsSearch() {
fetchAccounts( {
strAccountName : this.accountName,
listSelectedIndustries : this.selectedIndustries,
listSelectedTypes : this.selectedTypes
} )
.then( result => {
console.log(
'result is',
JSON.stringify( result )
);
if ( result.length > 0 ) {
this.listAccounts = result;
} else {
this.dispatchEvent(
new ShowToastEvent( {
title: 'Error!!',
message: 'No matching Accounts found',
variant: 'error',
mode: 'sticky'
} )
);
this.listAccounts = undefined;
}
} )
.catch( error => {
this.dispatchEvent(
new ShowToastEvent( {
title: 'Error!!',
message: JSON.stringify( error ),
variant: 'error',
mode: 'sticky'
} )
);
} )
}
handleFilterReset() {
this.accountName = '';
this.selectedTypes = [];
this.selectedIndustries = [ 'Technology' ];
const childList = this.template.querySelectorAll( "c-multi-select-picklist" );
childList.forEach( ( childElem ) => {
console.log( 'Label is', childElem.label );
if ( childElem.label == 'Industry' ) {
childElem.resetValues( this.selectedIndustries );
} else if ( childElem.label == 'Type' ) {
childElem.resetValues( this.selectedTypes );
}
} );
}
}
js-meta.xml:
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>62.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__Tab</target>
</targets>
</LightningComponentBundle>
Apex Class to fetch Account records:
public with sharing class AccountSearchController {
@AuraEnabled( cacheable = true )
public static List < Account > fetchAccounts(
String strAccountName,
List < String > listSelectedIndustries,
List < String > listSelectedTypes
) {
strAccountName = '%' + strAccountName + '%';
System.debug(
'Account Name is ' +
strAccountName
);
System.debug(
'Account Industries are ' +
listSelectedIndustries
);
System.debug(
'Account Types are ' +
listSelectedTypes
);
List < Account > listAccounts = [
SELECT Id, Name, Industry, Type
FROM Account
WHERE Name LIKE: strAccountName
AND Type IN: listSelectedTypes
AND Industry IN: listSelectedIndustries
];
return listAccounts;
}
}
Output:
lightning-dual-listbox can also be used to avoid the custom reusable Salesforce Lightning Web Component.