Thursday, October 22, 2015

Custom layout with h:selectOneRadio in JSF 2.2

Introduction

In HTML, particularly since HTML5 wherein the world is made more aware of semantic HTML, tables are discouraged for other purposes than tabular data. In JSF, the only components which by default render unnecessarily a HTML <table> this way are the <h:selectManyCheckbox> and <h:selectOneRadio>. If you loathe HTML tables for non-tabular data, then you could instead of the <h:selectManyCheckbox> render multiple <h:selectBooleanCheckbox>es in a loop based on an altered model. However, that isn't that trivial for a <h:selectOneRadio>. There was no way to achieve a custom layout without customizing the renderer and/or introducing a new component.

Ones who attempted to create multiple <h:selectOneRadio> components with each a single value using an <ui:repeat> or even <c:forEach>, likely already noticed it: the radio buttons are not grouped. When you select one radio button, then the others won't be unselected. The technical problem is, they don't share the same name attribute in generated HTML output. You could throw in some JavaScript to get the unselection to work anyway, but then there's the problem of setting the submitted value in the model. This works only if the last radio button of the group is being selected as they are in JSF side basically all processed individually instead of in a group and thus each radio button of the iteration would override the model value of the previous one.

Maddening. Fortunately a lot of component libraries offer a solution to this such as Tomahawk and PrimeFaces, which support a new layout attribute value of "spread" and an additional component to represent a single button (the <t:radio> resp. <p:radioButton>). Currently, there's ongoing discussion to adopt their approach in the standard JSF component set for the upcoming JSF 2.3.

Passthrough elements and attributes to rescue

But, since JSF 2.2, which came with new support for passthrough elements and attribtues, it's technically possible to override the name attribute when you set it as a passthrough attribute on a passthrough <input type="radio"> element. This opens up new possibilities. I created a quick attempt like below:

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:jsf="http://xmlns.jcp.org/jsf"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
      xmlns:a="http://xmlns.jcp.org/jsf/passthrough">

...

<h:form id="form">
    <ul>
        <ui:repeat id="items" value="#{bean.items}" var="item">
            <li>
                <input type="radio" jsf:id="item" a:name="#{hiddenItem.clientId}"
                    value="#{item}" a:checked="#{item eq hiddenItem.value ? 'checked' : null}" />
                <h:outputLabel for="item" value="#{item}" />
            </li>
        </ui:repeat>
    </ul>

    <h:inputHidden id="selectedItem" binding="#{hiddenItem}" value="#{bean.selectedItem}"
        rendered="#{facesContext.currentPhaseId.ordinal ne 6}" />

    <h:commandButton id="submit" value="Submit" action="#{bean.submit}" />
</h:form>

@Named
@RequestScoped
public class Bean {

    private List<String> items; // +getter
    private String selectedItem; // +getter+setter

    @PostConstruct
    public void init() {
        items = Arrays.asList("one", "two", "three");
    }

    public void submit() {
        System.out.println("Selected item: " + selectedItem);
    }

    // ...
}

It worked wonderfully! For the interested, here's how the generated HTML output look like:

<form id="form" name="form" method="post" action="/playground/test.xhtml" enctype="application/x-www-form-urlencoded">
    <input type="hidden" name="form" value="form" />

    <ul>
        <li>
            <input id="form:items:0:item" name="form:selectedItem" type="radio" value="one" />
            <label for="form:items:0:item">one</label>
        </li>
        <li>
            <input id="form:items:1:item" name="form:selectedItem" type="radio" value="two" />
            <label for="form:items:1:item">two</label>
        </li>
        <li>
            <input id="form:items:2:item" name="form:selectedItem" type="radio" value="three" />
            <label for="form:items:2:item">three</label>
        </li>
    </ul>

    <input id="form:submit" type="submit" name="form:submit" value="submit" />
    <input type="hidden" name="javax.faces.ViewState" id="j_id1:javax.faces.ViewState:0" value="-1234567890123456789:1234567890123456789" autocomplete="off" />
</form>

This is easily to be turned into a tagfile or composite.

How exactly does this work?

In this little snippet there are quite a lot of tricks:

  1. The plain HTML <input type="radio"> element is being used instead of <h:selectOneRadio> because the latter stubbornly renders a HTML table element.
  2. The jsf:id attribute turns the HTML element into a passthrough element which will be backed by a fullworthy JSF component in the JSF component tree, which is in this particular case of HTML <input type="radio"> element a <h:inputText> (see also table 8-4 of the Java EE 7 tutorial on the subject). Its renderer doesn't render a table element. The type="radio" will just be passed through so it ultimately appears as a radio button.
  3. The a:name passthrough attribute overrides the autogenerated HTML name attribute which would otherwise have the iteration index of <ui:repeat> prepended. It's referring the client ID of the <h:inputHidden> further down in the code. So ultimately all radio buttons will have the same name attribute in HTML and JSF will set the submitted value there in the hidden input component.
  4. The a:checked passthrough attribute is being used instead of HTML checked attribute, because the HTML attribute is otherwise still rendered even though there's a null value. As this is a boolean attribute, only its presence would already force the "checked" state in HTML, so it has to be absent altogether. The passthrough attribute does that; it won't render the attribute at all when the value is null. It's referring the value of the <h:inputHidden> further down in the code.
  5. The <h:inputHidden> is being used to capture and set the model value as its name attribute is being taken over by the radio buttons.
  6. The binding attribute of the hidden input offers the radio button the opportunity to take over its client ID as the name of the radio button group. Do note that you do absolutely not need to bind it to a bean property. It's just being bound to the current Facelet scope so it's available elsewhere in the same facelet. For a in depth explanation on binding attribute, see also How does the 'binding' attribute work in JSF? When and how should it be used? In case you'd like to reuse the whole snippet in a composite component like <my:selectOneRadio>, then use binding="#{cc.hiddenItem}" with a UIInput hiddenItem property in the backing component instead to avoid it being shared between multiple instances of the composite component in the same view.
  7. The rendered attribute of the hidden input ensures that it does not in any way generate its HTML output during the JSF render response phase, but is still available in all other phases so it can properly process the submitted value, convert/validate it and update the model value. It isn't possible to let the passthrough component behind the radio button do that job, because JSF uses the component's client ID to extract the submitted value from the request parameter map, while the request parameter name is represented by name attribute, which has been changed to the one of the hidden input in this trick.

