Hello friends, today we are going to explore How to add MultiSelect Picklist In LWC Datatable Inline Edit. One common requirement is to add a multi-select picklist within a Datatable component to allow users to select multiple values from a list. After so many requests and comments now I developed MultiSelect Picklist In LWC Datatable.
Also, check this: Refresh Standard Related List in LWC
Key Highlights :
- We can select multiple options and save them directly in LWC Datatable.
- Easy to understand and customize.
- Use Multiselect Picklist Blog code.
- Create a MultiSelect-Picklist type field in Datatable.
- We can edit this field.
- We can show/hide this picklist by the pen icon.
- Picklist options fetch dynamically for LWC Datatable.
Code :
AccountDataController.cls: I create a Prospect__c multi-select picklist custom fields so before running the code create this. Values (Prospect,Customer, Pending)
public class AccountDataController { @AuraEnabled (cacheable=true) public static List<Account> fetchAccounts(){ return [SELECT Id, Name, Type, Phone, Prospect__c FROM Account LIMIT 10]; } }
First of all, we create a custom multi-select picklist LWC component. So we can use this in our LWC component later.
MultiSelectPickList.Html :
<template> <!-- Start Header Label Passed from Parent --> <template if:true={label}> <label class="slds-form-element__label">{label}</label> </template> <!-- End Header Label Passed from Parent --> <div class="slds-combobox_container"> <div class="slds-combobox slds-dropdown-trigger slds-dropdown-trigger_click slds-is-open" aria-expanded="true" aria-haspopup="listbox" role="combobox" onmouseleave={handleMouseOut} onmouseenter={handleMouseIn}> <!-- Search Input --> <div class="slds-combobox__form-element slds-input-has-icon slds-input-has-icon_right" role="none"> <lightning-input disabled={disabled} class="inputBox" placeholder="Select an Option" onblur={handleBlur} onclick={showOptions} onkeyup={filterOptions} value={searchString} variant="label-hidden" id="combobox-id-1" data-id="tel"></lightning-input> <lightning-icon class="slds-input__icon" icon-name="utility:down" size="x-small" alternative-text="downicon"></lightning-icon> </div> <!-- Dropdown List --> <template if:true={showDropdown}> <div id="listbox-id-1" class={computedDropdownClass}> <ul class="slds-listbox slds-listbox_vertical recordListBox slds-dropdown_left slds-dropdown_length-with-icon-10 slds-dropdown_fluid" role="presentation"> <template if:false={noResultMessage}> <template for:each={optionData} for:item="option"> <li key={option.value} data-id={option.value} onmousedown={selectItem} class="slds-listbox__item eachItem" if:true={option.isVisible}> <template if:true={option.selected}> <lightning-icon icon-name="utility:check" size="x-small" alternative-text="icon"></lightning-icon> </template> <span class="slds-media slds-listbox__option_entity verticalAlign slds-truncate">{option.label}</span> </li> </template> </template> <template if:true={noResultMessage}> <li class="slds-listbox__item"> <span class="slds-media slds-listbox__option_entity verticalAlign slds-truncate">{noResultMessage}</span> </li> </template> </ul> </div> </template> </div> </div> <!-- Multi Select Pills --> <!--<template for:each={optionData} for:item="option"> <template if:true={option.selected}> <lightning-pill label={option.label} key={option.value} name={option.value} onremove={closePill}> <lightning-icon icon-name="custom:custom11" alternative-text="Account"></lightning-icon> </lightning-pill> </template> </template>--> </template>
MultiSelectPickList.Js :
import { LightningElement, track, api } from 'lwc'; export default class MultiSelectPickList extends LightningElement { @api options; @api selectedValue; @api selectedValues = []; @api label; @api disabled = false; @api multiSelect = false; @track value; @track values = []; @track optionData; @track searchString; @track noResultMessage; @track showDropdown = false; @api yaxis; @api connectedCallback() { this.showDropdown = false; var optionData = this.options ? (JSON.parse(JSON.stringify(this.options))) : null; var value = this.selectedValue ? (JSON.parse(JSON.stringify(this.selectedValue))) : null; var val = []; if (this.selectedValues) { val = (JSON.parse(JSON.stringify(this.selectedValues))).split(';'); } var values = val; if (value || values) { var searchString; var count = 0; for (var i = 0; i < optionData.length; i++) { if (this.multiSelect) { if (values.includes(optionData[i].value)) { optionData[i].selected = true; count++; } } else { if (optionData[i].value == value) { searchString = optionData[i].label; } } } if (this.multiSelect) this.searchString = count + ' Option(s) Selected'; else this.searchString = searchString; } this.value = value; this.values = values; this.optionData = optionData; } renderedCallback() { this.template.querySelector("[data-id=tel]")?.focus(); } @api clearAll() { var optionData = this.options ? (JSON.parse(JSON.stringify(this.options))) : null; for (var i = 0; i < optionData.length; i++) { if (this.multiSelect) { optionData[i].selected = false; } } this.searchString = 0 + ' Option(s) Selected'; this.selectedValues = []; this.optionData = optionData; } filterOptions(event) { this.searchString = event.target.value; if (this.searchString && this.searchString.length > 0) { this.noResultMessage = ''; if (this.searchString.length >= 2) { var flag = true; for (var i = 0; i < this.optionData.length; i++) { if (this.optionData[i].label.toLowerCase().trim().startsWith(this.searchString.toLowerCase().trim())) { this.optionData[i].isVisible = true; flag = false; } else { this.optionData[i].isVisible = false; } } if (flag) { this.noResultMessage = "No results found for '" + this.searchString + "'"; } } this.showDropdown = true; } else { this.showDropdown = false; } console.log(this.optionData); } selectItem(event) { var selectedVal = event.currentTarget.dataset.id; if (selectedVal) { var count = 0; var options = JSON.parse(JSON.stringify(this.optionData)); for (var i = 0; i < options.length; i++) { if (options[i].value === selectedVal) { if (this.multiSelect) { if (this.values.includes(options[i].value)) { this.values.splice(this.values.indexOf(options[i].value), 1); } else { this.values.push(options[i].value); } options[i].selected = options[i].selected ? false : true; } else { this.value = options[i].value; this.searchString = options[i].label; } } if (options[i].selected) { count++; } } this.optionData = options; if (this.multiSelect) { this.searchString = count + ' Option(s) Selected'; let ev = new CustomEvent('selectoption', { detail: this.values }); this.dispatchEvent(ev); } if (!this.multiSelect) { let ev = new CustomEvent('selectoption', { detail: this.value }); this.dispatchEvent(ev); } if (this.multiSelect) event.preventDefault(); else this.showDropdown = false; } } showOptions() { if (this.disabled == false && this.options) { this.noResultMessage = ''; this.searchString = ''; var options = JSON.parse(JSON.stringify(this.optionData)); for (var i = 0; i < options.length; i++) { options[i].isVisible = true; } if (options.length > 0) { this.showDropdown = true; } this.optionData = options; } } closePill(event) { var value = event.currentTarget.name; var count = 0; var options = JSON.parse(JSON.stringify(this.optionData)); for (var i = 0; i < options.length; i++) { if (options[i].value === value) { options[i].selected = false; this.values.splice(this.values.indexOf(options[i].value), 1); } if (options[i].selected) { count++; } } this.optionData = options; if (this.multiSelect) { this.searchString = count + ' Option(s) Selected'; let ev = new CustomEvent('selectoption', { detail: this.values }); this.dispatchEvent(ev); } } handleBlur() { var previousLabel; var count = 0; for (var i = 0; i < this.optionData.length; i++) { if (this.optionData[i].value === this.value) { previousLabel = this.optionData[i].label; } if (this.optionData[i].selected) { count++; } } if (this.multiSelect) { this.searchString = count + ' Option(s) Selected'; } else { this.searchString = previousLabel; } this.showDropdown = false; let ev = new CustomEvent('closepicklist', { detail: 'close' }); this.dispatchEvent(ev); } handleMouseOut() { //this.showDropdown = false; } handleMouseIn() { //this.showDropdown = true; } get computedDropdownClass() { let axis = this.yaxis; axis = axis + 300; let directionCss = ''; if (window.innerHeight < axis) { directionCss = 'showUpperDropDown'; } else { directionCss = ''; } let classs = 'slds-dropdown slds-dropdown_length-5 slds-dropdown_fluid ' + directionCss; return classs; /*if (this.showDropdown) { if (dropdownHeight === 'standard') { if (window.innerHeight <= VIEWPORT_HEIGHT_SMALL) { dropdownLengthClass = 'slds-dropdown_length-with-icon-7'; } else { dropdownLengthClass = 'slds-dropdown_length-with-icon-10'; } } else if (dropdownHeight === 'small') { dropdownLengthClass = 'slds-dropdown_length-with-icon-5'; } }*/ /*return classSet( `slds-listbox slds-listbox_vertical slds-dropdown slds-dropdown_fluid ${dropdownLengthClass}` ) .add({ 'slds-dropdown_left': alignment === 'left' || alignment === 'auto', 'slds-dropdown_center': alignment === 'center', 'slds-dropdown_right': alignment === 'right', 'slds-dropdown_bottom': alignment === 'bottom-center', 'slds-dropdown_bottom slds-dropdown_right slds-dropdown_bottom-right': alignment === 'bottom-right', 'slds-dropdown_bottom slds-dropdown_left slds-dropdown_bottom-left': alignment === 'bottom-left' }) .toString();*/ } }
MultiSelectPickList.css :
.verticalAlign { cursor: pointer; padding: 0px 5px !important; } .slds-dropdown { padding:0px !important; } .recordListBox { margin-top:0px !important; overflow-y: scroll; } .slds-listbox li { padding: .45rem 0.7rem !important; display: flex; } .inputBox input { padding-left: 10px; } .eachItem:hover { background-color: #F1F1F1; cursor: pointer; } /* For Scrolling */ ::-webkit-scrollbar { width: 7px; height: 7px; } ::-webkit-scrollbar-track { display: none !important; } ::-webkit-scrollbar-thumb { border-radius: 10px; background: rgba(0,0,0,0.4); } .slds-dropdown_fluid{ margin-top: 0px; } .showUpperDropDown{ transform: translate3d(0px, -102px, 0px); top: 0px; left: 0px; will-change: transform; }
Now we create a multi-select column component.
MultipicklistColumn.HTML :
<template> <div class="picklistSection" id="picklist"> <div if:true={showPicklist} class="picklist-section"> <c-multi-select-pick-list multi-select="true" onselectoption={handleSelectOptionList} options={options} selected-values={value} label="" onclosepicklist={closePicklist}> </c-multi-select-pick-list> </div> <div if:false={showPicklist} class="slds-table_edit_container slds-is-relative"> <span class="slds-grid slds-grid_align-spread slds-cell-edit"> <span class="slds-truncate" title={value}>{value}</span> <button data-id={context} class="slds-button slds-button_icon slds-cell-edit__button slds-m-left_x-small" tabindex="-1" title="Edit" name="tes" onclick={handleClick} data-name="myButtonName" > <svg class="slds-button__icon slds-button__icon_hint slds-button__icon_lock slds-button__icon_small slds-button__icon_edit slds-icon slds-icon-text-default slds-icon_xx-small" aria-hidden="true"> <use xlink:href="/_slds/icons/utility-sprite/svg/symbols.svg?cache=9.37.1#edit"></use> </svg> <span class="slds-assistive-text">Edit</span> </button> </span> </div> </div> </template>
MultipicklistColumn.JS: In this component, I used a static resource LWCDatatableMultiPicklist, Unzip and Upload this as a static resource and the name will be LWCDatatableMultiPicklist.
import { LightningElement, api, track } from 'lwc'; import { loadStyle } from 'lightning/platformResourceLoader'; import LWCDatatableMultiPicklist from '@salesforce/resourceUrl/LWCDatatableMultiPicklist'; export default class MultipicklistColumn extends LightningElement { @api label; @api placeholder; @api options; @api value; @api context; @track showPicklist = false; isRendered = false; @track yaxis; renderedCallback() { if (!this.isRendered) { Promise.all([ loadStyle(this, LWCDatatableMultiPicklist), ]).then(() => { }); } this.isRendered = true; } handleSelectOptionList(event) { //show the selected value on UI let picklistValues = ''; if (event.detail) { picklistValues = event.detail.join(';'); } console.log(picklistValues); this.value = picklistValues; //fire event to send context and selected value to the data table this.dispatchEvent(new CustomEvent('picklistchanged', { composed: true, bubbles: true, cancelable: true, detail: { data: { context: this.context, value: this.value } } })); } handleClick(event) { this.yaxis = event.clientY; this.showPicklist = true; } closePicklist() { this.showPicklist = false; } }
MultipicklistColumn.CSS:
.picklist-section{ margin-top: -1rem; margin-left: -0.5rem; position: absolute !important; min-width: 10%; z-index: 99999999999999999999999999; } .picklist-section .slds-dropdown{ //position: fixed !important; max-height: 120px; max-width: fit-content; overflow: auto; } .slds-grid .slds-hyphenate { width: 100% } .slds-grid .slds-truncate { width: 100% } .slds-listbox{ z-index: 99999999999999999999999999!important; }
2. LWCCustomDatatableType Component: We create a custom type Datatable here which extends standard LWC Datatable. Also, create an extra HTML file multiselectpicklistColumn, so we can put the multiselect component here. Below is an image of the structure.
multiselectpicklistColumn.HTML :
<template> <c-multipicklist-column label={typeAttributes.label} value={typeAttributes.value} placeholder={typeAttributes.placeholder} options={typeAttributes.options} context={typeAttributes.context}> </c-multipicklist-column> </template>
LWCCustomDatatableType.JS :
import LightningDatatable from 'lightning/datatable'; import multiselectpicklistColumn from './multiselectpicklistColumn.html'; export default class LWCCustomDatatableType extends LightningDatatable { static customTypes = { multiselectpicklistColumn: { template: multiselectpicklistColumn, editTemplate: multiselectpicklistColumn, standardCellLayout: true, typeAttributes: ['label', 'placeholder', 'options', 'value', 'context', 'variant','name'] } }; }
Now Create Datatable with custom Types
lWCDatatableWithMultipicklist.HTML :
<template> <!-- create card --> <lightning-card variant="Narrow" title="Multiselect PickList In LWC Inline Datatable Edit" icon-name="standard:folder" class="cardSpinner"> <!-- loader --> <div if:true={showSpinner}> <lightning-spinner alternative-text="Loading..." variant="brand"> </lightning-spinner> </div> <!-----/loader--------> <div class="slds-var-p-around_small"> <template if:true={data}> <c-l-w-c-custom-datatable-type key-field="Id" data={data} columns={columns} onpicklistchanged={multpicklistChanged} onvalueselect={handleSelection} draft-values={draftValues} oncellchange={handleCellChange} onsave={handleSave} oncancel={handleCancel}> </c-l-w-c-custom-datatable-type> </template> </div> </lightning-card> </template>
lWCDatatableWithMultipicklist.JS:
import { LightningElement, track, wire } from 'lwc'; import fetchAccounts from '@salesforce/apex/AccountDataController.fetchAccounts'; import ACCOUNT_OBJECT from '@salesforce/schema/Account'; import PROSPECT_FIELD from '@salesforce/schema/Account.Prospect__c'; import { updateRecord } from 'lightning/uiRecordApi'; import { ShowToastEvent } from 'lightning/platformShowToastEvent'; import { refreshApex } from '@salesforce/apex'; import { getPicklistValues, getObjectInfo } from 'lightning/uiObjectInfoApi'; const columns = [ { label: 'Name', fieldName: 'Name', editable: true }, { label: 'Phone', fieldName: 'Phone', type: 'phone', editable: true }, { label: 'Prospect', fieldName: 'Prospect', type: 'multiselectpicklistColumn', editable: false, typeAttributes: { placeholder: 'Choose Type', options: { fieldName: 'pickListOptions' }, value: { fieldName: 'Prospect__c' }, // default value for picklist, context: { fieldName: 'Id' } // binding account Id with context variable to be returned back } }, ] export default class LWCDatatableWithMultipicklist extends LightningElement { columns = columns; showSpinner = false; @track data = []; @track accountData; @track draftValues = []; lastSavedData = []; @track pickListOptions; isRenderd = false; @wire(getObjectInfo, { objectApiName: ACCOUNT_OBJECT }) objectInfo; //fetch picklist options @wire(getPicklistValues, { recordTypeId: "$objectInfo.data.defaultRecordTypeId", fieldApiName: PROSPECT_FIELD }) wirePickList({ error, data }) { if (data) { this.pickListOptions = data.values; } else if (error) { console.log(error); } } //here I pass picklist option so that this wire method call after above method @wire(fetchAccounts, { pickList: '$pickListOptions' }) accountDataWe(result) { this.accountData = result; if (result.data) { this.data = JSON.parse(JSON.stringify(result.data)); this.data.forEach(ele => { ele.pickListOptions = this.pickListOptions; }) this.lastSavedData = JSON.parse(JSON.stringify(this.data)); } else if (result.error) { this.data = undefined; } }; updateDataValues(updateItem) { let copyData = JSON.parse(JSON.stringify(this.data)); copyData.forEach(item => { if (item.Id === updateItem.Id) { for (let field in updateItem) { item[field] = updateItem[field]; } } }); //write changes back to original data this.data = [...copyData]; } updateDraftValues(updateItem) { let draftValueChanged = false; let copyDraftValues = [...this.draftValues]; //store changed value to do operations //on save. This will enable inline editing & //show standard cancel & save button copyDraftValues.forEach(item => { if (item.Id === updateItem.Id) { for (let field in updateItem) { item[field] = updateItem[field]; } draftValueChanged = true; } }); if (draftValueChanged) { this.draftValues = [...copyDraftValues]; } else { this.draftValues = [...copyDraftValues, updateItem]; } } //listener handler to get the context and data //updates datatable multpicklistChanged(event) { event.stopPropagation(); let dataRecieved = event.detail.data; console.log(event.detail.data); let updatedItem = { Id: dataRecieved.context, Prospect__c: dataRecieved.value}; console.log(updatedItem); this.updateDraftValues(updatedItem); this.updateDataValues(updatedItem); } //handler to handle cell changes & update values in draft values handleCellChange(event) { console.log(event.detail.draftValues[0]); let draftValues = event.detail.draftValues; draftValues.forEach(ele=>{ this.updateDraftValues(ele); }) } handleSave(event) { this.showSpinner = true; this.saveDraftValues = this.draftValues; const recordInputs = this.saveDraftValues.slice().map(draft => { const fields = Object.assign({}, draft); return { fields }; }); // Updateing the records using the UiRecordAPi const promises = recordInputs.map(recordInput => updateRecord(recordInput)); Promise.all(promises).then(res => { this.showToast('Success', 'Records Updated Successfully!', 'success', 'dismissable'); this.draftValues = []; return this.refresh(); }).catch(error => { console.log(error); this.showToast('Error', 'An Error Occured!!', 'error', 'dismissable'); }).finally(() => { this.draftValues = []; this.showSpinner = false; }); } handleCancel(event) { //remove draftValues & revert data changes this.data = JSON.parse(JSON.stringify(this.lastSavedData)); this.draftValues = []; } showToast(title, message, variant, mode) { const evt = new ShowToastEvent({ title: title, message: message, variant: variant, mode: mode }); this.dispatchEvent(evt); } // This function is used to refresh the table once data updated async refresh() { await refreshApex(this.accountData); } handleChange(event){ console.log(event.detail); } }
lWCDatatableWithMultipicklist.css :
.cardSpinner{ position: relative; }
lWCDatatableWithMultipicklist.js-meta.xml:
<?xml version="1.0"?> <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata"> <apiVersion>57.0</apiVersion> <isExposed>true</isExposed> <targets> <target>lightning__HomePage</target> </targets> </LightningComponentBundle>
Output :
Reference :
- LWC Datatable
- Inline Editing in lightning-datatable in LWC Salesforce
- Picklist in LWC Datatable Inline Edit
10 comments
I am not able to see edit svg icon in the LWC table
This works great, but the combobox options are cut off by the parent container. How can I ensure that, if there are several combobox options, they overflow and do not get cut off by the parent container?
You have to setup css in child component.
Hi, thanks for the solution. Could you please help with the data-inputable issue? Already set it to true in all possible places :), but still getting an error: Editable custom types must define an editTemplate that includes an element with attribute data-inputable set to “true”.
Hi, did you get any solution for data-inputable true error?
Still not
Can you guide on the css then? in your video it looks perfect as out of the box, but I am getting both the edit icons. Could you please help?
book a meeting https://topmate.io/rijwan_mohmmed, I will help you
please add css here, whatever i tried still parent container is cutting off the conbobox dropdown
Hi, I am facing [Cannot read properties of undefined (reading ‘valid’)]
processInlineEditFinish() error upon clicking save.
This is the underlying script (datatable.js)
function processInlineEditFinish(dt, reason, rowKeyValue, colKeyValue) {
const state = dt.state;
const inlineEditState = state.inlineEdit;
const shouldSaveData = reason !== ‘edit-canceled’ && !(inlineEditState.massEditEnabled && reason === ‘lost-focus’) && isValidCell(dt.state, rowKeyValue, colKeyValue);
if (shouldSaveData) {
const panel = dt.template.querySelector(IEDIT_PANEL_SELECTOR);
const editValue = panel.value;
const isValidEditValue = panel.validity.valid; (Here validity returns undefined)