Search code examples
ajaxjsfcomposite-component

How can I add f:ajax support to my composite component that binds a Collection to an h:selectOneMenu and h:selectManyListbox?


This is a follow-up to: How can I create a JSF composite component that binds a Collection to both an h:selectOneMenu and h:selectManyListbox?

I have a composite component that allows a user to toggle between a h:selectOneMenu and h:selectManyListbox and returns a Collection as the "selected" value. This works great. I'm now attempting to add ajax support to it so that, if a value is selected in the menu or listbox, an ajax event is fired (meaning mySelected should have its value populated and I can re-render any other component on the page I want.

<swr:singleMultiSelect list="#{myBean.myList}"
                       selected="#{myBean.mySelected}"
                       singleSelect="#{myBean.singleSelect}">
    <f:ajax event="valueChange" execute="@this" 
            render="buttonPanel"/>
</swr:singleMultiSelect>

With the following attribute in my composite component:

<cc:clientBehavior name="valueChange" event="valueChange"
                   targets="selectOneMenu selectManyListbox"/>

I'm having problems populating mySelected with the value of the selectOneMenu. When I do a non-ajax form submit, processUpdates takes care of this...I'm not quite sure how to have it do the same thing for ajax event though.

Here is my composite component code:

singleMultiSelect.xhtml

<?xml version="1.0" encoding="US-ASCII"?>
<!DOCTYPE html>
<ui:composition
    xmlns:f="http://xmlns.jcp.org/jsf/core"
    xmlns:h="http://xmlns.jcp.org/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:cc="http://java.sun.com/jsf/composite"
    xmlns:c="http://java.sun.com/jstl/core"
    xmlns:ace="http://www.icefaces.org/icefaces/components"
    >

    <cc:interface componentType="singleMultiSelect">

        <!-- The initial list of objects -->
        <cc:attribute name="list" type="java.util.List" required="true"/>
        <!-- The selected objects -->
        <cc:attribute name="selected" type="java.util.Collection" required="true"/>
        <!-- whether to display the selectOneMenu (true) or selectManyBox (false) -->
        <cc:attribute name="singleSelect" type="java.lang.Boolean" 
                  required="false" default="true"/>  
        <cc:clientBehavior name="valueChange" event="valueChange"
                       targets="selectOneMenu selectManyListbox"/>

    </cc:interface>
    <cc:implementation>

        <span id="#{cc.clientId}">

            <ace:checkboxButton id="singleSelectChkBx"
                                value="#{cc.attrs.singleSelect}">
                <ace:ajax render="#{cc.clientId}"/>
            </ace:checkboxButton>                    

            <h:selectOneMenu id="selectOneMenu" 
                             rendered="#{cc.attrs.singleSelect}"
                             binding="#{cc.singleSelected}">                    
                <f:selectItems value="#{cc.attrs.list}"/>
            </h:selectOneMenu>

            <h:selectManyListbox id="selectManyListbox" 
                                 rendered="#{! cc.attrs.singleSelect}"
                                 value="#{cc.attrs.selected}">
                <f:selectItems value="#{cc.attrs.list}"/>
            </h:selectManyListbox>

        </span>

    </cc:implementation>
</ui:composition>

SingleMultiSelect.java

public class SingleMultiSelect extends UINamingContainer {

    private UISelectOne singleSelected;

    public SingleMultiSelect() {
        super();
    }

    @Override
    public void processUpdates(FacesContext context) {
        super.processUpdates(context);

        if (getAttributes().get("singleSelect") == Boolean.TRUE) {
            HashSet selected = new HashSet();
            if(singleSelected.getValue() != null) {
                selected.add(singleSelected.getValue());
            }
            getValueExpression("selected").setValue(context.getELContext(), selected);
        }
    }

    @Override
    public void encodeBegin(FacesContext context) throws IOException {
        if (getAttributes().get("singleSelect") == Boolean.TRUE) {
            Collection selected = (Collection) getAttributes().get("selected");
            if (selected != null && !selected.isEmpty()) {
                singleSelected.setValue(selected.iterator().next());
            } else {
                singleSelected.setValue(null);
            }
        }

        super.encodeBegin(context);
    }
    /**
     * @return the singleSelected
     */
    public UISelectOne getSingleSelected() {
        return singleSelected;
    }

    /**
     * @param singleSelected the singleSelected to set
     */
    public void setSingleSelected(UISelectOne singleSelected) {
        this.singleSelected = singleSelected;
    }
}

Solution

  • Added an ajax tag to the selectOneMenu with a listener (bound to a method in the FacesComponent) which updates the selected Collection:

    singleMultiSelect.xhtml:

    <h:selectOneMenu id="selectOneMenu" 
                     rendered="#{cc.attrs.singleSelect}"
                     binding="#{cc.singleSelected}">                    
        <f:selectItems value="#{cc.attrs.list}"/>
        <f:ajax event="valueChange" execute="@this"
                        listener="#{cc.updateSelected}"/>
    </h:selectOneMenu>
    

    SingleMultiSelect.java:

    public void updateSelected(AjaxBehaviorEvent event) {
    
        FacesContext context = FacesContext.getCurrentInstance();
    
        if (getAttributes().get("singleSelect") == Boolean.TRUE) {
            HashSet selected = new HashSet();
            if(singleSelected.getValue() != null) {
                selected.add(singleSelected.getValue());
            }
            getValueExpression("selected").setValue(context.getELContext(), selected);
        }
    }