Reusable Salesforce Lightning Web Component multi select picklist

Reusable Salesforce Lightning Web Component multi select picklist

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>
                                        &nbsp;&nbsp;{option.label}
                                    </template>
                                    <template if:false={option.selected}>
                                        <span class="slds-media slds-listbox__option_entity slds-truncate">
                                            &nbsp;&nbsp;&nbsp;&nbsp;{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.

Leave a Reply