How do I attach a FacesMessage from the backing bean to a specific field in a ui:repeat?
Asked Answered
C

2

3

I have a form with a variable number of input elements, like this:

<ui:repeat var="_lang" value="#{myBean.languages}">
    <h:inputTextarea value="${_lang.title}" id="theTitle" />
    <h:messages for="theTitle"/>
</ui:repeat>

When a certain method in the backing bean is triggered, I want to add a message to, say, the second iteration of the ui:repeat, but not the other ones.

I've seen different variations of this question around here, and all problems appear to be due to the ui:repeat's iterations not being available in the JSF component tree.

What I have tried so far:

  • Bind the h:inputTextareas to a Map<String,UIComponent> in the bean. (a) ...Using ...binding="#{myBean.uiMap[_lang.id]}" (where _lang.id is a unique String). This produced JBWEB006017: Target Unreachable, ''BracketSuffix'' returned null. (I dumped a corresponding map of strings using the ids, the same syntax works just fine outside of binding) (b) ...or using ...binding="#{myBean.uiMap.get()}". This renders the page fine, but when I push the button for my method, the setter does not get called, and thus the UIComponents never get added to the Map.

  • Bind the h:inputTextareas to an array UIComponent[] in the bean, prepopulating it with the right number of nulls and then using the row counter of ui:repeat as an index in the xhtml file. Got Null Pointer exceptions, the setter of the array was never called and thus the array was never populated with the actual UIComponents.

  • Bind an outer h:panelGroup to the bean and try to find the input elements recursively amongst its children in the JSF tree. Only one of the inputs is ever found, see the "iterations not available" problem above.

  • I also tried replacing the ui:repeat with c:forEach and generate row numbers manually (so that they'd hopefully be available in the JSF tree), but there I didn't get any rendered output at all.

(Note: The goal is to display a validation error message, but they have to come from the backing bean. Using an f:validator or the likes, even custom ones, is not really an option because I need to validate against data in the backing bean.)

Frankly, I'm out of ideas. This can't be so difficult, can it?

Edit:

For my third attempt, binding to an outer h:panelGroup, here's my JSF finder function:

private List<UIComponent> findTitleComponents(UIComponent node) {
    List<UIComponent> found = new ArrayList<UIComponent>();
    for (UIComponent child : node.getChildren()) {
        if (child.getId().equals("theTitle")) {
            found.add(child);
            log.debug("have found "+child.getClientId());
        } else {
            found.addAll(findTitleComponents(child));
            log.debug("recursion into "+child.getClientId());
        }
    }
    return found;
}

I'm calling this on node, which is the binding UIComponent of the h:panelGroup around the ui:repeat. (I'm using recursion because my live application has a slightly more nested structure) This, I thought, should give me all "theTitle" textareas, so that I then could add messages and read attributes as I pleased. Alas, the method only returns one "theTitle" component, and the log messages show why:

In the DOM of the generated page, the ids are like "myform:myPanelGroup:0:theTitle" (including the iteration counter of the ui:repeat) while the bean only sees getClientId()s like myform:myPanelGroup:theTitle - and that only exist once, for the last (I guess?) iteration.

Chinquapin answered 2/10, 2013 at 7:4 Comment(2)
I feel that either the very first or the very last approach came closest. The last can only be some strange JSTL version thing. Something like <c:forEach var="i" begin="1" end="5"> works perfectly, just iterating over a collection doesn't. And the first... if only that strange Bracket error didn't occur...Chinquapin
I've found someone with a similar problem with the BracketSuffix error in the binding attribute seven years ago, but they didn't solve it either.Chinquapin
W
2

Your attempts to bind the input component to a map/array failed because there are not multiple of those components in the JSF component tree, but only one. The <ui:repeat> doesn't run during view build time producing the JSF component tree. Instead, it runs during view render time producing the HTML output. In other words, the child components of <ui:repeat> are reused everytime during generating HTML output of each iteration.

The particular exception, "Target Unreachable, ''BracketSuffix'' returned null" is been thrown because the variable #{_lang} isn't available during view build time, that moment when the UI component tree is constructed and all id and binding attributes are evaluated. It's only available during view render time.

Those binding attempts would have succeeded if you used <c:forEach> instead. It runs during view build time producing the JSF component tree. You would then end up with physically multiple instances of the child components which in turn produce each their own HTML output without being reused multiple times.

