Bitwise Breakdown

Multi-select Picklist in Salesforce LWC: Core Functionality

Published on 6 min read
Multi-select Picklist Salesforce LWC

In this blog post series, we will break down the development of a Multiselect Picklist Lightning Web Component (LWC) from scratch. We will cover each step in detail, including how to handle event bubbling to close the dropdown when clicking outside the component. By the end of this series, you’ll have a fully functional multiselect picklist ready for your Salesforce projects.

You can also find the complete source code for this component on GitHub: Salesforce Custom Components.


Declare Properties and Methods

Before we dive into the making our hands dirt, let’s declare the variables that we are gonna use for this component.

export default class SCMultiselectPicklist extends LightningElement {
  // non decorated properties
  options = [];
  emptyOptions = [];
  showDropdown = false;
  clickedOnComponent = false;
 
  // api properties
  @api get value() {
    return this.values;
  };
  set value(valueString) {
    this.values = valueString?.split(';') ?? [];
  }
  @api field;
  @api name;
  @api required;
  @api label;
  @api placeholder = 'Select values';
  @api recordTypeId = '012000000000000AAA'; // null record type id
 
  // track properties
  @track values = [];
 
  // getter methods
  get iconName() {
    return this.showDropdown ? 'utility:chevronup' : 'utility:chevrondown';
  }
}

Explanation:

  • We are using getter and setter for the value property as this might change from the parent, and we want to react to the changes which are made from the parent. Another reason is to split the value string which would come as semi-colon separated values.
  • recordTypeId is declared with value 012000000000000AAA which would help us fetch the master picklist value when it was not provided. If we need to fetch picklist values for particular record type then this property need to be sent from the parents.

Fetch Picklist Values

We use the lightning/uiObjectInfoApi module’s getPicklistValues to fetch the picklist options dynamically. The values are wired to our component using a reactive variable.

@wire(getPicklistValues, {
  recordTypeId: '$recordTypeId',
  fieldApiName: '$field'
})
wiredPicklistValues({data, error}) {
  if(data) {
    this.emptyOptions = data.values;
    this.options = data.values.map(option => ({
      ...option,
      checked: this.values.includes(option.value)
    }));
  }
  if(error) {
    console.log('Error fetching picklist values', error);
  }
}

Explanation: This method wires the picklist values from the server to our component, updating the options to checked based on values we get from the parent.

Define HTML Template

Lets write the HTML for input element and drop down container with the associated properties and event handlers.

<template>
  <div class="multi-select-container">
    <!-- input component -->
    <div class="multi-select-box">
      <div
        class="slds-form-element__control slds-input-has-icon slds-input-has-icon_right input-box"
      >
        <label class="slds-form-element__label">
          {label}
          <template if:true={required}>
            <abbr class="slds-required" title="required"> *</abbr>
          </template>
        </label>
        <input
          type="text"
          class="slds-input search-input"
          onclick={handleInputClick}
          placeholder={placeholder}
          name={name}
          onkeyup={handleInputKeyUp}
        />
        <lightning-icon
          icon-name={iconName}
          alternative-text="open dropdown"
          size="xx-small"
          class="dropdown-icon"
          onclick={handleDropdownIconClick}
        >
        </lightning-icon>
      </div>
    </div>
    <!-- dropdown component -->
    <div class="dropdown-container" onclick={handleDropdownClick} if:true={showDropdown}>
      <ul class="dropdown-container__list" id="multiselect-dropdown">
        <template for:each={options} for:item="option" for:index="index">
          <li
            class="dropdown-container__list-item"
            key={option.value}
            onclick={handleListClick}
            data-index={index}
            data-value={option.value}
          >
            <div class="list-item_option" aria-label={option.label}>
              <div class="icon-container">
                <lightning-icon
                  if:true={option.checked}
                  icon-name="utility:check"
                  alternative-text="checked"
                  size="xx-small"
                ></lightning-icon>
              </div>
              <span class="slds-media__body">
                <span class="slds-truncate">{option.label}</span>
              </span>
            </div>
          </li>
        </template>
      </ul>
    </div>
    <!-- pill component goes here -->
  </div>
</template>

Handle Toggle Dropdown

One of the critical functionality of this component is the dropdown behavior. The dropdown should remain open when interacting with the component and close when clicking anywhere outside the component. We achieve this by leveraging event bubbling. There might be other ways too, like by stopping the event propagation, but I prefer this way as it seems easy to understand and maintain.

connectedCallback() {
  document.addEventListener('click', this.handleDocumentClick.bind(this));
}
 
disconnectedCallback() {
  document.removeEventListener('click', this.handleDocumentClick.bind(this));
}
 
handleDocumentClick(event) {
  if (this.clickedOnComponent) {
    this.clickedOnComponent = false;
  } else {
    this.showDropdown = false;
  }
}
 
