How to display Multi Level Dependent Picklists in a Salesforce Lightning Web Component (LWC)

  

How to display Multi Level Dependent Picklists in a Salesforce Lightning Web Component (LWC)

 

Hi everyone!!!

This is Vijaya Kumar Golla. I have been looking for a solution to display multi level dependent picklist in salesforce lightning web component, but I could not find it in anywhere in any site/blog.

Here is the good news for those who are looking for solution to display multi level dependent picklists in salesforce LWC.

 First I am going to cover three level dependency picklists component and then I will show you how easy to enhance it with small changes to add one more dependent picklist so that it becomes a four level dependency picklists component.

 

Schama Changes :


In order to demonstrate this, I have created a custom object (Customer_Address__c) and 3 dependent picklists Country__c, State__c and City__c (Country is controlling field for State, State is controlling field for City)

 



 Field Dependency setup for controlling field Country and dependent field State

 


Field Dependency setup for controlling field State and dependent field City

 


 
 

Create a Lightning Web Component 

(here I have given a component name as multiLevelDependentPickLists)

 multiLevelDependentPickLists.html

<template>
    <lightning-card title="Address" class="slds-var-p-around_small">
        <div class="slds-grid slds-var-m-around_large slds-wrap">
            <div class="slds-col slds-size_3-of-12 slds-var-p-around_small">
                <lightning-combobox name="Country__c" label="Country" placeholder="Select Country"
                    options={countryPickListOptions} onchange={handleCountryPickListValueChange}>
                </lightning-combobox>
            </div>
            <div class="slds-col slds-size_3-of-12 slds-var-p-around_small">
                <lightning-combobox name="State__c" label="State" placeholder="Select State"
                    options={statePickListOptions} onchange={handleStatePickListValueChange}>
                </lightning-combobox>
            </div>
            <div class="slds-col slds-size_3-of-12 slds-var-p-around_small">
                <lightning-combobox name="City__c" label="City" placeholder="Select City" options={cityPickListOptions}
                    onchange={handleCityPickListValueChange}>
                </lightning-combobox>
            </div>

            <template if:true={selectedPickListValuesMap}>
                <div class="slds-col slds-size_12-of-12  slds-var-p-around_small">
                    <div class="slds-grid slds-wrap">
                        <template for:each={selectedPickListValues} for:item="mapKey">
                            <div key={mapKey.key} class="slds-col slds-size_3-of-12 slds-var-p-right_small">
                                <strong> Selected {mapKey.key} :</strong> {mapKey.value}
                            </div>
                        </template>
                    </div>
                </div>
            </template>
           
        </div>
    </lightning-card>
</template>

 

multiLevelDependentPickLists.js

import { LightningElement, wire } from 'lwc';
import CUSTOMER_ADDRESS from '@salesforce/schema/Customer_Address__c'
import { getPicklistValuesByRecordType } from 'lightning/uiObjectInfoApi';
import { getObjectInfo } from 'lightning/uiObjectInfoApi';

export default class MultiLevelDependentPickLists extends LightningElement {

    customerAddress = CUSTOMER_ADDRESS;
    customerAddressObjectInfoData
       
    selectedPickListValuesMap = new Map()
    pickListOptionsMap = new Map();
    pickListControllerValuesMap = new Map();
    dependentFields

    countryPickListOptions
    statePickListOptions
    cityPickListOptions

    @wire(getObjectInfo, { objectApiName: '$customerAddress' })
    customerAddressObjectInfo({ data, error }) {
        if (data) {
            this.customerAddressObjectInfoData = data
            this.dependentFields = { ...data.dependentFields }
        }
        if (error) {
            console.log(error)
        }
    }

    @wire(getPicklistValuesByRecordType, { objectApiName: '$customerAddress', recordTypeId: '$customerAddressObjectInfoData.defaultRecordTypeId' })
    processPickLists({ data, error }) {
        if (data) {
            this.processPicklistData(data)
        }
        if (error) {
            console.log(error)
        }
    }

