Search code examples
listjsfvalidationuirepeat

Validate if list in <ui:repeat><h:inputText> has at least one non-empty/null value


I've a List<String> in my model:

private List<String> list;

// Add to list: "d","e","f","a","u","l","t"
// Getter.

I'm presenting it in the view as below:

<ui:repeat value="#{bean.list}" varStatus="loop">
    <h:inputText value="#{bean.list[loop.index]}"/>
</ui:repeat>

This works fine. Now I'd like to validate if the list contains at least one non-empty/null item. How can I create a custom validator for this?


Solution

  • This isn't easily possible with a custom validator. There's namely physically only one input component whose state changes with every iteration round. Your best bet is to hook on postValidate event of <ui:repeat> and then visit its children via UIComponent#visitTree() on the UIRepeat.

    Here's a kickoff example:

    <ui:repeat value="#{bean.list}" varStatus="loop">
        <f:event type="postValidate" listener="#{bean.validateOneOrMore}" />
        <h:inputText value="#{bean.list[loop.index]}"/>
    </ui:repeat>
    

    With this validateOneOrMore() method (again, just a kickoff example, this approach naively assumes that there's only one UIInput component in the repeater):

    public void validateOneOrMore(ComponentSystemEvent event) {
        final FacesContext context = FacesContext.getCurrentInstance();
        final List<String> values = new ArrayList<>();
    
        event.getComponent().visitTree(VisitContext.createVisitContext(context), new VisitCallback() {
            @Override
            public VisitResult visit(VisitContext context, UIComponent target) {
                if (target instanceof UIInput) {
                    values.add((String) ((UIInput) target).getValue());
                }
                return VisitResult.ACCEPT;
            }
        });
    
        values.removeAll(Arrays.asList(null, ""));
    
        if (values.isEmpty()) {
            event.getComponent().visitTree(VisitContext.createVisitContext(context), new VisitCallback() {
                @Override
                public VisitResult visit(VisitContext context, UIComponent target) {
                    if (target instanceof UIInput) {
                        ((UIInput) target).setValid(false);
                    }
                    return VisitResult.ACCEPT;
                }
            });
    
            context.validationFailed();
            context.addMessage(null, new FacesMessage("Please fill out at least one!"));
        }
    }
    

    Note that it visits the tree twice; first time to collect the values and second time to mark those inputs invalid.

    OmniFaces has an <o:validateOneOrMore> component which does a similar thing on fixed components, but it isn't designed for usage in dynamically repeated components.

    See also: