views:

2345

answers:

3

Hi

When I am using h:selectOneRadio and supplying the list of values in a list as the entire radio button section is exposed as a single unbroken list. I need to arrange it in 3 columns. I have tried giving

<h:panelGrid id="radioGrid" columns="3">
<h:selectOneRadio id="radio1" value="#{bean.var}">
<f:selectItems id="rval" value="#{bean.list}"/>
</h:selectOneRadio>
</h:panelGrid>

But there is no difference in the rendered section. Its not broken up into columns. What am I doing wrong?

A: 

The h:panelGrid contains only one child (a h:selectOneRadio), so it will only ever render one column. The h:selectOneRadio renders a HTML table too. Its renderer only offers two layouts (lineDirection and pageDirection).

You have a few options

  • use JavaScript to modify the table after page load
  • find a 3rd party control that implements the functionality you want
  • write your own selectOneRadio control
McDowell
+3  A: 

An easy option is to write a custom renderer for h:selectOneRadio. I've outlined some of the steps in this post. If you need any more help, add a comment here.

Damo
Neat. It should be noted that this approach (renderer only) would change how all the h:selectOneRadio controls in the web app were rendered.
McDowell
Alternatively, you could extends the h:selectOneRadio component and provide your new renderer for the extension. Thus gives you two ways to show a group of radio buttons.
Drew
Thanks for the solution. The custom renderer should work if we only change the default table layout to a div based layout. This approach may also help in other alignment issues for various components. But due to lack of experience with renderer(and other internal methods used by JSF) I am unable to start off with the implementation. Could you help with a sample snippet as to how we override the method.
Barun
@Barun - note that the two major JSF implementations (Mojarra and MyFaces) are both open source (though with different licensing terms). There is also information on custom controls in the JEE5 tutorial: http://java.sun.com/javaee/5/docs/tutorial/doc/bnavg.html
McDowell
I've added the source code of the renderer to the blog post. Or just grab it here http://www.blog.locuslive.com/?attachment_id=35It's not the most elegant at this stage ;-)
Damo
@DamoThanks a lot...I asked for a snippet and I got the implementation. Just looking at it is giving me better understanding about the internal rendering mechanism. Thanks again :-)
Barun
A: 

I've adapted the code given by Damo, to work with h:selectOneRadio instead of h:selectManycheckbox. To get it working you will need to register it in your faces-config.xml, with:

<render-kit>
    <renderer>
        <component-family>javax.faces.SelectOne</component-family>
        <renderer-type>javax.faces.Radio</renderer-type>
        <renderer-class>test.components.SelectOneRadiobuttonListRenderer</renderer-class>
    </renderer>
</render-kit>

To compile it, you will also need the JSF implementation (typically found in some sort of jsf-impl.jar in you app server).

The code outputs the radiobuttons in divs, instead of a table. You can then use CSS to style them however you would like. I would suggest giving a fixed width to the checkboxDiv and inner divs, and then having the inner divs display as inline blocks:

div.radioButtonDiv{
    width: 300px;
}

div.radioButtonDiv div{
    display: inline-block;
    width: 100px;
}

Which should give the 3 columns you are looking for

The code:

package test.components;

import java.io.IOException;
import java.lang.reflect.Array;
import java.util.Collection;
import java.util.Iterator;

import javax.faces.component.NamingContainer;
import javax.faces.component.UIComponent;
import javax.faces.component.UISelectMany;
import javax.faces.component.UISelectOne;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.model.SelectItem;

import com.sun.faces.renderkit.RenderKitUtils;
import com.sun.faces.renderkit.html_basic.MenuRenderer;
import com.sun.faces.util.MessageUtils;
import com.sun.faces.util.Util;

