Searchable Picklist/Combobox In Lwc

Telegram logo Join our Telegram Channel

Hello Friends! I have created a Searchable Picklist/Combobox for the Lightning Web Component. In which you can type the value or select from the available options. This input element is very similar to the Subject field from the standard Task Object from Salesforce.

Searchable Picklist/Combobox In Lwc


Searchable Combobox component Code

Create a new Lwc with the name searchableCombobox  in your org and copy-paste the below code into the respective JS, HTML, and Meta-XML files.

searchableCombobox.js

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

export default class SearchableCombobox extends LightningElement {
	isOpen = false;
	highlightCounter = null;
	_value = "";

	@api messageWhenInvalid = "Please type or select a value";
	@api required = false;
	@api
	get value() {
		return this._value;
	}

	set value(val) {
		this._value = val;
	}

	@api label = "Subject";

	@track _options = [
		{
			label: "--None--",
			value: "",
		},
		{
			label: "Call",
			value: "Call",
		},
		{
			label: "Email",
			value: "Email",
		},
	];

	@api
	get options() {
		return this._options;
	}

	set options(val) {
		this._options = val || [];
	}

	get tempOptions() {
		let options = this.options;
		if (this.value) {
			options = this.options.filter((op) => op.label.toLowerCase().includes(this.value.toLowerCase()));
		}
		return this.highLightOption(options);
	}

	get isInvalid() {
		return this.required && !this.value;
	}

	get formElementClasses() {
		let classes = "slds-form-element";
		if (this.isInvalid) {
			classes += " slds-has-error";
		}
		return classes;
	}

	handleChange(event) {
		this._value = event.target.value;
		this.fireChange();
	}

	handleInput(event) {
		this.isOpen = true;
	}

	fireChange() {
		this.dispatchEvent(new CustomEvent("change", {detail: {value: this._value}}));
	}

	get classes() {
		let classes = "slds-combobox slds-dropdown-trigger slds-dropdown-trigger_click";
		if (this.isOpen) {
			return classes + " slds-is-open";
		}
		return classes;
	}

	get inputClasses() {
		let inputClasses = "slds-input slds-combobox__input";
		if (this.isOpen) {
			return inputClasses + " slds-has-focus";
		}
		return inputClasses;
	}

	allowBlur() {
		this._cancelBlur = false;
	}

	cancelBlur() {
		this._cancelBlur = true;
	}

	handleDropdownMouseDown(event) {
		const mainButton = 0;
		if (event.button === mainButton) {
			this.cancelBlur();
		}
	}

	handleDropdownMouseUp() {
		this.allowBlur();
	}

	handleDropdownMouseLeave() {
		if (!this._inputHasFocus) {
			this.showList = false;
		}
	}

	handleBlur() {
		this._inputHasFocus = false;
		if (this._cancelBlur) {
			return;
		}
		this.isOpen = false;

		this.highlightCounter = null;
		this.dispatchEvent(new CustomEvent("blur"));
	}

	handleFocus() {
		this._inputHasFocus = true;
		this.isOpen = true;
		this.highlightCounter = null;
		this.dispatchEvent(new CustomEvent("focus"));
	}

	handleSelect(event) {
		this.isOpen = false;
		this.allowBlur();
		this._value = event.currentTarget.dataset.value;
		this.fireChange();
	}

	handleKeyDown(event) {
		if (event.key == "Escape") {
			this.isOpen = !this.isOpen;
			this.highlightCounter = null;
		} else if (event.key === "Enter" && this.isOpen) {
			if (this.highlightCounter !== null) {
				this.isOpen = false;
				this.allowBlur();
				this._value = this.tempOptions[this.highlightCounter].value;
				this.fireChange();
			}
		} else if (event.key === "Enter") {
			this.handleFocus();
		}

		if (event.key === "ArrowDown" || event.key === "PageDown") {
			this._inputHasFocus = true;
			this.isOpen = true;
			this.highlightCounter = this.highlightCounter === null ? 0 : this.highlightCounter + 1;
		} else if (event.key === "ArrowUp" || event.key === "PageUp") {
			this._inputHasFocus = true;
			this.isOpen = true;
			this.highlightCounter = this.highlightCounter === null || this.highlightCounter === 0 ? this.tempOptions.length - 1 : this.highlightCounter - 1;
		}

		if (event.key === "ArrowDown" || event.key === "ArrowUp") {
			this.highlightCounter = Math.abs(this.highlightCounter) % this.tempOptions.length;
		}

		if (event.key === "Home") {
			this.highlightCounter = 0;
		} else if (event.key === "End") {
			this.highlightCounter = this.tempOptions.length - 1;
		}
	}

	highLightOption(options) {
		let classes = "slds-media slds-listbox__option slds-listbox__option_plain slds-media_small";

		return options.map((option, index) => {
			let cs = classes;
			let focused = "";
			if (index === this.highlightCounter) {
				cs = classes + " slds-has-focus";
				focused = "yes";
			}
			return {classes: cs, focused, ...option};
		});
	}

	renderedCallback() {
		this.template.querySelector("[data-focused='yes']")?.scrollIntoView();
	}
}


