jsfdatatablejsf-2.2selectoneradio

How to make selected h:selectOneRadio of h:dataTable remain selected on postback?


In normal circumstances like this:

  <h:form>           
    <h:selectOneRadio value="#{bean.gender}">
        <f:selectItem itemValue="Male" itemLabel="Male" />
        <f:selectItem itemValue="Female" itemLabel="Female" />
        <f:selectItem itemValue="Other" itemLabel="Other" />
    </h:selectOneRadio>
    <h:commandButton value="Submit" action="#{bean.action}" />
  </h:form>

Selecting one radio button disselects the other & the radio button will be remain selected on the postback. (when the same view is rendered)

However, when we're dealing with an iterating component like <h:dataTable>, the selection is lost.

Consider the snippet:

        <h:form id="hashMapFormId">           
            <b>HASH MAP:</b>
            <h:dataTable value="#{playersBean.dataHashMap.entrySet()}" var="t" border="1">
                <h:column>
                    <f:facet name="header">Select</f:facet>
                    <h:selectOneRadio id="radiosId" onclick="deselectRadios(this.id);" 
                                        value="#{playersBean.select}">
                        <f:selectItem itemValue="null"/>
                    </h:selectOneRadio>
                </h:column> 
            </h:dataTable>
            <h:commandButton value="Show Hash Map Selection" 
                             action="#{playersBean.showSelectedPlayer()}" />
        </h:form>

With disselecting the other radio buttons when one radio button is selected being implemented by simple JavaScript-

            function deselectRadios(id) {

                var f = document.getElementById("hashMapFormId");
                for (var i = 0; i < f.length; i++)
                {
                    var e = f.elements[i];
                    var eid = e.id;
                    if (eid.indexOf("radiosId") !== -1) {
                        if (eid.indexOf(id) === -1) {
                            e.checked = false;
                        } else {
                            e.checked = true;
                        }
                    }
                }
            }

Fire the GET request:
enter image description here

Select a radio button:
enter image description here

Now press the submit button, response:
enter image description here

You see that the radio button gets dis selected on postback. How to solve this shortcoming?

I know it very well that this is due to this component attribute itemValue being null:

<f:selectItem itemValue="null"/>

Solution

  • This trick is a leftover from JSF 1.x / 2.0/2.1 when it wasn't possible to use a <h:selectOneRadio> for single row selection in a <h:dataTable>. This trick originated in my 10 year old blog article Using Datatables - Select row by radio button.

    The root problem is, HTML radio buttons are grouped based on their name attribute, so the webbrowser knows which others to unselect when one is selected. But JSF generates by design a different one for each <h:dataTable> item, with the row index inlined and therefore they can't be grouped and hence the JavaScript based workaround.

    Since JSF 2.2, with the new passthrough elements and attributes feature, it's however possible to force the name attribute to the value of your choice and capture the selected item via a helper <h:inputHidden>. This is fleshed out in another blog article of me, from previous year: Custom layout with h:selectOneRadio in JSF 2.2. The article uses <ui:repeat> as an example, this can be rewritten to <h:dataTable> as below.

    <h:form>
        <h:dataTable value="#{bean.items}" var="item">
            <h:column>
                <input type="radio" jsf:id="item" a:name="#{hiddenItem.clientId}"
                    value="#{item.id}" a:checked="#{item.id eq bean.selectedItemId ? 'checked' : null}" />
            </h:column>
            <h:column>#{item.id}</h:column>
            <h:column>#{item.name}</h:column>
        </h:dataTable>
        <h:inputHidden id="selectedItem" binding="#{hiddenItem}" value="#{bean.selectedItemId}"
            rendered="#{facesContext.currentPhaseId.ordinal ne 6}" />
        <h:commandButton id="submit" value="Submit" action="#{bean.submit}" />
    </h:form>
    

    @Named
    @ViewScoped
    public class Bean implements Serializable {
    
        private List<Item> items;
        private Long selectedItemId;
    
        // ...
    
        public void submit() {
            System.out.println("Selected item ID: " + selectedItemId);
        }
    
        // ...
    }
    

    And yes, the selected radio button remains selected on postback this way. You can also pass whole entities, this only requires a converter on the <h:inputHidden>.