/**
 * This component ensures that h:selectOneRadio doesn't get rendered using
 * tables.  It is adapted from the code at:
 * http://www.blog.locuslive.com/?p=15
 * 
 * To register it for use, place the following in your faces config:
 * 
 * <render-kit>
 *      <renderer>
 *         <component-family>javax.faces.SelectOne</component-family>
 *         <renderer-type>javax.faces.Radio</renderer-type>
 *         <renderer-class>test.components.SelectOneRadiobuttonListRenderer</renderer-class>
 *      </renderer>
 * </render-kit>
 *  
 * The original comment is below:
 * 
 * ----------------------------------------------------------------------------- *  
 * This is a custom renderer for the h:selectManycheckbox
 * It is intended to bypass the incredibly sucky table based layout used
 * by the standard component.
 *
 * This layout uses an enclosing div with divs for each input.
 * This gives a default layout similar to a vertical layout
 * The layout can then be controlled by css
 *
 * This renderer assigns an class of "checkboxDiv" to the enclosing div
 * The class and styleClass attributes are then applied to the internal
 * divs that house the inputs
 *
 * The following attributes are ignored as they are no longer required when using CSS:
 * - pageDirection
 * - border
 *
 * Note that I am not supporting optionGroups at this stage. They would be relatively
 * easy to implement with another enclosing div
 *
 * @author damianharvey
 *
 */
public class SelectOneRadiobuttonListRenderer extends MenuRenderer {

    public void encodeEnd(FacesContext context, UIComponent component)
            throws IOException {

        if (context == null) {
            throw new NullPointerException(
                    MessageUtils.getExceptionMessageString(MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID,
                                                                        "context"));
        }
        if (component == null) {
            throw new NullPointerException(
                    MessageUtils.getExceptionMessageString(MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID,
                                                                        "component"));
        }

        // suppress rendering if "rendered" property on the component is
        // false.
        if (!component.isRendered()) {
            return;
        }

        ResponseWriter writer = context.getResponseWriter();
        assert(writer != null);

        writer.startElement("div", component);
        if (shouldWriteIdAttribute(component)) {
            writeIdAttributeIfNecessary(context, writer, component);
        }
        writer.writeAttribute("class", "radioButtonDiv", "class");

        Iterator items = RenderKitUtils.getSelectItems(context, component).iterator();
        SelectItem curItem = null;
        int idx = -1;
        while (items.hasNext()) {
            curItem = (SelectItem) items.next();
            idx++;
            renderOption(context, component, curItem, idx);
        }

        writer.endElement("div");

    }

    protected void renderOption(FacesContext context, UIComponent component, SelectItem curItem, int itemNumber)
            throws IOException {

        ResponseWriter writer = context.getResponseWriter();
        assert(writer != null);

        // disable the check box if the attribute is set.
        String labelClass = null;
        boolean componentDisabled = Util.componentIsDisabled(component);

        if (componentDisabled || curItem.isDisabled()) {
            labelClass = (String) component.
                    getAttributes().get("disabledClass");
        } else {
            labelClass = (String) component.
                    getAttributes().get("enabledClass");
        }

        writer.startElement("div", component);  //Added by DAMIAN

        String styleClass = (String) component.getAttributes().get("styleClass");
        String style = (String) component.getAttributes().get("style");

        if (styleClass != null) {
            writer.writeAttribute("class", styleClass, "class");
        }
        if (style != null) {
            writer.writeAttribute("style", style, "style");
        }

        writer.startElement("input", component);
        writer.writeAttribute("name", component.getClientId(context), "clientId");
        String idString = component.getClientId(context) + NamingContainer.SEPARATOR_CHAR + Integer.toString(itemNumber);
        writer.writeAttribute("id", idString, "id");
        String valueString = getFormattedValue(context, component, curItem.getValue());
        writer.writeAttribute("value", valueString, "value");
        writer.writeAttribute("type", "radio", null);

        Object submittedValues[] = getSubmittedSelectedValues(context, component);
        boolean isSelected;

        Class type = String.class;
        Object valuesArray = null;
        Object itemValue = null;
        if (submittedValues != null) {
            valuesArray = submittedValues;
            itemValue = valueString;
        } else {
            valuesArray = getCurrentSelectedValues(context, component);
            itemValue = curItem.getValue();
        }
        if (valuesArray != null) {
            type = valuesArray.getClass().getComponentType();
        }

        // I don't know what this does, but it doens't compile.  Commenting it
        // out doesn't seem to hurt
        // Map<String, Object> requestMap = context.getExternalContext().getRequestMap();
        // requestMap.put(ConverterPropertyEditorBase.TARGET_COMPONENT_ATTRIBUTE_NAME,
        //      component);

        Object newValue = context.getApplication().getExpressionFactory().
                coerceToType(itemValue, type);

        isSelected = isSelected(newValue, valuesArray);

        if (isSelected) {
            writer.writeAttribute(getSelectedTextString(), Boolean.TRUE, null);
        }

        // Don't render the disabled attribute twice if the 'parent'
        // component is already marked disabled.
        if (!Util.componentIsDisabled(component)) {
            if (curItem.isDisabled()) {
                    writer.writeAttribute("disabled", true, "disabled");
            }
        }

        // Apply HTML 4.x attributes specified on UISelectMany component to all
        // items in the list except styleClass and style which are rendered as
        // attributes of outer most table.
        RenderKitUtils.renderPassThruAttributes(writer, component, new String[] { "border", "style" });
        RenderKitUtils.renderXHTMLStyleBooleanAttributes(writer, component);

        writer.endElement("input");
        writer.startElement("label", component);
        writer.writeAttribute("for", idString, "for");
        // if enabledClass or disabledClass attributes are specified, apply
        // it on the label.
        if (labelClass != null) {
            writer.writeAttribute("class", labelClass, "labelClass");
        }
        String itemLabel = curItem.getLabel();
        if (itemLabel != null) {
            writer.writeText(" ", component, null);
            if (!curItem.isEscape()) {
                // It seems the ResponseWriter API should
                // have a writeText() with a boolean property
                // to determine if it content written should
                // be escaped or not.
                writer.write(itemLabel);
            }
            else {
                writer.writeText(itemLabel, component, "label");
            }
        }
        writer.endElement("label");

        writer.endElement("div");   //Added by Damian
    }

