4

It's not too hard to present the Contact/Account state and country picklists themselves, but the dependency filtering where when you select "United States" as the country only the 50 or so relevant states are presented seems hard to do.

I don't see anything helpful in the lightning:inputAddress component. The subject is not mentioned in the lightning:inputField documentation so I am assuming the dependency logic not addressed there - or is it?

I haven't found a paid or free component on the AppExchange for this.

Beyond that we seem to be back in the territory of each having to get hold of the data somehow - e.g. via a web service call to the User Interface API or in Apex through this somewhat hacky documented but hard to leverage in Apex serializing/deserializing technique - and then each writing our own client-side logic.

Is there a good ready made solution to this problem that I haven't found yet?

PS

Did some experimenting with Benj's serializing/deserializing technique and with a bit of caching added the data for the default state and country picklists can be generated in about 100ms. So if no better solution comes along, I'll use that code - added below in an answer - at the server-side and write a bit of custom client-side code.

  • If you think in terms of dependent picklists, do you have global picklists with other states defined when other countries are selected? If you do, I'd recommend you create custom metadata to define this parent-child dependency relationship that exists between the two picklists. – crmprogdev Apr 14 '18 at 14:15
  • @crmprogdev It's just the Contact and Account standard fields I'm trying to work with and leverage the underlying metadata of, and I don't want to add extra configuration if I an help it. – Keith C Apr 14 '18 at 14:29
  • Which picklists are you working with though? Do you have Global picklists enabled? Otherwise, one picklist can be easily defined as being dependent on the other. With multiple Global picklists, I'm not certain it's quite as simple, but haven't looked at it in a while. I'd be looking at taking the declarative approach to this where possible. – crmprogdev Apr 14 '18 at 15:21
  • @crmprogdev He's trying to tell you that he's not using a custom picklist. He's using the standard state and country picklists, so global values don't apply here – DavidSchach Apr 15 '18 at 4:45
5

For anyone interested, this is my mild reworking of Benj's code that works out the dependencies in Apex code. These changes are mainly aimed at performance (reducing CPU time from over a second to around 100ms for the default country/state dependency):

  • Caches conversion of validFor to controlling field picklist indexes so instead of calculating a few hundred of those only 9 calculations are needed for the default county/state picklists
  • Finds the "1"'s without checking every one of the "0" values in the bits string
  • Only deserializes the picklist entry values needed for the code
  • Ignores the picklist entries that are inactive
  • Outputs the values (code) not the labels so the resulting data is smaller Turns out that the state codes are not unique so the state label does need to be output

The output is of this form:

{
  ...
  "CV" : [ ],
  "CA" : [ "AB", "BC", "MB", "NB", "NL", "NT", "NS", "NU", "ON", "PE", "QC", "SK", "YT" ],
  "CM" : [ ],
  ...
}

and the code is:

public class DependentPicklists {

    private static final String BASE_64_CHARS = ''
            + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
            + 'abcdefghijklmnopqrstuvwxyz'
            + '0123456789+/';

    private class PicklistEntryBean {
        public Boolean active {get; set;}
        public String value {get; set;}
        public String validFor {get; set;}
    }

    private Map<String, Set<Integer>> cache;

    public Map<String, List<String>> controllingToDependentValues(SObjectField controlling, SObjectField dependent) {

        Map<String, List<String>> results = new Map<String, List<String>>();

        if (controlling != null && dependent != null) {

            cache = new Map<String, Set<Integer>>();

            List<String> controllingValues = new List<String>();

            for (PicklistEntry pe : controlling.getDescribe().getPicklistValues()) {
                if (pe.isActive()) {
                    results.put(pe.getValue(), new String[] {});
                    controllingValues.add(pe.getValue());
                }
            }

            for (PicklistEntryBean pe : picklistEntryBeans(dependent.getDescribe().getPicklistValues())) {
                if (pe.active) {
                    for (Integer index : validForToIndices(pe.validFor)) {
                        results.get(controllingValues.get(index)).add(pe.value);
                    }
                }
            }

        }

        return results;
    }

    private Set<Integer> validForToIndices(String validFor) {

        Set<Integer> indices = cache.get(validFor);
        if (indices == null) {
            indices = new Set<Integer>();

            String validForBits = base64ToBits(validFor);
            for (Integer i = validForBits.indexOf('1'); i >= 0; i = validForBits.indexOf('1', i + 1)) {
                indices.add(i);
            }

            cache.put(validFor, indices);
        }

        return indices;
    }

    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;
    }

    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;
    }

    private static PicklistEntryBean[] picklistEntryBeans(List<PicklistEntry> pes) {

        return (List<PicklistEntryBean>) JSON.deserialize(JSON.serialize(pes), List<PicklistEntryBean>.class);
    }
}
  • Its working fine for me but... validForToIndices,base64ToBits,decimalToBinary,BASE_64_CHARS,cache what and why this for? – Gottipati Vamsi Sep 3 '18 at 17:33
  • @GottipatiVamsi Those methods and arrays are just transforming from a compact, encoded format into the easier to use JSON format. – Keith C Sep 4 '18 at 8:17
  • We are using SF state/country picklists and passing in Contact.MailingState and Contact.MailingCountry SObjectField field references to controllingToDependentValues just returns and empty Map. Is this solution still valid? Mind updating the answer on how to call this from an Lightning Component? – Swisher Sweet Nov 18 '18 at 17:52
  • @SwisherSweet We are using this at the moment so AFAIK it works. To get the data you need to add the result of the controllingToDependentValues method to one of the responses in one of your AuraEnabled Apex methods. – Keith C Nov 18 '18 at 18:02
  • @KeithC the response is empty, so I would expect adding it to an Aura enabled method will just result in an empty response. I've posted a new question related to this: salesforce.stackexchange.com/questions/239763/… – Swisher Sweet Nov 18 '18 at 18:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.