I am exploring on lightning web components framework and it’s underground standard JS concepts. As a first POC in LWC , I will be sharing with you, reusable dependent picklist components which can be used in custom LWC or Aura record forms.
Developed the component in three approaches by exploring the ways the picklist entries can be pulled from server side to web component UI.
- Dependent picklist using Apex
- Dependent Picklist using UI ObjectInfo API
- Dependent Picklist using Record Edit Form
Dependent Picklist Using Apex
First, I will develop a component which fetches the picklist entries through an apex method.
Step 1 – Create An Apex Method To Fetch The Picklist Values.
Three parameters were shared to apex method to make it generic , they are object name, controlling and dependent picklist field api names. Wrapper class also be included with the class to share both picklist values in a useful format. Please go through PicklistValue class at the bottom of the class. Also, I have included the provisions for error handling but the wrapper should be modified accordingly (you can make the changes with respect to your requirement).
DependentPicklistController
public with sharing class DependentPicklistController { | |
@AuraEnabled(cacheable = true) | |
public static List<PicklistValue> getPicklistValues(String objApiName, String controlField, String dependentField){ | |
List<PicklistValue> pickListValues = new List<PicklistValue>(); | |
if(String.isBlank(objApiName) || String.isBlank(controlField) || String.isBlank(dependentField)) { | |
//return pickListValues; | |
objApiName = 'Account'; | |
controlField ='Country__c'; | |
dependentField= 'State__c'; | |
//enable the return statement and remove the above static assignment with some valid error value to update the UI | |
//return; | |
} | |
//Identify the sobject type from the object name using global schema describe function | |
Schema.SObjectType targetType = Schema.getGlobalDescribe().get(objApiName); | |
//Create an empty object based up on the above the sobject type to get all the field names | |
Schema.sObjectType objType = targetType.newSObject().getSobjectType(); | |
//fetch the all fields defined in the sobject | |
Map<String, Schema.SObjectField> objFieldMap = objType.getDescribe().fields.getMap(); | |
//Get the controlling and dependent picklist values from the objFieldMap | |
List<Schema.PicklistEntry> controlledPLEntries = objFieldMap.get(controlField).getDescribe().getPicklistValues(); | |
List<Schema.PicklistEntry> dependentPLEntries = objFieldMap.get(dependentField).getDescribe().getPicklistValues(); | |
// Wrap the picklist values using custom wrapper class – PicklistValue | |
for (Schema.PicklistEntry entry : controlledPLEntries) { | |
PicklistValue picklistValue = new PicklistValue(entry.isActive(), entry.isDefaultValue(), entry.getLabel(), entry.getValue()); | |
pickListValues.add(picklistValue); | |
} | |
//ValidFor is an indicator value for the controlling field which is base64 encrypted | |
//base64 value should be convered to 6bit grouped binary and 1 indicate the controlling field in a certain order | |
//Also,validFor value only be shown when it is serialized so it should be serialized then deserialized using PicklistValue wrapper class | |
for(PicklistValue plVal : deserializePLEntries(dependentPLEntries)) { | |
String decodedInBits = base64ToBits(plVal.validFor); | |
for(Integer i = 0; i< decodedInBits.length(); i++) { | |
// For each bit, in order: if it's a 1, add this label to the dependent list for the corresponding controlling value | |
String bit = decodedInBits.mid(i, 1); | |
if (bit == '1') { | |
PicklistValue dependentPLValue = new PicklistValue(plVal.active, plVal.defaultValue, plVal.label, plVal.value); | |
//Dependent picklist value is mapped to its parent controlling value through 'dependents' attribute in the PicklistValue wrapper class | |
if(pickListValues.get(i).dependents == null ) { | |
pickListValues.get(i).dependents = new List<PicklistValue>{dependentPLValue}; | |
}else{ | |
pickListValues.get(i).dependents.add(dependentPLValue); | |
} | |
} | |
} | |
} | |
return pickListValues; | |
} | |
private static List<PicklistValue> deserializePLEntries(List<Schema.PicklistEntry> plEntries) { | |
return (List<PicklistValue>) | |
JSON.deserialize(JSON.serialize(plEntries), List<PicklistValue>.class); | |
} | |
//Available base64 charecters | |
private static final String BASE_64_CHARS = '' +'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +'abcdefghijklmnopqrstuvwxyz' +'0123456789+/'; | |
// Convert decimal to binary representation (alas, Apex has no native method | |
// eg. 4 => '100', 19 => '10011', etc. | |
// Method: Divide by 2 repeatedly until 0. At each step note the remainder (0 or 1). | |
// These, in reverse order, are the binary. | |
private static String decimalToBinary(Integer val) { | |
String bits = ''; | |
while (val > 0) { | |
Integer remainder = Math.mod(val, 2); | |
val = Integer.valueOf(Math.floor(val / 2)); | |
bits = String.valueOf(remainder) + bits; | |
} | |
return bits; | |
} | |
// Convert a base64 token into a binary/bits representation | |
// e.g. 'gAAA' => '100000000000000000000' | |
private static String base64ToBits(String validFor) { | |
if (String.isEmpty(validFor)) { | |
return ''; | |
} | |
String validForBits = ''; | |
for (Integer i = 0; i < validFor.length(); i++) { | |
String thisChar = validFor.mid(i, 1); | |
Integer val = BASE_64_CHARS.indexOf(thisChar); | |
String bits = decimalToBinary(val).leftPad(6, '0'); | |
validForBits += bits; | |
} | |
return validForBits; | |
} | |
//Wrapper class | |
public class PicklistValue { | |
@AuraEnabled | |
public Boolean active { get; set; } | |
@AuraEnabled | |
public Boolean defaultValue { get; set; } | |
@AuraEnabled | |
public String label { get; set; } | |
@AuraEnabled | |
public String validFor { get; set; } | |
@AuraEnabled | |
public String value { get; set; } | |
@AuraEnabled | |
public List<PickListValue> dependents {get; set;} | |
public PicklistValue(){} | |
public PicklistValue(Boolean active, Boolean defaultValue, String label, String validFor, String value) { | |
this.active = active; | |
this.defaultValue = defaultValue; | |
this.label = label; | |
this.validFor = validFor; | |
this.value = value; | |
} | |
public PicklistValue(Boolean active, Boolean defaultValue, String label, String value) { | |
this.active = active; | |
this.defaultValue = defaultValue; | |
this.label = label; | |
this.validFor = validFor; | |
this.value = value; | |
} | |
public PicklistValue(String label, String value) { | |
this.label = label; | |
this.value = value; | |
} | |
} | |
} |
Step 2 – Create A Lightning Web Component
The LWC component will consume the apex controller method for picklist values by using wire decorator. Required apex method parameters are created as public attribute (@api) in the component side for user accessibility.
<!– template with two combobox or dropdown base component | |
to show controlling and dependent picklist fields with its attributes–> | |
<template> | |
<lightning-layout horizontal-align="space"> | |
<lightning-layout-item padding="around-small"> | |
<lightning-combobox | |
name="progress" | |
label="Country" | |
value={parentValue} | |
options={controlOptions} | |
onchange={handleControlChange} ></lightning-combobox> | |
</lightning-layout-item> | |
<lightning-layout-item padding="around-small"> | |
<lightning-combobox | |
class="dependent" | |
name="progress" | |
label="State" | |
value={dependentValue} | |
options={dependentOptions} | |
onchange={handleDependentChange} disabled={isDisabled}></lightning-combobox> | |
</lightning-layout-item> | |
</lightning-layout> | |
</template> |
import { api, LightningElement, track, wire } from 'lwc'; | |
import getPicklistValues from '@salesforce/apex/DependentPicklistController.getPicklistValues'; | |
const defaultOption = {label : '—None—',value: ''}; | |
export default class DependentPicklist extends LightningElement { | |
//public attribute for object selection | |
@api | |
objectApiName; | |
//Public attribute for controlling picklist field | |
@api | |
controlFieldName; | |
//Public attribute for dependent picklist field | |
@api | |
dependentFieldName; | |
//Public attribute for selected dependent picklist value | |
@api | |
dependentValue = ''; | |
//Public attribute for selected controlling picklist value | |
@api | |
parentValue =''; | |
//to stroe controlling picklist value options | |
controlOptions =[]; | |
//to stroe dependent picklist value options based on the controlling picklist value | |
dependentOptions = []; | |
data = []; | |
//to enable/disable the dependent picklist field in the UI | |
isDisabled = true; | |
/* | |
Wiring to getPicklistValues method defined in the apex class – DependentPicklistController | |
which returns picklist values based on the paramteres object name, controlling Field and dependent field name */ | |
@wire(getPicklistValues,{objApiName : '$objectApiName', controlField : '$controlFieldName', dependentField:'$dependentFieldName'}) | |
picklistData({data,error}) { | |
if(data){ | |
this.data = data; | |
this.updateOptions(); | |
//console.log(JSON.stringify(data)); | |
} | |
} | |
//Sets the controlling field options to show | |
updateOptions() { | |
let defaultValue = ''; | |
this.controlOptions = this.data.map((option) => { | |
if(option.defaultValue) { | |
defaultValue = option.value; | |
} | |
return { label: option.label, value: option.value } | |
}); | |
//show the default selected option as the first item in the dropdown otherwise –None– | |
if(defaultValue){ | |
this.controlOptions.push(defaultOption); | |
this.parentValue = defaultValue; | |
this.updateDependentOptions(); | |
this.isDisabled = false; | |
}else{ | |
//default –None– Option is already set in the defaultOption constant | |
this.controlOptions.unshift(defaultOption); | |
} | |
} | |
//set the dependent options in the dropdown field | |
updateDependentOptions() { | |
let defaultValue = ''; | |
//fetch the selected option in the controlling field with it's dependent picklist values | |
const selOptionDetail = this.data.filter(option => option.value === this.parentValue); | |
//check the dependent option list empty or not and sets in to the dependent field, | |
//filter function returns an array so fetching the value in the 0th index | |
if(selOptionDetail.length > 0 && selOptionDetail[0].dependents) { | |
this.dependentOptions = selOptionDetail[0].dependents.map((option) => { | |
if(option.defaultValue) { | |
defaultValue = option.value; | |
} | |
return { label: option.label, value: option.value } | |
}); | |
//sets the default selected option or –None– | |
if(defaultValue){ | |
this.dependentOptions.push(defaultOption); | |
this.dependentValue = defaultValue; | |
}else{ | |
this.dependentValue = ''; | |
this.dependentOptions.unshift(defaultOption); | |
} | |
//console.log(this.dependentOptions); | |
} | |
} | |
/*Handler for onchange event which initiate a custom event and set the value in the corresponding output attribute */ | |
handleControlChange(event) { | |
this.parentValue = event.detail.value; | |
//enable the dependent picklist field – only needed for initial selection | |
this.isDisabled = false; | |
//update the dependent field option with newly selected control field value | |
this.updateDependentOptions(); | |
this.dispatchEvent(new CustomEvent('controlchange',{detail : {value : this.parentValue}})); | |
} | |
/*Handler for onchange event which initiate a custom event and set the value in the corresponding output attribute */ | |
handleDependentChange(event) { | |
this.dependentValue = event.detail.value; | |
this.dispatchEvent(new CustomEvent('dependentchange',{detail : {value : this.dependentValue}})); | |
} | |
} |
<?xml version="1.0" encoding="UTF-8"?> | |
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata"> | |
<apiVersion>49.0</apiVersion> | |
<isExposed>true</isExposed> | |
<targets> | |
<target>lightning__AppPage</target> | |
<target>lightning__RecordPage</target> | |
<target>lightning__HomePage</target> | |
</targets> | |
</LightningComponentBundle> |
‘dependentValue’ and ‘parentValue’ are the output parameters for providing the dependent and controlling picklist field selected values. Also, the selections will be notified with separate custom events for both the fields, named ‘controlchange’ and ‘dependentchange’.
Please refer below documents for more details:
https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_picklist.htm
https://developer.salesforce.com/docs/component-library/documentation/en/lwc
Hope you guys are able to understand the implementation and the code is self explanatory. Also, I have included the necessary comments for most of the lines. Use the comment section for posting your valuable feedback and questions.
Read my next blog for same component implementation using Record edit form / UIObjectInfo api.
In this blog, I would like to share with you the same reusable custom dependent pick-list component which was explained in my previous part1 blog. Current component is fetching pick-list values from org using uiObjectInfoApi which was released in winter 21 instead of custom apex class.
Object-info API is flexible enough to provide complete single or multiple information as well as pick-list values based on the record type by consuming dynamic values through wire method. In our current scenario, we have consumed getObjectInfo, getPicklistValues api’s to get the necessary data to bind pick-list values to the component.
The below component code explains everything as it is simple and consumed only the above custom apis to bind the data. ‘dependentValue’ and ‘parentValue’ are the output parameters for providing the dependent and controlling picklist field selected values. Also, the selections will be notified with separate custom events for both the fields, named ‘controlchange’ and ‘dependentchange’.
<template> | |
<lightning-layout horizontal-align="space"> | |
<lightning-layout-item padding="around-small"> | |
<lightning-combobox | |
name="progress" | |
label="Status" | |
value={parentValue} | |
options={controlOptions} | |
onchange={handleControlChange} ></lightning-combobox> | |
</lightning-layout-item> | |
<lightning-layout-item padding="around-small"> | |
<lightning-combobox | |
class="dependent" | |
name="progress" | |
label="Status" | |
value={dependentValue} | |
options={dependentOptions} | |
onchange={handleDependentChange} disabled={isDisabled}></lightning-combobox> | |
</lightning-layout-item> | |
</lightning-layout> | |
</template> |
import { LightningElement,api,wire, track } from 'lwc'; | |
import { getObjectInfo,getPicklistValues,getPicklistValuesByRecordType } from 'lightning/uiObjectInfoApi'; | |
export default class DependentPicklist extends LightningElement { | |
//public attribute for object selection | |
@api | |
objectApiName; | |
//Public attribute for controlling picklist field | |
@api | |
controlFieldName; | |
//Public attribute for dependent picklist field | |
@api | |
dependentFieldName; | |
//Public attribute for recordtype id | |
@api | |
recordTypeId=''; | |
//Public attribute for selected dependent picklist value | |
@api | |
dependentValue = ''; | |
//Public attribute for selected controlling picklist value | |
@api | |
parentValue =''; | |
//to stroe controlling picklist value options | |
controlOptions =[]; | |
//to stroe dependent picklist value options based on the controlling picklist value | |
dependentOptions = []; | |
data = {}; | |
//to enable/disable the dependent picklist field in the UI – disabled it initially | |
isDisabled = true; | |
@track | |
parentField = ''; | |
dependentField = ''; | |
//Wire method which fetches the object detatils with all the dependent picklist fields, record types, field details etc | |
//based on the objectAPIName parameter | |
@wire(getObjectInfo, { objectApiName: '$objectApiName' }) | |
objectInfo({error,data}) { | |
if (data) { | |
//set the default recordtype id if there is no given value | |
if(!this.recordTypeId) { | |
this.recordTypeId = data.defaultRecordTypeId; | |
} | |
//Check if any dependent picklist fields available for the consdiered object | |
if(!(Object.keys(data.dependentFields).length === 0 && data.dependentFields.constructor === Object)){ | |
//check the given controlling field name is valid for this object or not, otherwise throw an appropriate error | |
if(data.dependentFields.hasOwnProperty(this.controlFieldName)){ | |
//console.log(data.dependentFields.hasOwnProperty(this.controlFieldName)); | |
//check the given dependent field name is valid for this object or not, otherwise throw an appropriate error | |
if(!data.dependentFields[this.controlFieldName].hasOwnProperty(this.dependentFieldName)) { | |
console.log('error') | |
return; | |
} | |
//if the fields are valid, then pass the fieldAPIName in appropriate for the getPicklistValue object info wire method | |
//basically, controlling the wire parameters – no possibilty for imperative accessibility to UIObjectInfo API | |
this.parentField = this.generateFieldAPIName(this.controlFieldName); | |
this.dependentField = this.generateFieldAPIName(this.dependentFieldName); | |
} | |
else { | |
console.log('error') | |
return; | |
} | |
} | |
} | |
} | |
//fethch the dependent picklist values based on the recordtype id and fieldAPI details | |
@wire(getPicklistValues, { recordTypeId: '$recordTypeId', fieldApiName: '$dependentField' }) | |
fetchDependentOptions( {error , data }) { | |
if(data) { | |
const controlKeys = {}; | |
//Handling the validFor value which denotes the controlling fields. | |
//check the json format, controllerValues in field:index format and converts it into index:field format | |
Object.keys(data.controllerValues).forEach((key) => { | |
Object.defineProperty(controlKeys, data.controllerValues[key], { | |
value: key, | |
writable: false | |
}); | |
//create a dependent data skelton | |
Object.defineProperty(this.data, key, { | |
value : {values : [], default: false , defaultValue : ''}, | |
writable : true | |
}); | |
}); | |
//dependent data should be formatted as controllingField value as the key and related dependent options in an array | |
//no need to iterate again when the controlling field value changes so this format would be helpful | |
data.values.forEach((val) => { | |
let option = {label : val.label, value : val.value}; | |
let isDefault = val.value === data.defaultValue.value ? true : false ; | |
val.validFor.forEach((key) => { | |
this.data[controlKeys[key]].values.push(option); | |
if(isDefault) { | |
this.data[controlKeys[key]].default = isDefault; | |
this.data[controlKeys[key]].defaultValue = val.value; | |
} | |
}); | |
}); | |
//set the dependent options once the dependent data is ready | |
this.setDependentOptions(); | |
//console.log(controlKeys); | |
//console.log(this.data); | |
}else{ | |
//handle the errors | |
console.log(JSON.stringify(error)); | |
} | |
} | |
//fethch the controlling picklist values based on the recordtype id and fieldAPI details | |
@wire(getPicklistValues,{ recordTypeId: '$recordTypeId', fieldApiName: '$parentField'}) | |
fetchControlOption( {error , data }) { | |
if(data) { | |
//sets the options to contriolling field | |
this.controlOptions = data.values.map((option) => { | |
return {label : option.label, value : option.value}; | |
}); | |
//default value for the controlling field | |
this.parentValue = data.defaultValue.hasOwnProperty('value') ? data.defaultValue.value : ''; | |
//initating to set the dependent option in the field | |
this.setDependentOptions(); | |
}else{ | |
//handle the errors in an appropriate way | |
console.log(JSON.stringify(error)); | |
} | |
} | |
setDependentOptions(){ | |
//only sets the dependent picklist options only if there any valid selection, valid depdendent data for the selected value | |
if(this.parentValue && this.data && this.data.hasOwnProperty(this.parentValue)) { | |
this.isDisabled = false; | |
//fetching the options from the data array using the controlling value as index | |
let selectOptions = this.data[this.parentValue]; | |
//sets the dependent options to the field | |
this.dependentOptions = selectOptions.values; | |
//set the default value | |
if(selectOptions.default){ | |
this.dependentValue = selectOptions.defaultValue; | |
} | |
} | |
} | |
/*Handler for onchange event which initiate a custom event and set the value in the corresponding output attribute */ | |
handleControlChange(event) { | |
this.parentValue = event.detail.value; | |
this.setDependentOptions(); | |
this.dispatchEvent(new CustomEvent('controlchange',{detail : {value : this.parentValue}})); | |
} | |
/*Handler for onchange event which initiate a custom event and set the value in the corresponding output attribute */ | |
handleDependentChange(event) { | |
this.dependentValue = event.detail.value; | |
this.dispatchEvent(new CustomEvent('dependentchange',{detail : {value : this.dependentValue}})); | |
} | |
//define the fieldAPIName for the getPickListValue UI objectinfo api | |
generateFieldAPIName(fieldName) { | |
return {"objectApiName": this.objectApiName,"fieldApiName": fieldName }; | |
} | |
} |
<?xml version="1.0" encoding="UTF-8"?> | |
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">; | |
<apiVersion>49.0</apiVersion> | |
<isExposed>true</isExposed> | |
<targets> | |
<target>lightning__AppPage</target> | |
<target>lightning__RecordPage</target> | |
<target>lightning__HomePage</target> | |
<targets> | |
</LightningComponentBundle> |
- getObjectInfo api fetches the record type and dependent field information based on the public attributes through a wire method objectInfo
- if the recordtypeid is not given, then assigns it from the objects default recordtypeId
- Next, verify the dependent pick-list fields passed through controlFieldName, dependentFieldName are valid for the given object.
- After successful validation, parentField and dependentField populates necessary field information to pull the pick-list values from salesforce org using getPicklistValues api.
- parentField and dependentField attributes acts as a controller for other two wire methods as we don’t have any option to call the ui apis in an imperative way. If the picklist fields are not valid, then both the attributes have only an empty string which restricts from pulling the data from server.
- fetchControlOption method sets the options for the controlling combobox field. Also, considering the default value selection as well.
- fetchDependentOptions method sets the options, not in a straight forward way as it contains validFor and ControllingFields attributes to indicate its controlling field. Please go through the JSON then you will be understand the logic.
- setDependentOptions is private method. which invokes multiple places to fill the dependent options only if the value is selected in the controlling field and valid data available for dependent field (as we don’t have any control on order of the wire method execution).
Please go through the below links for more details :
'개발자정보' 카테고리의 다른 글
Salesforce ADM-211 Exam Actual Questions (0) | 2022.04.16 |
---|---|
Salesforce ADM-201 Exam Actual Questions (0) | 2022.04.16 |
Configure Single Sign On Across Multiple Salesforce Orgs (0) | 2022.04.16 |
로컬 컴퓨터에서 LWC 개발 (0) | 2022.04.16 |
Lightning Context 현재 사용자의 세션 ID를 가져오는 방법 (0) | 2022.04.16 |