본문 바로가기

개발자정보

Custom Dependent Picklist In LWC

반응형

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/atlas.en-us.apexcode.meta/apexcode/apex_dynamic_describe_objects_understanding.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 :

https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.reference_wire_adapters_picklist_values

https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.reference_wire_adapters_object_info

반응형