LWC Multi Lookup - Reusable lightning multi lookup component

In my previous post, I have written about the reusable lookup lwc component. This post is an extension to it i.e., building reusable multi-lookup lwc component.


This reusable multi-lookup component can be used on any lightning web/aura components.

multiLookup.html

<template>
    <div class="slds-combobox_container" onmouseleave={hideOptions}>
        <div class={comboBoxClass} aria-expanded={comboExpanded} aria-haspopup="listbox" role="combobox">
            <div class="slds-combobox__form-element slds-input-has-icon slds-input-has-icon_right" role="none">
                <input type="text" class="slds-input slds-combobox__input slds-combobox__input-value" id="combobox-id"
                    autocomplete="off" aria-controls="listbox-id" role="textbox" placeholder={placeHolder}
                    onkeyup={handleSearch} onclick={showOptions}></input>
                <span class="slds-icon_container slds-icon-utility-search slds-input__icon slds-input__icon_right">
                    <lightning-icon icon-name="utility:search" size="x-small" class="slds-icon-text-default">
                    </lightning-icon>
                </span>
            </div>
            <div id="listbox-id" class="slds-dropdown slds-dropdown_length-5 slds-dropdown_fluid vk-scrollbar"
                role="listbox" onmouseleave={hideOptions}>
                <ul class="slds-listbox slds-listbox_vertical" role="presentation">
                    <template for:each={optionsToDisplay} for:item="option" for:index="index">
                        <li role="presentation" class="slds-listbox__item" key={option} data-index={index}
                            onclick={handleSelection}>
                            <div class="slds-media slds-listbox__option slds-listbox__option_entity slds-listbox__option_has-meta"
                                role="option">
                                <span class="slds-media__figure slds-listbox__option-icon">
                                    <lightning-icon lwc:if={option.isSelected} icon-name="utility:check" size="xx-small"
                                        class="slds-current-color"></lightning-icon>
                                </span>
                                <span if:true={option.hasIcon} class="slds-media__figure slds-listbox__option-icon">
                                    <span class="slds-icon_container">
                                        <lightning-icon icon-name={option.icon} size="x-small"></lightning-icon>
                                    </span>
                                </span>
                                <span class="slds-media__body">
                                    <span class="slds-listbox__option-text slds-listbox__option-text_entity">
                                        {option.label}
                                    </span>
                                </span>
                            </div>
                        </li>
                    </template>
                    <li if:true={noResultsFound} class="slds-p-left_small">&#9785; No results found</li>
                </ul>
            </div>
        </div>
    </div>
    <div if:true={showSelectedOptions} class="slds-listbox_selection-group">
        <ul class="slds-listbox slds-listbox_horizontal" role="listbox" aria-label="Selected Options:"
            aria-orientation="horizontal">
            <template for:each={selectedOptions} for:item="opt" for:index="index">
                <li class="slds-listbox-item" role="presentation" key={opt.index}>
                    <span class="slds-pill" role="option" tabindex="0" aria-selected="true">
                        <span if:true={opt.hasIcon} class="slds-icon_container slds-pill__icon_container">
                            <lightning-icon icon-name={opt.icon} size="x-small">
                            </lightning-icon>
                        </span>
                        <span class="slds-pill__label" title={opt.value}>{opt.value}</span>
                        <span class="slds-icon_container slds-pill__remove" title="Remove">
                            <lightning-button-icon name={index} variant="bare" icon-name="utility:close"
                                size="x-small" alternative-text="Remove selected option" onclick={removeSelectedOption}>
                            </lightning-button-icon>
                        </span>
                    </span>
                </li>
            </template>
        </ul>
    </div>
</template>

multiLookup.js

import { LightningElement, api, track } from "lwc";

const COMBO_BOX = "slds-combobox slds-dropdown-trigger slds-dropdown-trigger_click";
const IS_OPEN = " slds-is-open";
export default class MultiLookup extends LightningElement {
    @api placeHolder = "Search...";
    @track optionsToDisplay = [];
    @track selectedOptions = [];
    showSelectedOptions = false;
    comboBoxClass = COMBO_BOX;
    comboExpanded = false;
    searchKey;
    noResultsFound = false;
    _options = [];
    selOptionsMap = new Map();

    @api
    set options(value) {
        value.forEach((elm, index) => {
            let opt = { ...elm };
            opt.index = index;
            opt.hasIcon = opt.icon ? true : false;
            opt.isSelected = false;
            this.optionsToDisplay.push(opt);
            this._options.push(opt);
        });
    }
    get options() {
        return this._options;
    }

    showOptions() {
        this.comboExpanded = true;
        this.comboBoxClass = COMBO_BOX + IS_OPEN;
    }

    hideOptions() {
        this.comboExpanded = false;
        this.comboBoxClass = COMBO_BOX;
    }