This all also works with <c:forEach> instead of <ui:repeat>, you only need to move the <h:inputHidden> to the top, before the <c:forEach>. Otherwise, the hidden input's client ID isn't available at the moment a:name is evaluated. And, you need to append #{loop.index} to the input ID and label for as in jsf:id="item_#{loop.index}" and for="item_#{loop.index}". For a in depth explanation on properly using JSTL in JSF, see also JSTL in JSF2 Facelets… makes sense?

Oh, I'm using xmlns:a="http://xmlns.jcp.org/jsf/passthrough" instead of xmlns:p as shown in the JSF documentation and Java EE 7 tutorial, for the simple reason that I'd like to keep PrimeFaces happy ;) The a stands here just for "attribute".

How about complex objects as item values?

You can just attach a Converter to the <h:inputHidden> the usual way. Ones who are familiar with OmniFaces only need to keep in mind that you can't use a SelectItemsConverter as there's no means of a <f:selectItems> anymore. Fortunately, conversion against the list of entities is still possible with help of the OmniFaces ListConverter like below:

<h:form id="form">
    <ul>
        <ui:repeat id="items" value="#{bean.items}" var="item">
            <li>
                <input type="radio" jsf:id="item" a:name="#{hiddenItem.clientId}"
                    value="#{item}" a:checked="#{item eq hiddenItem.value ? 'checked' : null}" />
                <h:outputLabel for="item" value="#{item.someProperty}" />
            </li>
        </ui:repeat>
    </ul>

    <h:inputHidden id="selectedItem" binding="#{hiddenItem}" value="#{bean.selectedItem}"
        rendered="#{facesContext.currentPhaseId.ordinal ne 6}">
        <o:converter converterId="omnifaces.ListConverter" list="#{bean.items}" />
    </h:inputHidden>

    <h:commandButton id="submit" value="Submit" action="#{bean.submit}" />
</h:form>

The same applies to validation. You can just add required="true" to the <h:inputHidden> the usual way.

Happy coding!

8 comments:

Unknown said...

there is no attribute spread in primefaces. that can be confusing.
there is attribute "custom".
Usage is here:












Unknown said...

well couldnt post usage.
So without tags:
p:selectOneRadio id="placi" value="#{waybillController.placi}" layout="custom"
f:selectItem itemValue="1"

and then

p:radioButton for="placi" id="placiNad" itemIndex="0"

Unknown said...

Where would you attach ajax behaviour in such a scenario? (p:ajax on "change" event)

Unknown said...

This worked for me only when I removed passthrough namespace from 'name' and 'checked' attributes and only left jsf:id there so quotation:"so that the HTML5 input tag's attributes are treated as part of the Facelets page". Unfortunatelly, I couldn't make this work in PrimeFaces 'Tree' component after I've tried the 'custom' layout of PF OneRadio component, which failed obviously. The problem is that the name attribute of radio input is also prepended with a 'rowKey' (or node key if you want) no matter what clean solution you try to make (and I've tried a lot of them). So the last thing is to make custom Tree component or something similar. But anyway, this article was enlightening, thanks ;)

Anonymous said...

hello balusc,

is there a way to attach an ajax-listener to plain html input-elements like in the above radiobutton-example?

boltony said...

OMG! Thank You so much! This works perfectly! :D

Unknown said...

I'm trying to conditionally suppress the "checked" attribute for the radio buttons, following your example using passthrough. For the radio button attribute, I have:

p:checked="#{null}"

thinking that the checked attribute would not appear, yet it does:



Am I missing something? Any help you could offer would be greatly appreciated. Thanks!

LuisFGA said...

Greetings, mr. Scholtz and viewers!

Nowadays, it worked for me, but with some updates as following:

1. The passthrought was not needed, for already using html.
2. The checked condition was always printing, so i used 2 input, each with 'jsf:rendered' attribute for each case: equal and not equal.
3. The 'required' was not working on with a rendering condition, so i left it always rendering.

The code can be found here: https://gist.github.com/luisfga/fbaf01812780064382a7a1599b1113e3

* I tried to post the snippet here but the system does not allow tags
** I was using OpenLiberty21(MyFaces2.3.8) and OmniFaces3.11

Thank you very much, sir.