import { LightningElement, api } from "lwc";
const MINIMAL_SEARCH_TERM_LENGTH = 2; // Min number of chars required to search
const SEARCH_DELAY = 300; // Wait 300 ms after user stops typing then, peform search
export default class Lookup extends LightningElement {
@api label;
@api required;
@api placeholder = "";
@api isMultiEntry = false;
@api errors = [];
@api scrollAfterNItems;
searchTerm = "";
searchResults = [];
hasFocus = false;
loading = false;
isDirty = false;
cleanSearchTerm;
blurTimeout;
searchThrottlingTimeout;
curSelection = [];
// EXPOSED FUNCTIONS
@api
set selection(initialSelection) {
this.curSelection = Array.isArray(initialSelection)
? initialSelection
: [initialSelection];
}
get selection() {
return this.curSelection;
}
@api
setSearchResults(results) {
// Reset the spinner
this.loading = false;
// Clone results before modifying them to avoid Locker restriction
const resultsLocal = JSON.parse(JSON.stringify(results));
// Format results
this.searchResults = resultsLocal.map(result => {
// Clone and complete search result if icon is missing
if (this.searchTerm.length > 0) {
const regex = new RegExp(`(${this.searchTerm})`, "gi");
result.titleFormatted = result.title
? result.title.replace(regex, "$1")
: result.title;
result.subtitleFormatted = result.subtitle
? result.subtitle.replace(regex, "$1")
: result.subtitle;
}
if (typeof result.icon === "undefined") {
const { id, sObjectType, title, subtitle } = result;
return {
id,
sObjectType,
icon: "standard:default",
title,
subtitle
};
}
return result;
});
}
@api
getSelection() {
return this.curSelection;
}
// INTERNAL FUNCTIONS
updateSearchTerm(newSearchTerm) {
this.searchTerm = newSearchTerm;
// Compare clean new search term with current one and abort if identical
const newCleanSearchTerm = newSearchTerm
.trim()
.replace(/\*/g, "")
.toLowerCase();
if (this.cleanSearchTerm === newCleanSearchTerm) {
return;
}
// Save clean search term
this.cleanSearchTerm = newCleanSearchTerm;
// Ignore search terms that are too small
if (newCleanSearchTerm.length < MINIMAL_SEARCH_TERM_LENGTH) {
this.searchResults = [];
return;
}
// Apply search throttling (prevents search if user is still typing)
if (this.searchThrottlingTimeout) {
clearTimeout(this.searchThrottlingTimeout);
}
// eslint-disable-next-line @lwc/lwc/no-async-operation
this.searchThrottlingTimeout = setTimeout(() => {
// Send search event if search term is long enough
if (this.cleanSearchTerm.length >= MINIMAL_SEARCH_TERM_LENGTH) {
// Display spinner until results are returned
this.loading = true;
const searchEvent = new CustomEvent("search", {
detail: {
searchTerm: this.cleanSearchTerm,
selectedIds: this.curSelection.map(element => element.id)
}
});
this.dispatchEvent(searchEvent);
}
this.searchThrottlingTimeout = null;
}, SEARCH_DELAY);
}
isSelectionAllowed() {
if (this.isMultiEntry) {
return true;
}
return !this.hasSelection();
}
hasResults() {
return this.searchResults.length > 0;
}
hasSelection() {
return this.curSelection.length > 0;
}
// EVENT HANDLING
handleInput(event) {
// Prevent action if selection is not allowed
if (!this.isSelectionAllowed()) {
return;
}
this.updateSearchTerm(event.target.value);
}
handleResultClick(event) {
const recordId = event.currentTarget.dataset.recordid;
// Save selection
let selectedItem = this.searchResults.filter(
result => result.id === recordId
);
if (selectedItem.length === 0) {
return;
}
selectedItem = selectedItem[0];
const newSelection = [...this.curSelection];
newSelection.push(selectedItem);
this.curSelection = newSelection;
this.isDirty = true;
// Reset search
this.searchTerm = "";
this.searchResults = [];
// Notify parent components that selection has changed
this.dispatchEvent(new CustomEvent("selectionchange"));
}
handleComboboxClick() {
// Hide combobox immediatly
if (this.blurTimeout) {
window.clearTimeout(this.blurTimeout);
}
this.hasFocus = false;
}
handleFocus() {
// Prevent action if selection is not allowed
if (!this.isSelectionAllowed()) {
return;
}
this.hasFocus = true;
}
handleBlur() {
// Prevent action if selection is not allowed
if (!this.isSelectionAllowed()) {
return;
}
// Delay hiding combobox so that we can capture selected result
// eslint-disable-next-line @lwc/lwc/no-async-operation
this.blurTimeout = window.setTimeout(() => {
this.hasFocus = false;
this.blurTimeout = null;
}, 300);
}
handleRemoveSelectedItem(event) {
const recordId = event.currentTarget.name;
this.curSelection = this.curSelection.filter(item => item.id !== recordId);
this.isDirty = true;
// Notify parent components that selection has changed
this.dispatchEvent(new CustomEvent("selectionchange"));
}
handleClearSelection() {
this.curSelection = [];
this.isDirty = true;
// Notify parent components that selection has changed
this.dispatchEvent(new CustomEvent("selectionchange"));
}
// STYLE EXPRESSIONS
get getContainerClass() {
let css = "slds-combobox_container slds-has-inline-listbox ";
if (this.hasFocus && this.hasResults()) {
css += "slds-has-input-focus ";
}
if (this.errors.length > 0) {
css += "has-custom-error";
}
return css;
}
get getDropdownClass() {
let css =
"slds-combobox slds-dropdown-trigger slds-dropdown-trigger_click ";
if (
this.hasFocus &&
this.cleanSearchTerm &&
this.cleanSearchTerm.length >= MINIMAL_SEARCH_TERM_LENGTH
) {
css += "slds-is-open";
}
return css;
}
get getInputClass() {
let css = "slds-input slds-combobox__input has-custom-height ";
if (
this.errors.length > 0 ||
(this.isDirty && this.required && !this.hasSelection())
) {
css += "has-custom-error ";
}
if (!this.isMultiEntry) {
css +=
"slds-combobox__input-value " +
(this.hasSelection() ? "has-custom-border" : "");
}
return css;
}
get getComboboxClass() {
let css = "slds-combobox__form-element slds-input-has-icon ";
if (this.isMultiEntry) {
css += "slds-input-has-icon_right";
} else {
css += this.hasSelection()
? "slds-input-has-icon_left-right"
: "slds-input-has-icon_right";
}
return css;
}
get getSearchIconClass() {
let css = "slds-input__icon slds-input__icon_right ";
if (!this.isMultiEntry) {
css += this.hasSelection() ? "slds-hide" : "";
}
return css;
}
get getClearSelectionButtonClass() {
return (
"slds-button slds-button_icon slds-input__icon slds-input__icon_right " +
(this.hasSelection() ? "" : "slds-hide")
);
}
get getSelectIconName() {
return this.hasSelection() ? this.curSelection[0].icon : "standard:default";
}
get getSelectIconClass() {
return (
"slds-combobox__input-entity-icon " +
(this.hasSelection() ? "" : "slds-hide")
);
}
get getInputValue() {
if (this.isMultiEntry) {
return this.searchTerm;
}
return this.hasSelection() ? this.curSelection[0].title : this.searchTerm;
}
get getInputTitle() {
if (this.isMultiEntry) {
return "";
}
return this.hasSelection() ? this.curSelection[0].title : "";
}
get getListboxClass() {
return (
"slds-listbox slds-listbox_vertical slds-dropdown slds-dropdown_fluid " +
(this.scrollAfterNItems
? "slds-dropdown_length-with-icon-" + this.scrollAfterNItems
: "")
);
}
get isInputReadonly() {
if (this.isMultiEntry) {
return false;
}
return this.hasSelection();
}
get isExpanded() {
return this.hasResults();
}
}