    processPicklistData(pickListValues) {
        let pickListFieldNames = Object.keys(pickListValues.picklistFieldValues);
        for (const key of pickListFieldNames) {
            this.pickListOptionsMap.set(key, pickListValues.picklistFieldValues[key].values)
            this.pickListControllerValuesMap.set(key, pickListValues.picklistFieldValues[key].controllerValues)
        }
        //get the main options to display at first, i.e in this case Country is the main field
        this.countryPickListOptions = this.pickListOptionsMap.get('Country__c')
    }

    handleCountryPickListValueChange(event) {
        //Step1: clear all the dependent fields of the current field Country (till the last depth)
        this.clearDependencies({ ...this.dependentFields }, 'Country__c')

        //Step2: update selected values
        this.updateSelectedPicklistData(event.target.name, event.target.value)

        //Step3: get the next dependet options to display, i.e in this case State is the next dependent object for Country
        this.statePickListOptions = this.pickListOptionsMap.get('State__c').filter(stateOption => {
            return stateOption.validFor[0] == this.pickListControllerValuesMap.get('State__c')[event.target.value]
        })
    }

    handleStatePickListValueChange(event) {
        //Step1: clear all the dependent fields of the current field State (till the last depth)
        this.clearDependencies({ ...this.dependentFields }, 'State__c')

        //Step2: update selected values
        this.updateSelectedPicklistData(event.target.name, event.target.value)

        //Step3: get the next dependet options to display, i.e in this case City is the next dependent object for State
        this.cityPickListOptions = this.pickListOptionsMap.get('City__c').filter(cityOption => {
            return cityOption.validFor[0] == this.pickListControllerValuesMap.get('City__c')[event.target.value]
        })
    }

    handleCityPickListValueChange(event) {
        //Step1: clear all the dependent fields of the current field City (till the last depth)
        this.clearDependencies({ ...this.dependentFields }, 'City__c')

        //Step2: update selected values
        this.updateSelectedPicklistData(event.target.name, event.target.value)

        //Step3: get the next dependet options to display, here we know there is no more dependets for City as of now, so we can skip this step3.
       
    }

    /*clears all the dependent fields of  given fieldName*/
    clearDependencies(dependentFields, fieldName) {
        dependentFields = this.sliceDependentChain(dependentFields, fieldName);
        if (!dependentFields) {
            return
        }
        let dependentFieldNames = Object.keys(dependentFields)
        if (dependentFieldNames.length == 0) {
            return
        } else {
            fieldName = dependentFieldNames[0];
            //clear the field with this field name
            let comboboxs = this.template.querySelectorAll('lightning-combobox')
            comboboxs.forEach(combobox => {
                if (combobox.name == fieldName) {
                    combobox.value = ''
                    combobox.options = {}
                    this.selectedPickListValuesMap.set(fieldName, '')
                }
            })

            //recursive call till the last dependent
            this.clearDependencies({ ...dependentFields }, fieldName)
        }
    }

    sliceDependentChain(dependentFields, fieldName) {
        let dependentFieldNames = Object.keys(dependentFields)
        let numberOfDependents = dependentFieldNames.length
        let nextDependentFieldNames = dependentFieldNames
        while (numberOfDependents > 0) {

            if (dependentFields[fieldName]) {
                return dependentFields[fieldName]
            } else {
                dependentFields = dependentFields[nextDependentFieldNames[0]]
                nextDependentFieldNames = Object.keys(dependentFields)
                if (nextDependentFieldNames.length == 0) {
                    break;
                }
                numberOfDependents = nextDependentFieldNames.length
            }
        }
        return null;
    }

    updateSelectedPicklistData(pickListName, selectedValue) {
        this.selectedPickListValuesMap= new Map(this.selectedPickListValuesMap) /* Optional Line: assigned with new map just to rerender and display the selected values
        This line is optional only required if you want to display the selected values*/
        this.selectedPickListValuesMap.set(pickListName, selectedValue) /*Required Line to update the selected value in the map */
    }

    get selectedPickListValues() {
        let selectedPickListValues=[]
        for (const key of this.selectedPickListValuesMap.keys()) {
            selectedPickListValues.push({key:key, value:this.selectedPickListValuesMap.get(key) })
        }
        return selectedPickListValues
    }
   
}


Demo:

 


Enhancing it to Four Level Dependent Picklist

Schama Update:

add one more picklist field Street__c to our custom object Customer_Address__c

 


 

