Question

I try to write my own tree component. A tree node renders as a div containing child components of the tree component, for example:

<my:tree id="extendedTree"
         value="#{controller.rootNode}"
         var="node">
    <h:outputText id="xxx" value="#{node.name}" />
    <h:commandLink value="Test" actionListener="#{controller.nodeSelectionActionListener}" />
</my:tree>

So far, so good - everything works as expected, but the h:outputText gets the same id repeatedly.
So I had my component implement javax.faces.NamingController, overwriting getContainerClientId():

@Override
public String getContainerClientId(FacesContext context) {
    String clientId = super.getClientId(context);
    String containerClientId = clientId + ":" + index;
    return containerClientId;
}

index is set and updated during iteration over the nodes. But getContainerClientId() is called only once for every children (not for every iteration and every children, as I would expect). That causes every child id to be prefixed with the same container id:

form:treeid:0:xxx

Same thing for overwriting getClientId().

What did I miss?

Was it helpful?

Solution

The answer is hidden in the bottom of chapter 3.1.6 of JSF 1.2 specification:

3.1.6 Client Identifiers

...

The value returned from this method must be the same throughout the lifetime of the component instance unless setId() is called, in which case it will be recalculated by the next call to getClientId().

In other words, the outcome of getClientId() is by default cached by the JSF component as implemented in UIComponentBase#getClientId() (see also the nullcheck at line 275 as it is in Mojarra 1.2_15) and this cache is resetted when the UIComponentBase#setId() is called (see also line 358 as it is in Mojarra 1.2_15). As long as you don't reset the cached client ID, it will return the same value on every getClientId() call.

So, while rendering the children in encodeChildren() implementation of your component or the renderer which shall most probably look like this,

for (UIComponent child : getChildren()) {
    child.encodeAll(context);
}

you should for every child be calling UIComponent#setId() with the outcome of UIComponent#getId() to reset the internally cached client ID before encoding the child:

for (UIComponent child : getChildren()) {
    child.setId(child.getId());
    child.encodeAll(context);
}

The UIData class behind the <h:dataTable> implementation does that by the way also (see line 1382 as it is in Mojarra 1.2_15). Note that this is not JSF 1.x specific, the same applies on JSF 2.x as well (and also on UIRepeat class behind Facelets <ui:repeat>).

OTHER TIPS

It's worth mentioning that if your component's children have children, then it may also be necessary to refresh their cached ids, too. With this mark-up, slightly adapted from the original:

<my:tree id="extendedTree"
         value="#{controller.rootNode}"
         var="node">
  <h:panelGroup layout="block" id="nodeBlock">
    <h:outputText id="xxx" value="#{node.name}" />
    <h:commandLink value="Test" actionListener="#{controller.nodeSelectionActionListener}" />
  </h:panelGroup>
</my:tree>

The id for the <panelGroup> comes out OK after applying BalusC's fix above but all the sub-components come out with 0 in the iterator.

To fix that, iterate through all the levels of children too and refresh their cached ids. So: child.setId(child.getId()); becomes uncacheId(child); where the uncacheId function is defined:

private void uncacheId(UIComponent el) {
    el.setId(el.getId());
    el.getChildren().forEach(this::uncacheId);
}

That may be obvious but it took me a while to figure out, so ...

h:outputText id gives you same as you didn't make it dynamic. You can create it like:

 <h:outputText id="xxx_#{node.id}" value="#{node.name}" />

Assume node has an 'id' attribute which is an unique.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top