[SalesForce] inputcheckbox behavior with select option actionfunctions clarification

After looking at a LOT of posts, I've concluded that I'm confused on exactly how input checkboxes behave. I've created a custom wrapper class around a select option that looks like list<string boxId, boolean selected, string Option, string Descr>MS.

As it turns out, it appears that inputcheckboxes won't allow me to use anything other than an actual textstring for an Id. As in it won't allow me to assing a variable to it. Is that correct behavior? I'd like to first confirm that is what's expected and it doesn't have anything to do with something else I've done.

That issue aside, it seems that I'm having difficulty retrieve the value of the checkbox when it's been checked. I've attempted several different methods, so am getting rather perplexed. I want to assign the value to the "selected" variable in the wrapper class which I've also been using in the code on my page which has been assigned an initial value of "false". Here's the most recent iteration of the relevant code and related portions of the controller. I look forward to being enlightened.

Page:

<apex:pageBlockSection Title="Call Types" id="chckbxs"  collapsible="false" >
    <apex:pageBlockTable width="100%"  id="data" value="{!MS}" var="m" align="center" >
        <apex:column id="col1" >
            <apex:outputLabel value="{!m.Option}" >
                <apex:inputCheckbox value="{!m.selected}" styleClass="$('.'+{!m.boxId}+'_box')" label="{!m.Option}" onselect="assignSelectedJS(m.boxId,m.selected,m.Option,m.Descr)" onclick="$('.'+{!m.boxId}+'_descr').toggle();"/>
            </apex:outputLabel>
            <apex:outputLabel value="{!m.Descr}" styleClass="$('.'+{!m.boxId}+'_descr')" >
                <apex:inputTextarea label="{!m.Descr}" value="{!m.Descr}" styleClass="$('.'+{!m.boxId}+'_descr')" />
            </apex:outputLabel>
        </apex:column>  
    </apex:pageBlockTable>      
    <apex:actionFunction action="{!assignSelected}"
        name="assignSelectedJS" rerender="chckbxs">
        <apex:param name="boxId" assignTo="{!m.boxId}" value="" />
        <apex:param name="selected" assignTo="{!m.selected}" value="" />
        <apex:param name="Option" assignTo="{!m.Option}" value="" />
        <apex:param name="Description" assignTo="{!m.Descr}" value="" />
    </apex:actionFunction>

</apex:pageBlockSection>          
<apex:commandButton value="Save Selected" action="{!ProcessSelected}" rerender="thePage" immediate="true">
</apex:commandButton>

In case you're wondering, I'd like the description box to show/hide when the checkbox is clicked, thus the reason for the some of the code that you're seeing, but that's secondary to what I'm trying to learn because right now, I'm not seeing the values for the checkboxes appear in the controller when checked. Also, this code is far different than what I started with. I'm just putting up something you can look at, so I can learn how this needs to be done. I'm tired of wandering in circles in the forest!

Controller:

