Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Handle cases where one dependent option can be used for multiple controlling options
/*
* Apex doesn't expose dependent picklist info directly, but it's possible to expose.
* Approach:
* * Schema.PicklistEntry doesn't expose validFor tokens, but they are there, and can be accessed by serializing to JSON
* (and then for convenience, deserializing back into an Apex POJO)
* * validFor tokens are converted from base64 representations (e.g. gAAA) to binary (100000000000000000000)
* each character corresponds to 6 bits, determined by normal base64 encoding rules.
* * The binary bits correspond to controlling values that are active - e.g. in the example above, this dependent option
* is available for the first controlling field only.
*
* by Benj Kamm, 2017
* CC BY-SA 3.0 (http://creativecommons.org/licenses/by-sa/3.0/us/)
*/
public class HL_FieldDescribeUtil {
public static Map<String, List<String>> getDependentOptionsImpl(Schema.SObjectField theField, Schema.SObjectField ctrlField) {
// validFor property cannot be accessed via a method or a property,
// so we need to serialize the PicklistEntry object and then deserialize into a wrapper.
List<Schema.PicklistEntry> contrEntries = ctrlField.getDescribe().getPicklistValues();
List<PicklistEntryWrapper> depEntries =
HL_FieldDescribeUtil.wrapPicklistEntries(theField.getDescribe().getPicklistValues());
// Set up the return container - Map<ControllingValue, List<DependentValues>>
Map<String, List<String>> objResults = new Map<String, List<String>>();
List<String> controllingValues = new List<String>();
for (Schema.PicklistEntry ple : contrEntries) {
String label = ple.getLabel();
objResults.put(label, new List<String>());
controllingValues.add(label);
}
for (PicklistEntryWrapper plew : depEntries) {
String label = plew.label;
String validForBits = base64ToBits(plew.validFor);
for (Integer i = 0; i < validForBits.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 = validForBits.mid(i, 1);
if (bit == '1') {
objResults.get(controllingValues.get(i)).add(label);
}
}
}
return objResults;
}
// 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.
public 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'
public 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 = base64Chars.indexOf(thisChar);
String bits = decimalToBinary(val).leftPad(6, '0');
validForBits += bits;
}
return validForBits;
}
private static final String base64Chars = '' +
'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
'abcdefghijklmnopqrstuvwxyz' +
'0123456789+/';
private static List<PicklistEntryWrapper> wrapPicklistEntries(List<Schema.PicklistEntry> PLEs) {
return (List<PicklistEntryWrapper>)
JSON.deserialize(JSON.serialize(PLEs), List<PicklistEntryWrapper>.class);
}
public class PicklistEntryWrapper {
public String active {get; set;}
public String defaultValue {get; set;}
public String label {get; set;}
public String value {get; set;}
public String validFor {get; set;}
}
}
/**
* getDependentPicklistOptions
* by Benj Kamm, 2012
* (inspired by http://iwritecrappycode.wordpress.com/2012/02/23/dependent-picklists-in-salesforce-without-metadata-api-or-visualforce/)
* CC BY-SA 3.0 (http://creativecommons.org/licenses/by-sa/3.0/us/)
*
* Build an Object in which keys are valid options for the controlling field
* and values are lists of valid options for the dependent field.
*
* Method: dependent PickListEntry.validFor provides a base64 encoded
* string. After decoding, each of the bits (reading L to R)
* corresponds to the picklist values for the controlling field.
*/
function getDependentOptions (objName, ctrlFieldName, depFieldName) {
// Isolate the Describe info for the relevant fields
var objDesc = sforce.connection.describeSObject(objName);
var ctrlFieldDesc, depFieldDesc;
var found = 0;
for (var i=0; i<objDesc.fields.length; i++) {
var f = objDesc.fields[i];
if (f.name == ctrlFieldName) {
ctrlFieldDesc = f;
found++;
} else if (f.name == depFieldName) {
depFieldDesc = f;
found++;
}
if (found==2) break;
}
// Set up return object
var dependentOptions = {};
var ctrlValues = ctrlFieldDesc.picklistValues;
for (var i=0; i<ctrlValues.length; i++) {
dependentOptions[ctrlValues[i].label] = [];
}
var base64 = new sforce.Base64Binary("");
function testBit (validFor, pos) {
var byteToCheck = Math.floor(pos/8);
var bit = 7 - (pos % 8);
return ((Math.pow(2, bit) & validFor.charCodeAt(byteToCheck)) >> bit) == 1;
}
// For each dependent value, check whether it is valid for each controlling value
var depValues = depFieldDesc.picklistValues;
for (var i=0; i<depValues.length; i++) {
var thisOption = depValues[i];
var validForDec = base64.decode(thisOption.validFor);
for (var ctrlValue=0; ctrlValue<ctrlValues.length; ctrlValue++) {
if (testBit(validForDec, ctrlValue)) {
dependentOptions[ctrlValues[ctrlValue].label].push(thisOption.label);
}
}
}
return dependentOptions;
}
var OBJ_NAME = 'Custom_Object__c';
var CTRL_FIELD_NAME = "Controlling_Field__c";
var DEP_FIELD_NAME = "Dependent_Field__c";
var options = getDependentOptions(OBJ_NAME, CTRL_FIELD_NAME, DEP_FIELD_NAME);
console.debug(options);
@mshimdeloitte