    handleSearch(event) {
        const searchKey = event.target.value;
        this.noResultsFound = false;
        if (searchKey) {
            this.optionsToDisplay = this._options.filter((obj) =>
                obj.value.toLowerCase().includes(searchKey.toLowerCase())
            );
            if (this.optionsToDisplay.length === 0) this.noResultsFound = true;
            this.showOptions();
        } else {
            this.hideOptions();
            this.optionsToDisplay = this._options;
        }
    }

    handleSelection(event) {
        let index = Number(event.currentTarget.dataset.index);
        let selOption = this.optionsToDisplay[index];
        selOption.isSelected = !selOption.isSelected;

        if (selOption.isSelected) this.selOptionsMap.set(selOption.index, selOption);
        else this.selOptionsMap.delete(selOption.index);
        
        this.sendSelectedOptions();
    }

    removeSelectedOption(event) {
        let index = event.target.name;
        let selOption = this.selectedOptions[index];
        selOption.isSelected = false;

        this.selOptionsMap.delete(selOption.index);
        this.sendSelectedOptions();
    }

    sendSelectedOptions() {
        this.selectedOptions = Array.from(this.selOptionsMap.values());
        this.showSelectedOptions = this.selectedOptions.length > 0;
        this.dispatchEvent(new CustomEvent("change", { detail: this.selectedOptions }));
    }

    @api //Method can be called from parent component
    clear() {
        this.selectedOptions = [];
        this.selOptionsMap.forEach((val, key) => {
            val.isSelected = false;
        });
        this.selOptionsMap = new Map();
        this.showSelectedOptions = false;
        this.template.querySelector(".slds-combobox__input-value").value = "";
        this.optionsToDisplay = this._options;
    }
}

multiLookup.css

.slds-input {
    min-height: 1.5rem !important;
    height: 1.7rem !important;
}
.slds-dropdown {
    margin-top: 0 !important;
}
.slds-listbox_vertical .slds-listbox__option_has-meta .slds-media__figure {
    margin-top: 1px;
}
.slds-listbox_vertical .slds-listbox__option_entity {
    padding: 0.25rem;
}
.slds-listbox_vertical .slds-listbox__option_entity .slds-media__figure {
    margin-right: 0.2rem;
}
.slds-listbox__option-icon .slds-current-color {
    color: #0176d3;
}
.slds-listbox_selection-group {
    padding-right: 0.75rem !important;
    overflow: unset !important;
    height: auto;
}
.slds-listbox_selection-group .slds-listbox-item {
    padding: 0;
}
.slds-listbox_horizontal li + li,
.slds-listbox--horizontal li + li {
    padding-left: 0;
}
.slds-pill__icon_container {
    margin-right: 0;
}
.slds-pill__label {
    margin-left: 0.25rem;
}
.vk-scrollbar::-webkit-scrollbar {
    width: 7px;
    height: 7px;
}
.vk-scrollbar::-webkit-scrollbar-track {
    box-shadow: inset 0 0 6px #bfbfbf;
    -webkit-box-shadow: inset 0 0 6px #bfbfbf;
    border-radius: 0.25rem;
}
.vk-scrollbar::-webkit-scrollbar-thumb {
    background: #95989d;
    border-radius: 0.25rem;
}
.vk-scrollbar::-webkit-scrollbar-thumb:hover {
    background: #313235;
}

Now, let's reuse the above multiLookup component.

multiLookupHolder.html

<template>
    <lightning-card title="Multi-lookup Component">
        <lightning-layout vertical-align="center">
            <lightning-layout-item size="6" padding="around-small">
                Selected Opportunities Id's: {selectedOppsIds}
                <c-multi-lookup if:true={showOppLookup} options={opps} place-holder="Select Opportunities..." 
                				onchange={handleOppsChange}></c-multi-lookup>
            </lightning-layout-item>
        </lightning-layout>
    </lightning-card>
</template>

multiLookupHolder.js

import { LightningElement, track, wire } from 'lwc';
import getOpportunities from '@salesforce/apex/OpportunitiesController.getOpportunities';

export default class MultiLookupHolder extends LightningElement {
    @track error;
    @track opps = [];
    @track showOppLookup = false;
    @track selectedOppsIds;

    @wire(getOpportunities)
    wOpps({error,data}){
        if(data){
            for(let i=0; i<data.length; i++){
                let obj = {value: data[i].Id, label: data[i].Name, icon:'standard:opportunity'};
                this.opps.push(obj);
            }
            this.showOppLookup = true;
        }else{
            this.error = error;
        }       
    }
 
   //On Opportunities selection/clear
   handleOppsChange(event){
        let opps = event.detail;
        this.selectedOppsIds = '';
        opps.forEach(opp => {
            this.selectedOppsIds += opp.value+'; ';
        });
    }
}

PS: Hope this helps! Please share your feedback/suggestions/any exciting posts you come across. Happy Coding!

1 Comments

  1. Hi . Venky I have a Query if suppose i have four different list in that case how do i persist value across Lookup

    ReplyDelete
Post a Comment
Previous Post Next Post