Field Dependency setup for controlling field City and dependent field Street

 



 

 Update LWC

Add one lightning combo in HTML file to display Street picklists (highlighted the added piece of code in  yellow color)

multiLevelDependentPickLists.js

<template>
    <lightning-card title="Address" class="slds-var-p-around_small">
        <div class="slds-grid slds-var-m-around_large slds-wrap">
            <div class="slds-col slds-size_3-of-12 slds-var-p-around_small">
                <lightning-combobox name="Country__c" label="Country" placeholder="Select Country"
                    options={countryPickListOptions} onchange={handleCountryPickListValueChange}>
                </lightning-combobox>
            </div>
            <div class="slds-col slds-size_3-of-12 slds-var-p-around_small">
                <lightning-combobox name="State__c" label="State" placeholder="Select State"
                    options={statePickListOptions} onchange={handleStatePickListValueChange}>
                </lightning-combobox>
            </div>
            <div class="slds-col slds-size_3-of-12 slds-var-p-around_small">
                <lightning-combobox name="City__c" label="City" placeholder="Select City" options={cityPickListOptions}
                    onchange={handleCityPickListValueChange}>
                </lightning-combobox>
            </div>

            <!-- Added for Street enhancement to demo level 4 dependent picklist-->
            <div class="slds-col slds-size_3-of-12 slds-var-p-around_small">
                <lightning-combobox name="Street__c" label="Street" placeholder="Select Street" options={streetPickListOptions}
                    onchange={handleStreetPickListValueChange}>
                </lightning-combobox>
            </div>

            <template if:true={selectedPickListValuesMap}>
                <div class="slds-col slds-size_12-of-12  slds-var-p-around_small">
                    <div class="slds-grid slds-wrap">
                        <template for:each={selectedPickListValues} for:item="mapKey">
                            <div key={mapKey.key} class="slds-col slds-size_3-of-12 slds-var-p-right_small">
                                <strong> Selected {mapKey.key} :</strong> {mapKey.value}
                            </div>
                        </template>
                    </div>
                </div>
            </template>
           
        </div>
    </lightning-card>
</template>

 

Add below highlighted code (in yellow color) to give a life to street picklist,

import { LightningElement, wire } from 'lwc';
import CUSTOMER_ADDRESS from '@salesforce/schema/Customer_Address__c'
import { getPicklistValuesByRecordType } from 'lightning/uiObjectInfoApi';
import { getObjectInfo } from 'lightning/uiObjectInfoApi';

export default class FourLevelDependetPickLists extends LightningElement {

    customerAddress = CUSTOMER_ADDRESS;
    customerAddressObjectInfoData
       
    selectedPickListValuesMap = new Map()
    pickListOptionsMap = new Map();
    pickListControllerValuesMap = new Map();
    dependentFields

    countryPickListOptions
    statePickListOptions
    cityPickListOptions
    streetPickListOptions  /*Added for Street enhancement to demo level 4 dependent picklist */

    @wire(getObjectInfo, { objectApiName: '$customerAddress' })
    customerAddressObjectInfo({ data, error }) {
        if (data) {
            this.customerAddressObjectInfoData = data
            this.dependentFields = { ...data.dependentFields }
        }
        if (error) {
            console.log(error)
        }
    }

    @wire(getPicklistValuesByRecordType, { objectApiName: '$customerAddress', recordTypeId: '$customerAddressObjectInfoData.defaultRecordTypeId' })
    processPickLists({ data, error }) {
        if (data) {
            this.processPicklistData(data)
        }
        if (error) {
            console.log(error)
        }
    }

    processPicklistData(pickListValues) {
        let pickListFieldNames = Object.keys(pickListValues.picklistFieldValues);
        for (const key of pickListFieldNames) {
            this.pickListOptionsMap.set(key, pickListValues.picklistFieldValues[key].values)
            this.pickListControllerValuesMap.set(key, pickListValues.picklistFieldValues[key].controllerValues)
        }
        //get the main options to display at first, i.e in this case Country is the main field
        this.countryPickListOptions = this.pickListOptionsMap.get('Country__c')
    }

