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;
}
}