Controller {

    public void assignSelected(){

        // cycle through list of SelectOptions and check to see if true

        for(ssOption m : MS) {
            if(m.selected == true) {
                selectedOptions.add(m.boxId);
                posOption.put(m.boxId,m.Option);
                posDesc.put(m.boxId,m.Descr);                
            }
        }

    }

    public pageReference processSelected() {

        Do something with selected... 

        list<Task>ntsks = new list<Task>();

        For(string b:selectedOptions){
            task t = new task( Description = posDesc.get(b).....;
            ntsks.add(t); 
        }
        if(ntsks.isEmpty() == false) Database.SaveResult[] lsr = Database.insert(ntsks,false);

    }                   

}

Best Answer

I'll update this with an additional version that uses an actionFunction in the way that it appears you're trying to use it... but wanted to start with this base.


Update: ActionFunction version added down below. It demonstrates the concept, but isn't 100% functional from a requirements perspective in the same way that the actionSupport version is primarily because of the lifecycle of the operations in this page.


Nothing fancy initially, just actionSupport to provide the rerender onchange event to the checkbox. Removed the styleClass attributes written in a jQuery-like selector syntax as they don't directly contribute to the solution.

This page maintains the collection of all items (selected and non) in the page viewstate along with all of the other bits of data contained in the wrapper class instances.

Non-actionFunction version

enter image description here

VF markup:

<apex:page id="thePage" controller="CheckboxActionFunctionDemo">
    <apex:form id="theForm">
        <!-- Show messages to the user here -->
        <apex:pageMessages />

        <!-- Iterate the wrapper class instances and render each in a row-->
        <apex:pageBlock id="thePageblock">
            <apex:pageBlockTable id="data" value="{!ssOptions}" var="ssOpt">
                <apex:column id="col1" >
                    <apex:outputLabel value="{!ssOpt.Option}" for="theCheckbox"></apex:outputLabel>
                    <apex:inputCheckbox id="theCheckbox" value="{!ssOpt.selected}" label="{!ssOpt.Option}">
                        <apex:actionSupport event="onchange" rerender="theForm" status="theStatus" />
                    </apex:inputCheckbox>

                    <apex:outputLabel value="{!ssOpt.Descr}" for="theTextArea" rendered="{!ssOpt.selected}"></apex:outputLabel>
                    <apex:inputTextarea id="theTextArea" label="{!ssOpt.Descr}" value="{!ssOpt.optionText}" rendered="{!ssOpt.selected}" />
                </apex:column>  
            </apex:pageBlockTable>
        </apex:pageBlock>       

        <!-- command button does _not_ have immediate=true, which discards viewstate data in the post -->
        <apex:commandButton value="Save Selected" action="{!processSelected}" rerender="theForm" status="theStatus"></apex:commandButton>

        <!-- show something while we rerender -->
        <apex:actionStatus startText="Working" id="theStatus" />
    </apex:form>
</apex:page>

The controller:

public with sharing class CheckboxActionFunctionDemo {

    public class ssOption {
        public string boxId         { get; set; }
        public boolean selected     { get; set; }
        public string option        { get; set; }
        public string descr         { get; set; }
        public string optionText    { get; set; }
    }

    public List<ssOption> ssOptions { get; set; }

    public CheckboxActionFunctionDemo() {

        // init the list
        ssOptions = new List<ssOption>();

        // populate the list
        for (integer i = 1; i < 12; i++) {
            ssOption opt = new ssOption();
            opt.boxId = '' + i;
            opt.selected = false;
            opt.option = 'Option ' + i;
            opt.descr = 'The description for option ' + i;
            opt.optionText = '';
            ssOptions.add(opt);
        }

    }

    public void assignSelected() {
        // don't really need to iterate the ssOptions collection 
        // and add the elements to a 'selected' list to achieve the behavior desired
        // as long as all items in the ssOptions collection stay in viewstate
    }

    public void processSelected() {

        try {
            // Create a task for every selected thing
            List<Task> ntsks = new List<Task>();
            for (ssOption opt : ssOptions) {

                if (opt.selected) {
                    Task t = new Task(Subject = opt.option, Description = opt.descr);
                    ntsks.add(t);
                }
            }

            if (ntsks.size() > 0) {
                insert ntsks;

                // show the user the good news
                ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'Save was successful'));
            }

        } catch (Exception ex) {

            // show the problem
            ApexPages.addMessages(ex);
        }
    }
}

ActionFunction version

The actionfunction has 4 named parameters on it, these show up in the parameters collection in the controller just as URL params and all of the other form data. The parameter values from calling the actionfunction will be available there and don't necessarily need to be assigned to a controller member using the assignTo syntax.

Data is passed to the actionfunction the same way you would with any JS function params. In this case 3 of them are strings populated with data from merge fields and the 4th is the checked attribute of the checkbox DOM element.

onchange="assignSelectedJS('{!ssOpt.boxId}', this.checked, '{!ssOpt.Option}', '{!ssOpt.Descr}');"

The page:

<apex:page id="thePage" controller="CheckboxActionFunctionDemo2">
    <apex:form id="theForm">

        <apex:pageMessages />

        <apex:pageBlock id="thePageblock">
            <apex:pageBlockTable id="data" value="{!ssOptions}" var="ssOpt">
                <apex:column id="col1" >
                    <apex:outputLabel value="{!ssOpt.Option}" for="theCheckbox"></apex:outputLabel>
                    <apex:inputCheckbox id="theCheckbox" value="{!ssOpt.selected}" label="{!ssOpt.Option}" 
                        onchange="assignSelectedJS('{!ssOpt.boxId}', this.checked, '{!ssOpt.Option}', '{!ssOpt.Descr}');"></apex:inputCheckbox>

                    <apex:outputLabel value="{!ssOpt.Descr}" for="theTextArea" rendered="{!ssOpt.selected}"></apex:outputLabel>
                    <apex:inputTextarea id="theTextArea" label="{!ssOpt.Descr}" value="{!ssOpt.optionText}" rendered="{!ssOpt.selected}" />
                </apex:column>  
            </apex:pageBlockTable>
        </apex:pageBlock>       
        <apex:commandButton value="Save Selected" action="{!processSelected}" rerender="theForm" status="theStatus"></apex:commandButton>

        <apex:actionFunction name="assignSelectedJS" action="{!assignSelected}" rerender="theForm" status="theStatus">
            <apex:param name="boxId"  value="" />
            <apex:param name="selected"  value="" />
            <apex:param name="Option"  value="" />
            <apex:param name="Description" value="" />
        </apex:actionFunction>

        <apex:actionStatus startText="Working" id="theStatus" />
    </apex:form>
</apex:page> 

The controller:

public with sharing class CheckboxActionFunctionDemo2 {

    public class ssOption {
        public string boxId         { get; set; }
        public boolean selected     { get; set; }
        public string option        { get; set; }
        public string descr         { get; set; }
        public string optionText    { get; set; }
    }

    public List<ssOption> ssOptions { get; set; }

    public CheckboxActionFunctionDemo2() {

        // init the list
        ssOptions = new List<ssOption>();

        // populate the list
        for (integer i = 1; i < 12; i++) {
            ssOption opt = new ssOption();
            opt.boxId = '' + i;
            opt.selected = false;
            opt.option = 'Option ' + i;
            opt.descr = 'The description for option ' + i;
            opt.optionText = '';
            ssOptions.add(opt);
        }

    }

    public void assignSelected() {

        try {

            // this map will contain all named params this page is aware of - including the 4 from the actionFunction
            Map<String, Object> paramsMap = ApexPages.currentPage().getParameters();

            // demonstrating the above claim
            /*
            for (String key : paramsMap.keyset()) {
                system.debug('Key: ' + key + ' & Value: ' + paramsMap.get(key));
            }
            */

            // get the params that the actionFunction sent us
            String boxId = (String)paramsMap.get('boxId');
            Boolean selected = Boolean.valueOf(paramsMap.get('selected'));
            String option = (String)paramsMap.get('Option');
            String Description = (String)paramsMap.get('Description');

            // the instance that we're going to update
            ssOption theSSOption = null;

            // this is terribly inefficient - don't do this loop, use a map instead.
            for (ssOption opt : ssOptions) {

                if (opt.boxId == boxId) {
                    theSSOption = opt; // found the match
                    break; // get out of the loop
                }
            }

            if (theSSOption == null) {
                ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.WARNING, 'No matching id found.'));
                return; 
            }

            // update the instance member data
            theSSOption.selected = selected;
            theSSOption.option = option;
            theSSOption.descr = Description;

            // all done, rerender will fire and show hidden fields driven off of checkbox selected state
            // show the user the good news
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'fields updated with data from actionfunction params')); 

        } catch (Exception ex) {
            system.debug(ex);
            ApexPages.addMessages(ex);
        }
    }

    public void processSelected() {

        try {
            // do something with selected
            List<Task> ntsks = new List<Task>();
            for (ssOption opt : ssOptions) {

                if (opt.selected) {
                    Task t = new Task(Subject = opt.option, Description = opt.descr);
                    ntsks.add(t);
                }
            }

            if (ntsks.size() > 0) {
                insert ntsks;

                // show the user the good news
                ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'Save was successful'));
            }

        } catch (Exception ex) {

            // show the problem
            ApexPages.addMessages(ex);
        }
    }
}