This comment has been minimized.

Copy link

@mshimdeloitte mshimdeloitte commented Apr 19, 2016

This helped me a lot! Thank you. I was wondering why you would store everything in an object rather than an array?

@quoctc

This comment has been minimized.

Copy link

@quoctc quoctc commented Apr 7, 2017

It's not working for mobile SDK. the pos look wrong. how can i know the exactly pos of control field?

@bmodeprogrammer

This comment has been minimized.

Copy link

@bmodeprogrammer bmodeprogrammer commented Apr 18, 2017

@boxfoot can you explain me why you do the testBit? thank you. Thats helped me a lot.

@boxfoot

This comment has been minimized.

Copy link
Owner Author

@boxfoot boxfoot commented May 18, 2017

@quoctc @bmodeprogrammer I just added a new approach that is easier to understand -- it uses a different approach instead of bitwise operators and is much easier to understand.

The new version is in Apex but should be easy enough for you to adapt a javascript version.

@crowz4k

This comment has been minimized.

Copy link

@crowz4k crowz4k commented May 21, 2017

what is the validForBits.mid(i, 1);, what those that mean some string.mid what function is that in java c# ?

@negra1m

This comment has been minimized.

Copy link

@negra1m negra1m commented Nov 13, 2017

Why couldn't I retrieve this data var "objDesc = sforce.connection.describeSObject(objName)" Sforce is not defined?

@boxfoot

This comment has been minimized.

Copy link
Owner Author

@boxfoot boxfoot commented Dec 19, 2017

@crowz4k you're taking each bit from validForBits one at a time. the string.mid(position, number) function takes number bits from position position

@vinegrao95 you need to add the salesforce javascript sdk to your page - it will provide sforce as a global variable.

@daneshd

This comment has been minimized.

Copy link

@daneshd daneshd commented Feb 16, 2018

In this stackexchange answer, you mentioned it would be rearranged as bytes: 10000000 00000000 00000000 but in your comments in this gist, it's e.g. 'gAAA' => '100000000000000000000'.

Splitting that in 8ths like the SE answer, that's 10000000 00000000 00000 - is that missing 3 zeros at the end there? Trying to make sure I understand the logic correctly!

@ViktorMants

This comment has been minimized.

Copy link

@ViktorMants ViktorMants commented Nov 23, 2018

This is a great solution and it helped me a lot. Thank you

@fahey252

This comment has been minimized.

Copy link

@fahey252 fahey252 commented Oct 6, 2020

For JavaScript, this may be helpful:

Helper functions:

  /*
  Dependent picklists: https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_calls_describesobjects_describesobjectresult.htm
  Base64 characters: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
  A is index 0 so : 000000
  I is index 8 so : 001000
  Q is index 16 so: 010000
  o is index 40 so: 101000

  QAAA is 010000 000000 000000 000000
    Since binary 1 at 2nd position, not index, means the dependent field is valid option for the 2nd option in parent picklist
  IAAA is 001000 000000 000000 000000
    Since binary 1 at 3rd position, not index, means the dependent field is valid option for the 3rd option in parent picklist
  oAAA is 101000 000000 000000 000000
    Since binary 1 at 1st and 3rd position, not index, means the dependent field is valid option for the 1st and 3rd option in parent picklist

  Given: 'QAAA', return string like: '010000000000000000000000'
  */
  base64EncodingToBinaryBits: (value = '') => {
    const base64CharacterSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
    const characterValues = [...value];   // 'QAAA' becomes ['Q', 'A', 'A', 'A']
    const binaryValuesOfCharacterPosition = characterValues.map(character => {
      const characterPosition = base64CharacterSet.indexOf(character);
      const binaryRadix = 2;
      const binaryRepresentation = characterPosition.toString(binaryRadix);
      const base64Radix = 6;    // 2^6 is 64, 6 is the target length of of string
      const base64BinaryRepresentation = binaryRepresentation.padStart(base64Radix, '0'); // adds any needed leading 0's

      return base64BinaryRepresentation;
    });
    const binaryBits = binaryValuesOfCharacterPosition.join('');

    return binaryBits;
  },
  /*
    Binary strings are interpreted from left to right
    Consider on when the binary value is 1
  */
  isBinaryValueOnAtIndex: (binaryStringValue = '', index = 0) => {
    const bit = binaryStringValue[index];
    const isOn = bit
      ? bit === '1'
      : false;

    return isOn;
  }

Tests for helpers:

describe('Base64 string decoded to binary string', () => {
  it('No position on', () => {
    const base64PositionOn = undefined;
    const binaryPositionOn = '';
    const expectedBinaryPositionOn = stringUtilities.base64EncodingToBinaryBits(base64PositionOn);

    expect(expectedBinaryPositionOn).toEqual(binaryPositionOn);
  });

  it('2nd position on', () => {
    const base64PositionOn = 'QAAA';
    const binaryPositionOn = '010000000000000000000000';
    const expectedBinaryPositionOn = stringUtilities.base64EncodingToBinaryBits(base64PositionOn);

    expect(expectedBinaryPositionOn).toEqual(binaryPositionOn);
  });

  it('1st and 3rd position on', () => {
    const base64PositionOn = 'oAAA';
    const binaryPositionOn = '101000000000000000000000';
    const expectedBinaryPositionOn = stringUtilities.base64EncodingToBinaryBits(base64PositionOn);

    expect(expectedBinaryPositionOn).toEqual(binaryPositionOn);
  });
});

describe('Binary string values on or off', () => {
  it('Value on at index for no binary value, index out of bounds', () => {
    const binaryValue = '';
    const someRandomOutOfBoundsIndex = 5;
    const isIndexOn = stringUtilities.isBinaryValueOnAtIndex(binaryValue, someRandomOutOfBoundsIndex);

    expect(isIndexOn).toEqual(false);
  });

  it('Value on at index', () => {
    const binaryValue = '101000000000000000000000';
    const isFirstIndexOn = stringUtilities.isBinaryValueOnAtIndex(binaryValue, 0);
    const isThirdIndexOn = stringUtilities.isBinaryValueOnAtIndex(binaryValue, 2);

    expect(isFirstIndexOn).toEqual(true);
    expect(isThirdIndexOn).toEqual(true);
  });

  it('Value off at index', () => {
    const binaryValue = '101000000000000000000000';
    const isSecondIndexOn = stringUtilities.isBinaryValueOnAtIndex(binaryValue, 1);
    const isFourthIndexOn = stringUtilities.isBinaryValueOnAtIndex(binaryValue, 3);

    expect(isSecondIndexOn).toEqual(false);
    expect(isFourthIndexOn).toEqual(false);
  });
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.