I've been doing a gap analysis for our migration from Seam 2 to Seam 3, and I was dismayed to find that the <s:decorate> tag is gone! In Seam 2, you create a template like this:
And now required fields are noted with an asterisk (*), all fields are automatically validated, and when they have errors, a special style is applied and the error message appears to the right of the field:
Pretty slick! I definitely need this functionality when I migrate to Seam 3, but the <s:decorate> tag is gone, and I had a hard time finding a replacement.
Replacing with UIInputContainer
Fortunately, a close replacement actually exists in Seam 3's Faces module... it just isn't described as such! Seam Faces provides a component called UIInputContainer. When this is combined with a JSF 2 composite component, you can get the same functionality.
First, create your composite component. I created mine at WebContent/resources/orr/decorate.xhtml:
<?xml version="1.0"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:composite="http://java.sun.com/jsf/composite">
<composite:interface componentType="org.jboss.seam.faces.InputContainer" />
<composite:implementation>
<div>
<h:outputLabel id="label" value="#{cc.attrs.label}:"
styleClass="#{cc.attrs.invalid ? 'invalid' : ''}">
<h:outputText styleClass="required" rendered="#{cc.attrs.required}"
value="*" />
</h:outputLabel>
<!-- h:panelGroup is a workaround for a JSF bug, see http://java.net/jira/browse/JAVASERVERFACES-1991 -->
<h:panelGroup styleClass="value #{invalid?'errors':''}" >
<composite:insertChildren />
</h:panelGroup>
<h:message id="message" errorClass="invalid message"
rendered="#{cc.attrs.invalid}" />
</div>
</composite:implementation>
</html>
Since I put the composite component in WebContent/resources/orr/decorate.xhtml, the namespace is http://java.sun.com/jsf/composite/orr and the tag name is decorate.
Now use this new tag in your Facelets page:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:o="http://java.sun.com/jsf/composite/orr">
<o:decorate label="Country:">
<h:inputText value="#{location.country}" required="true"/>
</o:decorate>
And that's it! This will generate roughly the same output and behavior as Seam 2's <s:decorate> tag. So all along, there was a pretty good replacement, but either this wasn't made clear anywhere, or my Google skills aren't quite as good as I think. I imagine this would be documented in JBoss's Seam 2 to Seam 3 migration guide, if such a thing existed...
While there are many resources out there on creating basic custom converters, I had difficulty finding a good explanation of how to create custom converters with attributes. Here are the steps I followed:
Note: I built this using Seam 2.2 on JBoss EAP 5.1, but this should work for any JSF 1.2 application using Facelets.
The USAPhoneNumber class
We'll be creating a converter for a class called USAPhoneNumber. There's nothing special about this class, just a POJO with an attribute for each "part" of a US phone number.
First we'll create an implementation of javax.faces.convert.Converter. We might want the ability to convert it into a few different styles, such as 212-555-7456, (212) 555-7456, 212 555 7456, etc. To support this, we are creating an attribute called style, which will accept values like parentheses, dashes, and spaces.
package org.orr.customconverter;
import java.io.Serializable;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;
public class PhoneNumberConverter implements Converter, Serializable {
private static final long serialVersionUID = 1L;
private String style;
private static final Style DEFAULT_STYLE = Style.DASHES;
private enum Style {
DASHES, SPACES, PARENTHESES
};
public Object getAsObject(FacesContext context, UIComponent component,
String stringValue) {
if (stringValue == null || stringValue.trim().length() == 0)
return null;
// We COULD try to read in the value based on the style, but in this
// case, it's easiest to just strip out all non-numeric characters and
// require that the number be greater than 10 digits, with any digits
// past 10 becoming the extension
String rawNumber = stringValue.replaceAll("[^0-9]", "");
USAPhoneNumber number = null;
if (rawNumber.length() < 10)
throw new ConverterException(new FacesMessage(
"Phone number must have at least 10 numeric characters"));
else if (rawNumber.length() == 10)
number = new USAPhoneNumber(rawNumber.substring(0, 3),
rawNumber.substring(3, 6), rawNumber.substring(6));
else
number = new USAPhoneNumber(rawNumber.substring(0, 3),
rawNumber.substring(3, 6), rawNumber.substring(6, 10),
rawNumber.substring(10));
return number;
}
public String getAsString(FacesContext context, UIComponent component,
Object value) {
USAPhoneNumber number = (USAPhoneNumber) value;
if (number == null)
return "";
String stringValue = null;
Style styleEnum = style == null ? DEFAULT_STYLE : Style.valueOf(style
.toUpperCase());
switch (styleEnum) {
case DASHES:
stringValue = number.getAreaCode() + "-" + number.getPrefix() + "-"
+ number.getLineNumber() + getFormattedExtension(number);
break;
case SPACES:
stringValue = number.getAreaCode() + " " + number.getPrefix() + " "
+ number.getLineNumber() + getFormattedExtension(number);
break;
case PARENTHESES:
stringValue = "(" + number.getAreaCode() + ") "
+ number.getPrefix() + "-" + number.getLineNumber()
+ getFormattedExtension(number);
break;
default:
throw new ConverterException(new FacesMessage("Unsupported style: "
+ style));
}
return stringValue;
}
private String getFormattedExtension(USAPhoneNumber number) {
if (number.getExtension() == null)
return "";
else
return " x" + number.getExtension();
}
public String getStyle() {
return style;
}
public void setStyle(String style) {
this.style = style;
}
}
If you've written a JSF converter before, this will look pretty familiar. However, there are a few things to note:
You must implement java.io.Serializable. After the RENDER RESPONSE phase, JSF serializes the view; in the RENDER RESPONSE phase, it deserializes it. If your converter does not implement Serializable, the attribute(s) (style in this example) will be lost.
You must not make your converter a Seam component. Seam provides some handy annotations to save some of the configuration overhead in creating a converter (see section 33.2 in the Seam reference for details). However, if you are using the same converter with different attribute values on the same page, Seam will reuse the same instance with the same attribute values on the entire page. Note: this might be avoidable by using the STATELESS scope, but I haven't tried it.
Create taglib.xml
Now we need to create a Facelets tag library definition. We'll call it orr-taglib.xml and put it in WebContent/META-INF:
Next, we need to register our converter in faces-config.xml. Note: this is one of those steps that Seam can save for us, but since we aren't making this a Seam component, we need to register the converter manually.
When we first load the page, we can see our converter in action:
And since we have our converter on the inputText component, we can see it converter back to a USAPhoneNumber when we add an extension:
Hopefully, all of this will be a lot easier in future versions of JSF. For those of us stuck on JSF 1.2, though, creating our own converters with attributes can come in handy!