Putting in a panel group and attempting to find all children obviously won't work for the reasons mentioned before. The <ui:repeat> doesn't generate physically multiple JSF components in the component tree. Instead, it reuses the same component to produce the HTML output multiple times depending on the state of the current iteration round.

Replacing by <c:forEach> should have worked. Perhaps you were facing a timing issue because it runs during view build time and you're preparing the model during e.g. preRenderView instead of @PostConstruct or so.

All of above is easier to understand if you carefully read JSTL in JSF2 Facelets... makes sense?


As to your concrete functional requirement, you would normally use a Validator for the job. If you register it on the input component, then it would be invoked for every iteration round. You would immediately have the right input component with the right state at hands as 2nd argument of validate() method and the submitted/converted value as 3rd argument.

If you really need to perform the job afterwards, for example because you need to know about all inputs, then you should be programmatically iterating over the <ui:repeat> yourself. You can do that with help of UIComponent#visitTree() this allows you to collect the input component's state of every iteration round.

E.g.

final FacesContext facesContext = FacesContext.getCurrentInstance();
UIComponent repeat = getItSomehow(); // findComponent, binding, etc.

repeat.visitTree(VisitContext.createVisitContext(facesContext), new VisitCallback() {
    @Override
    public VisitResult visit(VisitContext context, UIComponent target) {
        if (target instanceof UIInput && target.getId().equals("theTitle")) {
            String clientId = target.getClientId(facesContext);
            Object value = ((UIInput) target).getValue();
            // ...
            facesContext.addMessage(clientId, message);                
        }
        return VisitResult.ACCEPT;
    }
});

See also:

Wardlaw answered 2/10, 2013 at 11:37 Comment(7)
Thank you for the extended answer, that was new and valuable piece of information!Care
Very, very interesting! So visitTree() sees things that don't exist for getChildren(). Well... As long as it works - and it does. :-) With one odd exception: When I generate my web page from a two-element list, the DOM contains myform:0 and myform:1, but visitTree claims to be also visiting myform:2, which appears to be simply a clone of myform:1. Is this by design?Chinquapin
In other words, instead of (with my earlier getChildren() attempt) getting only one "theTitle" (with invalid client id) I now get three, the last of which with a non-existent iteration count. The correct number would have been two "theTitle"s...Chinquapin
No, the model still only contains two rows. But I'll check in a testbed... tomorrow. Too many things are happening in the actual project code.Chinquapin
Which JSF impl/version? If Mojarra, make sure you use the latest. There were previously been too many issues as to state management of <ui:repeat> (currently there are still some minors).Wardlaw
My local server says "Mojarra 2.1.19-jbossorg-1 20130304-0420". Not sure what the production servers are / will be running. I understand that that is not the newest. I'll see what I can do w/rp to updating, at least locally.Chinquapin
OK, the problem with visitTree() iterating through one more element than the <ui:repeat> (and its underlying Collection) actually has in the DOM can be reproduced in a simple, independent test case, both in Mojarra 2.1.13 and 2.1.19. My current implementation of the error-message-applier method works OK nonetheless. I'll try to update Mojarra at my earliest convenience, but it's very good that the result is working as it should nonetheless.Chinquapin
C
0

There is another option: Make your own replacement of the whole FacesMessages shebang. With blackjack. And...

Anyway, based on discussions with Johannes Brodwall we opted to avoid the whole visitTree mess and build our own messages mechanism. This included:

1) A ViewScoped bean containing a Map of Multimaps:

private Map<Object, Multimap<String, String>> fieldValidationMessages = new HashMap<>();

This takes an Object as a field identifier (could be the respective bean itself, a UI component or even a String generated at runtime inside the ui:repeat. That identifier then can have an arbitrary number of String messages on another arbitrary number of sub-fields. Very flexible.

The bean also had convenience methods for getting and setting messages on fields and subfields, and for checking whether any messages are stored at all (i.e. whether there were validation errors).

2) A simple xhtml include that displays error messages for a given field, replacing h:messages for...

And that's already it. The catch is that this runs during the application and rendering phase of the lifecycle instead of JSF's own validation phase. But since our project already decided to do bean validation instead of lifecycle validation, this was not an issue.

Chinquapin answered 3/12, 2013 at 15:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.