Jumat, 31 Januari 2014

Styling options in h:selectOneMenu






Introduction


Whenever you want to style a HTML 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 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.
*
*

* <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>
*

*
* And define the 'optionClasses' attribute as a f:attribute of the h:selectOneMenu or
* h:selectManyMenu as follows:
*
*

* <f:attribute name="optionClasses" value="option1,option2,option3" />
*

*
* 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:





javax.faces.SelectOne
javax.faces.Menu
net.balusc.jsf.renderer.html.ExtendedMenuRenderer


javax.faces.SelectMany
javax.faces.Menu
net.balusc.jsf.renderer.html.ExtendedMenuRenderer



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:




value="#{myBean.selectedItem}">
name="optionClasses" value="option1, option2" />
value="#{myBean.selectItems}" />

value="submit" action="#{myBean.submit}" />


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