Automatically setting the label of a component in JSF 2
Name: a value is required here.
If the label attribute isn't used, JSF will show a generated Id instead that is nearly always completely incomprehensible to users. So, this label is something you definitely want to use.
Of course, if the label is going to be used in the error message to identify said component, it should also be rendered on screen somewhere so the user knows which component has that label. For this JSF has the separate <h:outputLabel>
component, which is typically but not necessarily placed right before the input component it labels.
The problem
The thing is though that this label component should nearly always have the exact same value as the label attribute of the component it labels, e.g.
<h:outputLabel for="username" value="Username" /> <h:inputText id="username" value="#{myBean.username}" label="Username" required="true" />
There's a duplication here that feels rather unnecessary (it feels even worse when the label comes from a somewhat longer expression, which is typical for I18N). My co-worker Bauke identified this problem quite some time ago.
Finding a solution
It appears though that an implementation that automatically sets the label attribute of the target "for" component to the value of the outputLabel isn't that difficult, although there are a couple of things to keep in mind.
For starters, a component in JSF doesn't directly have something akin to an @PostConstruct method in which you can set up things. There are tag handlers and meta rules in which you can set up attributes, but when they execute not all components have to exist yet.
Luckily, we always have the plain old constructor and since JSF 2 components can register themselves for system events. This gets us into a method where we can setup things.
Additionally, we have to be aware of state. System event listeners are luckily not stateful, so perfectly suited for tasks that need to be setup once (Phase listeners are stateful though, and will 'come back' after every postback). Attributes of a component are by default stateful, so we only need to set those once, not at every postback. Finally, the API distinguishes between deferred expressions (value expressions) and literals. If we want to support dynamic labels and only want to setup the wiring once, it's important to take this distinction into account.
Finally, when searching for the target "for" component we can take advantage of the fact that typically this component will be close by. Compared to the regular search on the view root, the well-known "relative-up/down" search algorithm is probably more efficient here. This algorithm will start the search in the first naming container that is the parent of the component from where we start our search in the component tree. This will work its way up until there are no more parents, and if the component then still isn't found (practically this is rare if the component indeed exists), then a downward sweep will be done starting from the root.
So, this all comes down to the following piece of code then (slightly abbreviated):
@FacesComponent(OutputLabel.COMPONENT_TYPE) public class OutputLabel extends HtmlOutputLabel implements SystemEventListener { public OutputLabel() { if (!isPostback()) { getViewRoot().subscribeToViewEvent(PreRenderViewEvent.class, this); } } @Override public void processEvent(SystemEvent event) throws AbortProcessingException { String forValue = (String) getAttributes().get("for"); if (!isEmpty(forValue)) { UIComponent forComponent = findComponentRelatively(this, forValue); if (getOptionalLabel(forComponent) == null) { ValueExpression valueExpression = getValueExpression("value"); if (valueExpression != null) { forComponent.setValueExpression("label", valueExpression); } else { forComponent.getAttributes().put("label", getValue()); } } } } }
After creating a tag for this component, the following can now be used on a Facelet:
<o:outputLabel for="username" value="Username" /> <h:inputText id="username" value="#{myBean.username}" required="true" />
If a form is posted with this in it and no value is entered, JSF will respond with something like:
Username: Validation Error: Value is required.
An implementation of this is readily available in the new OmniFaces library, from where you can find the documentation and the full source code. There's also a live demo available.
Arjan Tijms
Comments
Post a Comment