    // ------------------------------------------------- Package Private Methods


    String getSelectedTextString() {

        return "checked";

    }

    /** For some odd reason this is a private method in the MenuRenderer superclass
     *
     * @param context
     * @param component
     * @return
     */
    private Object getCurrentSelectedValues(FacesContext context,
            UIComponent component) {

        if (component instanceof UISelectMany) {
            UISelectMany select = (UISelectMany) component;
            Object value = select.getValue();

            if (value instanceof Collection) {

                Collection<?> list = (Collection) value;
                int size = list.size();
                if (size > 0) {
                    // get the type of the first element - Should
                    // we assume that all elements of the List are
                    // the same type?
                    return list.toArray((Object[]) Array.newInstance(list.iterator().next().getClass(), size));
                }
                else {
                    return ((Collection) value).toArray();
                }

            }
            else if (value != null && !value.getClass().isArray()) {
                logger.warning("The UISelectMany value should be an array or a collection type, the actual type is " + value.getClass().getName());
            }

            return value;
        }

        UISelectOne select = (UISelectOne) component;
        Object returnObject;
        if (null != (returnObject = select.getValue())) {
            Object ret = Array.newInstance(returnObject.getClass(), 1);
            Array.set(ret, 0, returnObject);
            return ret;
        }
        return null;

    }

    /** For some odd reason this is a private method in the MenuRenderer superclass
     *
     * @param context
     * @param component
     * @return
     */
    private Object[] getSubmittedSelectedValues(FacesContext context, UIComponent component) {

        if (component instanceof UISelectMany) {
            UISelectMany select = (UISelectMany) component;
            return (Object[]) select.getSubmittedValue();
        }

        UISelectOne select = (UISelectOne) component;
        Object returnObject;
        if (null != (returnObject = select.getSubmittedValue())) {
            return new Object[] { returnObject };
        }
        return null;

    }

    /** For some odd reason this is a private method in the MenuRenderer superclass
     *
     * @param itemValue
     * @param valueArray
     * @return
     */
    private boolean isSelected(Object itemValue, Object valueArray) {

        if (null != valueArray) {
            if (!valueArray.getClass().isArray()) {
                logger.warning("valueArray is not an array, the actual type is " + valueArray.getClass());
                return valueArray.equals(itemValue);
            }
            int len = Array.getLength(valueArray);
            for (int i = 0; i < len; i++) {
                Object value = Array.get(valueArray, i);
                if (value == null) {
                    if (itemValue == null) {
                        return true;
                    }
                }
                else if (value.equals(itemValue)) {
                    return true;
                }
            }
        }
        return false;

    }


}
Luke