    handleCountryPickListValueChange(event) {
        //Step1: clear all the dependent fields of the current field Country (till the last depth)
        this.clearDependencies({ ...this.dependentFields }, 'Country__c')

        //Step2: update selected values
        this.updateSelectedPicklistData(event.target.name, event.target.value)

        //Step3: get the next dependet options to display, i.e in this case State is the next dependent object for Country
        this.statePickListOptions = this.pickListOptionsMap.get('State__c').filter(stateOption => {
            return stateOption.validFor[0] == this.pickListControllerValuesMap.get('State__c')[event.target.value]
        })
    }

    handleStatePickListValueChange(event) {
        //Step1: clear all the dependent fields of the current field State (till the last depth)
        this.clearDependencies({ ...this.dependentFields }, 'State__c')

        //Step2: update selected values
        this.updateSelectedPicklistData(event.target.name, event.target.value)

        //Step3: get the next dependet options to display, i.e in this case City is the next dependent object for State
        this.cityPickListOptions = this.pickListOptionsMap.get('City__c').filter(cityOption => {
            return cityOption.validFor[0] == this.pickListControllerValuesMap.get('City__c')[event.target.value]
        })
    }

    handleCityPickListValueChange(event) {
        //Step1: clear all the dependent fields of the current field City (till the last depth)
        this.clearDependencies({ ...this.dependentFields }, 'City__c')

        //Step2: update selected values
        this.updateSelectedPicklistData(event.target.name, event.target.value)

        //Step3: get the next dependet options to display, i.e in this case Street is the next dependent object for City
        /*Added for Street enhancement to demo level 4 dependent picklist */
        this.streetPickListOptions = this.pickListOptionsMap.get('Street__c').filter(streetOption => {
            return streetOption.validFor[0] == this.pickListControllerValuesMap.get('Street__c')[event.target.value]
        })
    }

    /*Added for Street enhancement to demo level 4 dependent picklist */
    handleStreetPickListValueChange(event) {
        //Step1: clear all the dependent fields of the current field City (till the last depth)
        this.clearDependencies({ ...this.dependentFields }, 'Street__c')

        //Step2: update selected values
        this.updateSelectedPicklistData(event.target.name, event.target.value)

        //Step3: get the next dependet options to display, here we know there is no more dependets for Street.
       
    }

    /*clears all the dependent fields of  given fieldName*/
    clearDependencies(dependentFields, fieldName) {
        dependentFields = this.sliceDependentChain(dependentFields, fieldName);
        if (!dependentFields) {
            return
        }
        let dependentFieldNames = Object.keys(dependentFields)
        if (dependentFieldNames.length == 0) {
            return
        } else {
            fieldName = dependentFieldNames[0];
            //clear the field with this field name
            let comboboxs = this.template.querySelectorAll('lightning-combobox')
            comboboxs.forEach(combobox => {
                if (combobox.name == fieldName) {
                    combobox.value = ''
                    combobox.options = {}
                    this.selectedPickListValuesMap.set(fieldName, '')
                }
            })

            //recursive call till the last dependent
            this.clearDependencies({ ...dependentFields }, fieldName)
        }
    }

    sliceDependentChain(dependentFields, fieldName) {
        let dependentFieldNames = Object.keys(dependentFields)
        let numberOfDependents = dependentFieldNames.length
        let nextDependentFieldNames = dependentFieldNames
        while (numberOfDependents > 0) {

            if (dependentFields[fieldName]) {
                return dependentFields[fieldName]
            } else {
                dependentFields = dependentFields[nextDependentFieldNames[0]]
                nextDependentFieldNames = Object.keys(dependentFields)
                if (nextDependentFieldNames.length == 0) {
                    break;
                }
                numberOfDependents = nextDependentFieldNames.length
            }
        }
        return null;
    }

    updateSelectedPicklistData(pickListName, selectedValue) {
        this.selectedPickListValuesMap= new Map(this.selectedPickListValuesMap) /* Optional Line: assigned with new map just to rerender and display the selected values
        This line is optional only required if you want to display the selected values*/
        this.selectedPickListValuesMap.set(pickListName, selectedValue) /*Required Line to update the selected value in the map */
    }

