For some reason Seam Framework doesn’t include form validation to ensure at least one checkbox is selected out of a group of checkboxes. Like a radio button, but more than one may be selected. One custom code sample was dodgy because validation takes place in JSF lifecycle before the model is updated, so you must wait for the component tree to be built before such a validation can take place.

You may use jQuery validation but what if the user has JavaScript disabled in their web browser? Or the JS payload hasn’t yet arrived? You still require backend assertion for the correct entity state.

The solution I found wasn’t optimal but hey, I’m not the only one wanting this simple form functionality.

Define a custom <mytaglib:atLeastOneValidator> tag

/resources/WEB-INF/web.xml



.
.
.
  
    facelets.LIBRARIES
    /WEB-INF/compositions.taglib.xml
  

/resources/WEB-INF/compositions.taglib.xml




  http://mytaglib.com/jsf
  
    atLeastOneValidator
    
      atLeastOneValidator
    
  

Create AtLeastOneValidator validator specific for that tag

/src/main/validator/AtLeastOneValidator.java

package yourpackage.validator;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.component.html.HtmlSelectBooleanCheckbox;
import javax.faces.context.FacesContext;
import javax.faces.validator.ValidatorException;

import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.faces.Validator;
import org.jboss.seam.annotations.intercept.BypassInterceptors;
import org.jboss.seam.ui.component.html.HtmlLabel;

@Name("atLeastOneValidator")
@Validator
@BypassInterceptors
@Scope(ScopeType.CONVERSATION)
public class AtLeastOneValidator implements javax.faces.validator.Validator, Serializable {

    private static final long serialVersionUID = -4249428843435574402L;

    private static Map> formCheckboxes;

    public void validate(FacesContext context, UIComponent component, Object value)
        throws ValidatorException {

        List checkboxes = new ArrayList();
        String groupWith = "";
        UIComponent rootComponent = FacesContext.getCurrentInstance().getViewRoot();

        if (!(component instanceof HtmlSelectBooleanCheckbox)) {
            throw new ValidatorException(createErrorMessage("atLeastOneValidator can only be used on HtmlSelectBooleanCheckbox components."));
        }

        if (component.getAttributes().get("groupWith") != null) {
            groupWith = (String)component.getAttributes().get("groupWith");
        }

        if (formCheckboxes == null) {
            formCheckboxes = new HashMap>();
        }

        if (formCheckboxes.get(groupWith) == null) {
            formCheckboxes.put(groupWith, new HashMap());
        }

        // Store this component's value, queuing to be checked at the last
        // checkbox in the group to see of any component has the value
        // set to "true"
        formCheckboxes.get(groupWith).put((String)component.getAttributes().get("id"), (Boolean)value);

        // retrieve all checkboxes under this group in the component tree
        getCheckboxes(rootComponent, checkboxes, groupWith);

        // Last checkbox in the component tree of this group, so
        // now check whether at least one is checked.
        if (component.equals(checkboxes.get(checkboxes.size()-1))) {
            boolean atLeastOneChecked = false;
            String styleClass = "";

            for (String componentId : formCheckboxes.get(groupWith).keySet()) {
                if (formCheckboxes.get(groupWith).get(componentId)) {
                    atLeastOneChecked = true;
                    break;
                }
            }

            // highlight or unmark checkboxes/labels
            if (!atLeastOneChecked) {
                styleClass = "required";
            }
            for (HtmlSelectBooleanCheckbox checkbox : checkboxes) {
                if (checkbox.getParent() instanceof HtmlLabel) {
                    ((HtmlLabel)checkbox.getParent()).setStyleClass(styleClass);
                } else {
                    checkbox.setStyleClass(styleClass);
                }
            }

            if (!atLeastOneChecked) {
                throw new ValidatorException(createErrorMessage("At least one " +
                    ((groupWith == null || groupWith.length() == 0) ? "" : "\"" + groupWith + "\" ")
                    + "checkbox must be selected."));
            }
        }
    }

    /**
     * Recursively traverse a component tree to retrieve all the
     * HtmlSelectBooleanCheckbox objects containing a specific groupWith=""
     * attribute.
     *
     * @param component Initial invocation should be the root component.
     * @param checkboxes Initial invocation should be instantiated blank list.
     * @param groupWith Name/label given to the group of checkboxes. null
     *                  or a blank string returns ALL checkboxes in the tree.
     * @return List of child checkboxes in a component tree.
     */
    protected List getCheckboxes(UIComponent component, List checkboxes, String groupWith) {

        if (component != null) {
            for (UIComponent childComponent : component.getChildren()) {
                if (childComponent instanceof HtmlSelectBooleanCheckbox) {
                    if ((groupWith == null || groupWith.length() == 0) ||
                        groupWith.equals(childComponent.getAttributes().get("groupWith"))) { 
                        checkboxes.add((HtmlSelectBooleanCheckbox)childComponent);
                    }
                } else if (childComponent.getChildCount() > 0) {
                    getCheckboxes(childComponent, checkboxes, groupWith);
                }
            }
        }

        return checkboxes;
    }

    private FacesMessage createErrorMessage(String s) {
        FacesMessage message = new FacesMessage();
        message.setDetail(s);
        message.setSummary(s);
        message.setSeverity(FacesMessage.SEVERITY_ERROR);
        return message;
    }
}

Test out <mytaglib:atLeastOneValidator>

/view/atLeastOneValidatorTest.xhtml




    
        
        .
        .
        .
        
            
                
            
            
        
        
            
                
            
            
        
        
            
                
            
            
        
        
            
                
            
            
        
        
            
                
            
            
        
        .
        .
        .