handleInputClick() {
  this.clickedOnComponent = true;
  this.showDropdown = true;
}
 
handleDropdownIconClick(event) {
  this.clickedOnComponent = true;
  this.showDropdown = !this.showDropdown;
}
 
handleListClick(event) {
  // code to select the option goes here
  this.clickedOnComponent = true;
}

Explanation:

  • Event Bubbling is a mechanism where an event propagates up from the innermost element to the outer elements. We listen for a click event on the entire document and decide whether to close the dropdown based on where the click originated. More on event bubbling - Geeksforgeeks (Event Bubbling).
  • If the click happens inside the component, we prevent the dropdown from closing. If it happens outside, we hide the dropdown. This is controlled via the clickedOnComponent flag.

Handle User Inputs

When the user clicks on the input box, we show the dropdown with all available picklist values. As they type, we filter the picklist values based on the input values.

handleInputKeyUp(event) {
  this.options = this.emptyOptions
    .filter((option) =>
      option.value.toLowerCase().includes(event.target.value.toLowerCase())
    )
    .map((option) => ({
      ...option,
      ariaLabel: option.label + ' ' + option.checked,
      checked: this.values.includes(option.value)
    }));
}

Explanation: This method captures user input, filters the picklist options, and updates the options.

Toggle Values

When a user selects an option, we either add or remove the value from the selected list.

handleListClick(event) {
  this.selectPicklistValue(event.currentTarget.dataset.value);
  this.clickedOnComponent = true;
}
 
selectPicklistValue(value) {
  const index = this.values.indexOf(value);
  if (index === -1) {
    this.values.push(value);
  } else {
    this.values.splice(index, 1);
  }
  this.toggleChecked(value);
  this.dispatchEvent(
    new CustomEvent('valuechange', {
      detail: { value: this.values.join(";"), name: this.name }
    })
  );
}
 
toggleChecked(name) {
  this.options = this.options.map((option) => ({
    ...option,
    checked: name === option.value ? !option.checked : option.checked,
    ariaLabel : option.label + ' ' + name === option.value ? !option.checked : option.checked
  }));
}

Explanation:

  • The handleListClick method handles the event when a user clicks on an option.
  • The selectPicklistValue method updates the values and send custom event to parent.
  • The toggleChecked method updates the options by toggling the matching option.

Display Selected Values

The selected values are displayed as pills below the input box. Each pill has a close icon to remove the value from the list.

<div class="pill-container">
  <template for:each={values} for:item="value">
    <lightning-pill
      label={value}
      key={value}
      data-value={value}
      onremove={handleCrossIconClick}
    ></lightning-pill>
  </template>
</div>

The user can remove values by clicking on the close icon, which triggers the handleCrossIconClick method.

handleCrossIconClick(event) {
  const index = this.values.indexOf(event.currentTarget.dataset.value);
  this.values.splice(index, 1);
  this.toggleChecked(event.currentTarget.dataset.value);
  this.dispatchEvent(
    new CustomEvent('valuechange', {
      detail: { value: this.values.join(";"), name: this.name }
    })
  );
}

Styling the Component

We want the input element, dropdown and pills to appear neatly aligned, with scrollable options for dropdown.

Here’s the CSS file that we need:

.multi-select-container {
  position: relative;
}
 
.dropdown-icon {
  position: absolute;
  right: 8px;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  height: calc(100% - 22px);
  cursor: pointer;
}
 
.dropdown-container {
  position: absolute;
  width: 100%;
  left: 0;
  background-color: white;
  z-index: 10;
  border: 1px solid #797979;
  border-radius: 4px;
  overflow: hidden;
}
 
.dropdown-container__list {
  padding: 0;
  list-style: none;
  max-height: 150px;
  overflow-y: scroll;
}
 
.dropdown-container__list-item {
  padding: 4px 12px;
}
 
.dropdown-container__list-item:hover,
.dropdown-container__list-item:focus {
  background-color: #33333322;
}
 
/* focused item css goes here */
 
.icon-container {
  height: 14px;
  width: 14px;
  display: inline-block;
}
 
.list-item_option {
  display: flex;
  gap: 4px;
  cursor: pointer;
}
 
.pill-container {
  margin-top: 5px;
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}

Explanation:

  • We give the dropdown container a subtle shadow for better visual separation from the page.
  • The max-height property ensures that if there are many options, the list doesn’t extend too far down the page. Instead, it scrolls within a defined height.
  • We also add hover effects on list items for visual feedback.

Conclusion

In this post, we walked through the structure and logic of building a multiselect picklist in Salesforce LWC. We covered how to handle picklist values, filter options based on user input, manage selected values, and control dropdown visibility using event bubbling.

To see the full code and more, visit the GitHub repository: Salesforce Custom Components.

In the next post, we will go deeper into advanced features like performance optimizations, and accessibility improvements. Stay tuned!