    get selectedPickListValues() {
        let selectedPickListValues=[]
        for (const key of this.selectedPickListValuesMap.keys()) {
            selectedPickListValues.push({key:key, value:this.selectedPickListValuesMap.get(key) })
        }
        return selectedPickListValues
    }
   
}

Demo:


 

Like this you can add/enhance this with any number of dependent picklists.







Comments

  1. Hi Vijaya, very nice blog. We've this 4 level dependent picklist (all custom fields) on account object.
    What change you foresee? In your implementation all fields are picklist

    ReplyDelete
    Replies
    1. Hi Gaurang, In my implementation all four fields are custom dependent picklists. We can apply above implementation for any dependent picklists by replacing with the corresponding picklist api names in places of Country__c, State__c, City__c, street__c (also replace the custom object name).
      Please let me know if you need more details on this.

      Delete
  2. Thanks Vijaya.

    I've created same in account object. Its working fine but how can I save the picklist values in the database after making selection?

    Here is what I've done
    1 - Create 4 custom picklist fields
    2 - Create LWC (same as above). I need expose as lightning page and flow component as part of account create screen flow

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
  4. Hi Gaurang,

    As you know all the selected values are stored selectedPickListValuesMap. Please note that we can update the values in many ways as per the requirement.
    one of the way is to display a button "Save Address" in LWC and update the record data onclick event as shown in the below code.

    Add "Save Address" button in html with event onclick={handleSaveAddress}
    -------------------------



    Add below code in js
    -----------------------
    import {updateRecord} from 'lightning/uiRecordApi';
    import { api } from 'lwc';


    @api recordId; //it gets recordId if you are page is Record Page, otherwise get the id for which you want to update the address

    handleSaveAddress() {
    const fields = {};
    fields['Id']=this.recordId; // pass here the record id that you have to update.
    fields['Country__c']=this.selectedPickListValuesMap.get('Country__c')?this.selectedPickListValuesMap.get('Country__c'):null;
    fields['State__c']=this.selectedPickListValuesMap.get('State__c')?this.selectedPickListValuesMap.get('State__c'):null;
    fields['City__c']=this.selectedPickListValuesMap.get('City__c')?this.selectedPickListValuesMap.get('City__c'):null;
    fields['Street__c']=this.selectedPickListValuesMap.get('Street__c')?this.selectedPickListValuesMap.get('Street__c'):null;
    const recordInput = {fields};

    updateRecord(recordInput).then((result) =>{
    this.dispatchEvent(new ShowToastEvent ({title:'Address Updaate', message:'Address Updated successfully record : '+result.id, variant:'success', mode: 'pester'}));
    }).catch(error=> {
    this.dispatchEvent(new ShowToastEvent ({title:'Address Update', message:'Error on Address Update : '+error.body.message, variant:'error', mode: 'pester'}));
    });
    }

    ReplyDelete
    Replies
    1. In case if you want to create the record then

      Add "Create Address" button in html with event onclick={handleCreateRecord}
      -------------------------



      Add below code in js
      -----------------------
      import {createRecord} from 'lightning/uiRecordApi'

      handleCreateRecord() {
      const fields = {};

      fields['Country__c']=this.selectedPickListValuesMap.get('Country__c')?this.selectedPickListValuesMap.get('Country__c'):null;
      fields['State__c']=this.selectedPickListValuesMap.get('State__c')?this.selectedPickListValuesMap.get('State__c'):null;
      fields['City__c']=this.selectedPickListValuesMap.get('City__c')?this.selectedPickListValuesMap.get('City__c'):null;
      fields['Street__c']=this.selectedPickListValuesMap.get('Street__c')?this.selectedPickListValuesMap.get('Street__c'):null;
      const recordInput = {apiName:'Customer_Address__c', fields};

      createRecord(recordInput).then((result) =>{
      this.dispatchEvent(new ShowToastEvent ({title:'Address Create', message:'Address created successfully record : '+result.id, variant:'success', mode: 'pester'}));
      }).catch(error=> {
      this.dispatchEvent(new ShowToastEvent ({title:'Address Create', message:'Error on Address Create : '+error.body.message, variant:'error', mode: 'pester'}));
      });
      }

      Delete
    2. Thanks Vijaya. I'll try this out

      Delete

Post a Comment