Wednesday, August 6, 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

17 comments:

Vivek said...

I would like to use this example of yours. Please provide details regarding methods such as getFormattedValue, etc which are probably part of your package?

And thanks for the great work

BalusC said...

It is just inherited from MenuRenderer. It is all the complete code.

Vivek said...

Would this ExtendedRenderer work for a radio button? Does the same issue exist as for a list box? And can f:selectItem be included instead of f:selectItems?

reachout said...

How can I be you?

BalusC said...

Try another parents :)

Sanjeev said...

i am using apache myfaces implementation, I couldn't get this to work, as MenuRenderer is not in myfaces

Sanjeev said...

how can i get this working in MyFaces implementation

Sanjeev said...

Anything on my comments?

BalusC said...

I don't use MyFaces, so I can't give a straight answer. But all you need to do is to determine which renderer is associated with the component and then override it.

Christian said...

I cannot understand why my application is not using my extension of the MenuRenderer.

I am using Richfaces in general but my h:selectOneMenu and internal f:selectItem are totally JSF.

I exactly followed your steps but seems that the rendering of the element it is still done by the MenuRenderer code and not mine.

Please help,
Christian

thangaraj said...

Hi BalusC!

Nice Article!
Nice Job!!!

You are saving the time of people!!!

Congrats!!!
Thangaraj K
eBBi-soft

Anurag said...

nice work...
How can I change style of input and image of list. Can I ?

Anurag said...

Can I change only options with this way ?

Julian Osorio Amaya said...

Hi BalusC

I'm populating a h:selectOneMenu tag with a db query but I also need to add a new select to this using javascript.
I'm using this javascript code to add the new option:
var oOpcion = document.createElement("OPTION");
oOpcion.value = 0;
oOpcion.text = myObject.textoTel;
var selTel1 = document.getElementById('formInfoGeneralEscrita:formRadica:telefono1');
addOpcion( selTel1, oOpcion );

the new option is added to the h:selectOneMenu but after submitting the form I'm having a Validation Error: Value is not valid.

Is there another way to add new options to a h:selectOneMenu avoiding javascript use?

Unknown said...

Wow, incredible. Still, 3 years after the original post, this turns out to be a great help!! I started out "the other alternative" but soon realized that it turned out to be too much hard work for a very small gain. You have saved me tons of work, thanks man!!

Thang Pham said...
This comment has been removed by the author.
Thang Pham said...
This comment has been removed by the author.