Thursday, August 7, 2008

Styling options in h:selectOneMenu

Introduction

Whenever you want to style a HTML <option> element using CSS, you could just use its style or, preferably, class attribute. But in the default Sun JSF Mojarra implementation there is no comparable attribute available for that. The h:selectOneMenu, h:selectManyMenu and f:selectItem tags simply doesn't support it.

When looking at comparable attributes in other elements, you'll notice that h:dataTable has an elegant approach in form of the rowClasses attribute which accepts a commaseparated string of CSS class names which are to be applied on the <tr> elements repeatedly. Now, it would be nice to let among others the h:selectOneMenu support a similar optionClasses attribute.

This can be achieved at two ways: overriding the default renderer class and using the f:attribute to add it as an external component attribute, or overriding the default renderer class, the component class and the tag class to let it support the optionClasses attribute. It might be obvious that the first way is a bit hacky, but it costs much less effort. The second way is more elegant, but it require more code and a custom tld file which should copy all existing component attributes over (tld files unfortunately doesn't know anything about inheritance). BalusC did it and the tld file was almost 500 lines long for only the selectOneMenu and selectManyMenu. Ouch.

This article will handle only the first approach in detail.

Back to top

ExtendedMenuRenderer

Here is how the extended MenuRenderer look like:

package net.balusc.jsf.renderer.html;

import java.io.IOException;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.convert.Converter;
import javax.faces.model.SelectItem;

import com.sun.faces.renderkit.html_basic.MenuRenderer;

/**
 * Extended menu renderer which renders the 'optionClasses' attribute above the standard menu
 * renderer. To use it, define it as follows in the render-kit tag of faces-config.xml.
 * 
 * <pre>
 * &lt;renderer&gt;
 *     &lt;component-family&gt;javax.faces.SelectOne&lt;/component-family&gt;
 *     &lt;renderer-type&gt;javax.faces.Menu&lt;/renderer-type&gt;
 *     &lt;renderer-class&gt;net.balusc.jsf.renderer.html.ExtendedMenuRenderer&lt;/renderer-class&gt;
 * &lt;/renderer&gt;
 * &lt;renderer&gt;
 *     &lt;component-family&gt;javax.faces.SelectMany&lt;/component-family&gt;
 *     &lt;renderer-type&gt;javax.faces.Menu&lt;/renderer-type&gt;
 *     &lt;renderer-class&gt;net.balusc.jsf.renderer.html.ExtendedMenuRenderer&lt;/renderer-class&gt;
 * &lt;/renderer&gt;
 * </pre>
 * 
 * And define the 'optionClasses' attribute as a f:attribute of the h:selectOneMenu or 
 * h:selectManyMenu as follows:
 * 
 * <pre>
 * &lt;f:attribute name="optionClasses" value="option1,option2,option3" /&gt;
 * </pre>
 * 
 * It accepts a comma separated string of CSS class names which are to be applied on the options
 * repeatedly (the same way as you use rowClasses in h:dataTable). The optionClasses will be
 * rendered only if there is no 'disabledClass' or 'enabledClass' being set as an attribute.
 * 
 * @author BalusC
 * @link http://balusc.blogspot.com/styling-options-in-hselectonemenu.html
 */
public class ExtendedMenuRenderer extends MenuRenderer {

    // Override -----------------------------------------------------------------------------------
    
    /**
     * @see com.sun.faces.renderkit.html_basic.MenuRenderer#renderOption(
     *      javax.faces.context.FacesContext, javax.faces.component.UIComponent,
     *      javax.faces.convert.Converter, javax.faces.model.SelectItem, java.lang.Object, 
     *      java.lang.Object[])
     */
    protected void renderOption(FacesContext context, UIComponent component, Converter converter,
        SelectItem currentItem, Object currentSelections, Object[] submittedValues)
            throws IOException
    {
        // Copied from MenuRenderer#renderOption() (and a bit rewritten, but that's just me) ------

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

        // Write 'option' tag.
        writer.writeText("\t", component, null);
        writer.startElement("option", component);

        // Write 'value' attribute.
        String valueString = getFormattedValue(context, component, currentItem.getValue(), converter);
        writer.writeAttribute("value", valueString, "value");

        // Write 'selected' attribute.
        Object valuesArray;
        Object itemValue;
        if (containsaValue(submittedValues)) {
            valuesArray = submittedValues;
            itemValue = valueString;
        } else {
            valuesArray = currentSelections;
            itemValue = currentItem.getValue();
        }
        if (isSelected(context, itemValue, valuesArray)) {
            writer.writeAttribute("selected", true, "selected");
        }

        // Write 'disabled' attribute.
        Boolean disabledAttr = (Boolean) component.getAttributes().get("disabled");
        boolean componentDisabled = disabledAttr != null && disabledAttr.booleanValue();
        if (!componentDisabled && currentItem.isDisabled()) {
            writer.writeAttribute("disabled", true, "disabled");
        }

        // Write 'class' attribute.
        String labelClass;
        if (componentDisabled || currentItem.isDisabled()) {
            labelClass = (String) component.getAttributes().get("disabledClass");
        } else {
            labelClass = (String) component.getAttributes().get("enabledClass");
        }

        // Inserted custom code which checks the optionClasses attribute --------------------------

        if (labelClass == null) {
            String optionClasses = (String) component.getAttributes().get("optionClasses");
            if (optionClasses != null) {
                String[] labelClasses = optionClasses.split("\\s*,\\s*");
                String indexKey = component.getClientId(context) + "_currentOptionIndex";
                Integer index = (Integer) component.getAttributes().get(indexKey);
                if (index == null || index == labelClasses.length) {
                    index = 0;
                }
                labelClass = labelClasses[index];
                component.getAttributes().put(indexKey, ++index);
            }
        }

        // The remaining copy of MenuRenderer#renderOption() --------------------------------------

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

        // Write option body (the option label).
        if (currentItem.isEscape()) {
            String label = currentItem.getLabel();
            if (label == null) {
                label = valueString;
            }
            writer.writeText(label, component, "label");
        } else {
            writer.write(currentItem.getLabel());
        }

        // Write 'option' end tag.
        writer.endElement("option");
        writer.writeText("\n", component, null);
    }

}

Configure it as follows in the faces-config.xml:

<render-kit>
    <renderer>
        <component-family>javax.faces.SelectOne</component-family>
        <renderer-type>javax.faces.Menu</renderer-type>
        <renderer-class>net.balusc.jsf.renderer.html.ExtendedMenuRenderer</renderer-class>
    </renderer>
    <renderer>
        <component-family>javax.faces.SelectMany</component-family>
        <renderer-type>javax.faces.Menu</renderer-type>
        <renderer-class>net.balusc.jsf.renderer.html.ExtendedMenuRenderer</renderer-class>
    </renderer>
</render-kit>

That's all!

Back to top

Basic demonstration example

And now a basic demonstration example how to use it.

The relevant part of the JSF file should look like:

<h:form>
    <h:selectOneMenu value="#{myBean.selectedItem}">
        <f:attribute name="optionClasses" value="option1, option2" />
        <f:selectItems value="#{myBean.selectItems}" />
    </h:selectOneMenu>
    <h:commandButton value="submit" action="#{myBean.submit}" />
</h:form>

Note the f:attribute: this sets the optionClasses attribute value which is been picked up by the ExtendedMenuRenderer. It will apply the given CSS style classes repeatedly on the rendered option elements. You can even use EL in it so that a backing bean can generate the desired String of comma separated CSS style classes based on some conditions.

The CSS styles are definied as follows:

option.option1 {
    background-color: #ccc;
}

option.option2 {
    background-color: #fcc;
}

Note that some web browsers wouldn't apply this on the selected option in the h:selectOneMenu. If desired, you need to add a style class for the <select> element then and apply it as h:selectOneMenu styleClass="className" then.

And now the demo backing bean code, just as usual. Nothing special here.

package mypackage;

import java.util.ArrayList;
import java.util.List;

import javax.faces.model.SelectItem;

public class MyBean {

    // Properties ---------------------------------------------------------------------------------
    
    private List<SelectItem> selectItems;
    private String selectedItem;

    {
        fillSelectItems();
    }

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

    // Getters ------------------------------------------------------------------------------------
    
    public List<SelectItem> getSelectItems() {
         return selectItems;
    }

    public String getSelectedItem() {
         return selectedItem;
    }

    // Setters ------------------------------------------------------------------------------------
    
    public void setSelectedItem(String selectedItem) {
        this.selectedItem = selectedItem;
    }

    // Helpers ------------------------------------------------------------------------------------

    private void fillSelectItems() {
        selectItems = new ArrayList<SelectItem>();
        selectItems.add(new SelectItem("value1", "label1"));
        selectItems.add(new SelectItem("value2", "label2"));
        selectItems.add(new SelectItem("value3", "label3"));
        selectItems.add(new SelectItem("value4", "label4"));
        selectItems.add(new SelectItem("value5", "label5"));
    }

}

Just run it all and you'll see that the options are colored light gray and light red repeatedly!

All the stuff is build, compiled and tested successfully with Sun JSF Mojarra 1.2_09 and Apache Tomcat 6.0.14 in Eclipse Europa IDE. The output is rendered nicely in recent versions of all commonly used browsers (FF, IE, Opera and Safari).

Back to top

And the listboxes then?

Indeed, this won't work for the listboxes (among others h:selectOneListbox and h:selectManyListbox). But you can just follow the same approach and create a renderer which extends com.sun.faces.renderkit.html_basic.ListboxRenderer which is to be configured on the renderer-type of javax.faces.Listbox.

Back to top

Copyright - There is no copyright on the code. You can copy, change and distribute it freely. Just mentioning this site should be fair.

(C) August 2008, BalusC