searchableCombobox.html

<template>
    <div class={formElementClasses}>
        <label
            class="slds-form-element__label"
            for="combobox-id-2"
            id="combobox-label-id-130"
        >
            <abbr
                if:true={required}
                class="slds-required"
                title="required"
            >* </abbr>
            {label}
        </label>
        <div class="slds-form-element__control">
            <div class="slds-combobox_container">
                <div class={classes}>
                    <div
                        class="slds-combobox__form-element slds-input-has-icon slds-input-has-icon_right"
                        role="none"
                    >
                        <div class="slds-form-element">
                            <div class="slds-form-element__control slds-input-has-icon slds-input-has-icon_right ">
                                <lightning-icon
                                    size="x-small"
                                    class="slds-icon slds-input__icon slds-input__icon_right slds-icon-text-default"
                                    icon-name="utility:search"
                                ></lightning-icon>
                                <!-- sldsValidatorIgnoreNextLine -->
                                <input
                                    required={required}
                                    type="text"
                                    id="text-input-id-1"
                                    class={inputClasses}
                                    value={value}
                                    onkeyup={handleChange}
                                    onfocus={handleFocus}
                                    onblur={handleBlur}
                                    onkeydown={handleKeyDown}
                                    oninput={handleInput}
                                    aria-invalid={isInvalid}
                                    aria-describedby="form-error-01"
                                />
                            </div>
                        </div>

                    </div>
                    <div
                        if:true={tempOptions.length}
                        id="listbox-id-4"
                        class="slds-dropdown slds-dropdown_length-5 slds-dropdown_fluid"
                        role="listbox"
                        onmousedown={handleDropdownMouseDown}
                        onmouseup={handleDropdownMouseUp}
                        onmouseleave={handleDropdownMouseLeave}
                    >
                        <ul
                            class="slds-listbox slds-listbox_vertical"
                            role="presentation"
                        >
                            <li
                                for:each={tempOptions}
                                for:item="option"
                                key={option.value}
                                role="presentation"
                                class="slds-listbox__item"
                            >
                                <div
                                    onclick={handleSelect}
                                    data-value={option.value}
                                    class={option.classes}
                                    role="option"
                                    data-focused={option.focused}
                                >
                                    <span class="slds-media__figure slds-listbox__option-icon"></span>
                                    <span class="slds-media__body">
                                        <span
                                            class="slds-truncate"
                                            title={option.label}
                                        >{option.label}</span>
                                    </span>
                                </div>
                            </li>
                        </ul>
                    </div>
                </div>
            </div>
        </div>
        <div
            class="slds-form-element__help"
            if:true={isInvalid}
            id="form-error-01"
        >{messageWhenInvalid}</div>
    </div>
</template>


searchableCombobox.js-meta.xml

<?xml version="1.0" encoding="UTF-8" ?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>55.0</apiVersion>
    <isExposed>false</isExposed>
</LightningComponentBundle>


Use Searchable Combobox component

Add the below code to the HTML file where you want to use the free text Combobox.

<c-searchable-combobox
    required="true"
    label="Searchable Combobox"
    options={options}
    value={value}
    onchange={handleChange}
></c-searchable-combobox>

Add the below code to the JavaScript file where you want to use the free text Combobox.

value = "";
options = [
	{label: "--None--", value: ""},
	{label: "Call", value: "Call"},
	{label: "Email", value: "Email"},
	{label: "Message", value: "Message"},
	{label: "Task", value: "Task"},
	{label: "Visit", value: "Visit"},
	{label: "Other", value: "Other"},
];

handleChange(event) {
	this.value = event.detail.value;
	console.log(this.value);
}


Hope this was helpful. Please raise an issue to this GitHub repo: Searchable-Combobox-Lwc if you find any.


6 comments:
  1. What if the value is different from label in the options passed. How to show the label instead of value after selection. For example - A list of product name is passed as label and value is their Id.

    ReplyDelete
    Replies
    1. As per the above code the search will work on the labels only. In that case the users will be still see the results based on the labels.

      Delete
    2. I am also facing the same issue. I need to populate label different then value.

      Delete
    3. In that case you need to update the getTempOptions method like this:

      get tempOptions() {
      let options = this.options;
      if (this.value) {
      options = this.options.filter((op) => op.label.toLowerCase().includes(this.value.toLowerCase()) || op.value.toLowerCase().includes(this.value.toLowerCase()));
      }
      return this.highLightOption(options);
      }

      this will match both options and labels

      Delete
  2. Hi Rahul, I've multiple search boxes in my searchable combobox component and they are all opening their respective drop downs at the same time when the above code is modified and used. Can you suggest me some work around that? Thanks in advance

    ReplyDelete
    Replies
    1. You should use multiple searchable component instances for multiple combo boxes and make sure that you have not bounded same attribute for all the components.

      Delete

Hi there, comments on this site are moderated, you might need to wait until your comment is published. Spam and promotions will be deleted. Sorry for the inconvenience but we have moderated the comments for the safety of this website users. If you have any concern, or if you are not able to comment for some reason, email us at rahul@forcetrails.com