[SalesForce] How to make a Cascading (Dependent) Select List (Drop Down) in a Data Table in VisualForce

Outside of a data table, a cascading select list (or drop down) is done generically like so:

<apex:selectList value="{!parentValue}">
    <apex:selectOptions value="{!ParentOptions}" />
    <apex:actionSupport event="onchange" rerender="children">
</apex:selectList>
<apex:outputPanel id="children"
    <apex:selectList value="{!childValue}">
        <apex:selectOptions value="{!ChildOptions}" />
    </apex:selectList>
</apex:outputPanel>

Where the controller has the following:

public String parentValue {get; set;}
public String childValue = {get; set;}
public List<SelectOption> getParentOptions() {
    List<SelectOption> options = new List<SelectOption>();
    for (obj__c obj : [select id, name from obj__c]) {
        options.add(new SelectOption(obj.id, obj.name));
    }
    return options;
}
public List<SelectOption> getParentOptions() {
    List<SelectOption> options = new List<SelectOption>();
    for (obj2__c obj : [select id, name from obj2__c where obj__c = :parentValue]) {
        options.add(new SelectOption(obj2.id, obj2.name));
    }
    return options;
}

How do you do this in a data table, where a SelectList in one column, when changed, rerenders a dependent SelectList in another column, given that apex:ouputPanel's id can't be dynamically created?

Basically I have a custom object that I wish to display in tabular form and allow the user to edit it. I use on all the String fields of the custom object to allow the user to double click and edit the field. Where there is essentially a picklist of available options for the user to chose from, I use to allow the user to change the option. However, one of these selectList's options will be dependent upon the selection of another SelectList's value. I would not want to refresh the entire data table each time one of the rows selectList's is changed as that would be slow.

<apex:dataTable value="{!objects}" var = "{!o}">
    <apex:column>
        <apex:selectList value="{!o.parent__c}">
            <apex:selectOptions value="{!parentItems}" />
            <!-- <apex:actionSupport> -->
        </apex:selectList>
    </apex:column>
    <apex:column>
        <apex:selectList value="{!o.dependent__c}">
            <apex:selectOptions value="{!depndentItems}" />
        </apex:selectList>
    </apex:column>
</apex:dataTable>

Thank you.

Best Answer

What you need is a Wrapper for each data row you want to display. the wrapper will take care of the SelectOptions.

Below is an example, that displays a list of Opportunities for which you can select a Contact based on the Opportunity's AccountId

To avoid to run into Governor Limits #getItems() is responsible for setting up the selectOptions in bulk initially.

public class SelectListController {

    ItemWrapper[] items;

    // account select options for the opportunities
    public List<SelectOption> getParentOptions() {

        List<SelectOption> options = new List<SelectOption>();

        for (Account record : [select Id, Name from Account LIMIT 100])
        {
            options.add(new SelectOption(record.Id,record.Name));
        }
        return options;
    }


    // returns list of ItemWrappers, each holding an opportuity record as well
    // as a list of contact selectOptions for the opportunity account id
    public ItemWrapper[] getItems(){

        if (items == null)
        {

            items = new ITemWrapper[]{};

            Opportunity[] records = [
                select Id
                     , Name
                     , AccountId
                  from Opportunity
                 where AccountID != null
                 LIMIT 10];

            Id[] accountIds = new Id[]{};
            for (Opportunity record:records)
            {
                accountIds.add(record.AccountId);
            }

            // query accounts with contacts to bulk setup select options
            Map<Id,Account> accountMap = new Map<Id,Account>([
                select Id
                     , (
                select Id
                     , Name
                  from Contacts)
                  from Account
                 where Id IN: accountIds]);

            // setup one ItemWrapper per Opportunity and generate the selectOptions using the Account Map
            for (Opportunity record:records)
            {
                ItemWrapper item = new ItemWrapper(record);
                items.add(item);

                // could by null if AccountId is null
                if (accountMap.containsKey(record.AccountId))
                {
                    for (Contact contact:accountMap.get(record.AccountId).Contacts)
                    {
                        item.contactOptions.add(new SelectOption(contact.Id,contact.Name));
                    }                
                }
            }
        }

        return items;
    }


    // Wrapper with Opportunity and Contact SelectOptions for the Opp's AccountId
    public class ItemWrapper {

        public Opportunity record {get;set;}

        public String contactValue {get;set;}

        public SelectOption[] contactOptions {get;set;}

        public ItemWrapper(Opportunity record){
            this.record = record;
            this.contactOptions = new SelectOption[]{};
        }


        public Id getParentValue(){
            return record.AccountId;
        }


        // changing the parent value, will replace the current contact options with
        // contacts from the for the new account
        public void setParentValue(Id value){

            if (value != record.AccountId)
            {
                contactOptions = new SelectOption[]{};

                for (Contact contact:[select Id, Name from Contact where AccountId =: value ORDER BY NAME])
                {
                    contactOptions.add(new SelectOption(contact.Id,contact.Name));
                }
            }
            record.AccountId = value;
        }
    }
}

Here's the VF Page

<apex:page controller="SelectListController">

<apex:form>
    <apex:pageBlock>

        <apex:pageBlockTable value="{!items}" var="i">

            <apex:column value="{!i.record.Name}"/>

            <apex:column>
                <apex:selectList value="{!i.ParentValue}" size="1">
                    <apex:selectOptions value="{!parentOptions}"/>
                    <apex:actionSupport event="onchange" reRender="contacts"/>
                </apex:selectList>
            </apex:column>

            <apex:column>
                <apex:selectList value="{!i.contactValue}" size="1" id="contacts">
                    <apex:selectOptions value="{!i.contactOptions}"/>
                </apex:selectList>
            </apex:column>


        </apex:pageBlockTable>

    </apex:pageBlock>
</apex:form>
</apex:page>