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 Combobox component Code
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.
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.
ReplyDeleteAs 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.
DeleteI am also facing the same issue. I need to populate label different then value.
DeleteIn that case you need to update the getTempOptions method like this:
Deleteget 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
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
ReplyDeleteYou 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