3
votes

I'm having a bit of trouble getting AJAX calls to fire properly in a nested composite component with Mojarra 2.0.3 and PrimeFaces.

The child composite component looks something like this:

<cc:interface componentType="therapy">
    <cc:attribute name="therapyType" type="java.lang.String" required="true"/>
    <cc:attribute name="patientId" type="java.lang.String" required="true"/>
    <cc:attribute name="showHistory" type="java.lang.Boolean" required="false" default="true"/>
    <cc:attribute name="width" type="java.lang.String" required="false" default="350px"/>
    <cc:attribute name="maxHistory" type="java.lang.String" required="false" default="3"/>
    <cc:attribute name="collectDoctor" type="java.lang.Boolean" required="false" default="false"/>
    <cc:attribute name="collectCareDate" type="java.lang.Boolean" required="false" default="false"/>
    <cc:attribute name="important" type="java.lang.Boolean" requred="false" default="false"/>
</cc:interface>

<cc:implementation>

    <script>
        function #{cc.clientId}Toggle(){
            $("##{cc.clientId}_newbutton").toggle();
            $("##{cc.clientId}_savebuttons").toggle();

            if(#{cc.attrs.collectDoctor}){
                $("##{cc.clientId}_doctor").toggle();
            }

            if(#{cc.attrs.collectCareDate}){
                $("##{cc.clientId}_care").toggle();
            }

            $("##{cc.clientId}_newTherapy").toggle(50);
        }

        function #{cc.clientId}rowHighlight(event){
            if(event.status == 'begin'){
                $("##{cc.clientId}_loader").toggle();
            }
            if(event.status == 'success'){
                $("##{cc.clientId}\\:histTable tr:eq(1)").effect("highlight", {color:"#FED17A", easing:"easeInCubic"}, 2000);
            }
        }

        $(function(){
            if(#{cc.attrs.important}){
                $("div[class~='ui-panel-titlebar'][id^='#{cc.clientId}']").css("background", "#FED17A");
            }
        });
    </script>

    <h:form prependId="false">
        <p:panel styleClass="mcoPanel" style="width:#{cc.attrs.width};">
                <f:facet name="header">
                    <h:panelGroup>
                        <span id="#{cc.clientId}_title">#{cc.myType.word}</span>
                        <span id="#{cc.clientId}_newbutton" class="mcoPanel-controls">
                            <span onclick="#{cc.clientId}Toggle();">
                                <h:graphicImage name="page_new.gif" library="images"/>
                            </span>
                        </span>
                        <span id="#{cc.clientId}_savebuttons" class="mcoPanel-controls" style="display:none;">
                            <span id="#{cc.clientId}_loader" style="display:none;">
                                <h:graphicImage name="ajax-loader.gif" library="images" height="16" width="16"/>
                            </span>
                            <h:commandLink action="#{cc.saveNewTherapy}">
                                <f:ajax execute="newOnTherapy newExemption newDoctor newCareDate" render="@form" onevent="#{cc.clientId}rowHighlight"/>
                                <h:graphicImage name="action_save.gif" library="images"/>
                            </h:commandLink>
                            <span onclick="#{cc.clientId}Toggle();">
                                <h:graphicImage name="page_cross.gif" library="images"/>
                            </span>
                        </span>
                    </h:panelGroup>
            </f:facet>
            <div id="#{cc.clientId}_newTherapy" class="mcoPanel-new" style="display:none;">
                <h:outputLabel for="newOnTherapy" value="Satisfied:" style="position:relative; top: -10px;"/>
                <p:selectOneMenu id="newOnTherapy" label="Satisfied" value="#{cc.newOnTherapyValue}" style="width: 60px;">
                    <f:selectItem itemLabel=""/>
                    <f:selectItems value="#{cc.yesNoList}"/>
                </p:selectOneMenu>
                <br/>
                <h:outputLabel for="newExemption" value="Exemption:" style="position:relative; top: -10px;"/>
                <p:selectOneMenu id="newExemption" value="#{cc.newExemption}" style="width: 150px;">
                    <f:selectItems value="#{cc.exemptions}"/>
                </p:selectOneMenu>
                <span id="#{cc.clientId}_doctor" style="display:none">
                    <br/>
                    <h:outputLabel for="newDoctor" value="Doctor:"/>
                    <p:inputText id="newDoctor" value="#{cc.newDoctor}"/>
                </span>
                <span id="#{cc.clientId}_care" style="display:none">
                    <br/>
                    <h:outputLabel for="newCareDate" value="Care Date:"/>
                    <p:calendar id="newCareDate" label="Care Date" value="#{cc.newCareDate}" showButtonPanel="true">
                        <f:converter converterId="dateOfBirthConverter"/>
                    </p:calendar>
                </span>
            </div>
            <h:messages id="#{cc.clientId}_messages" errorClass="errorMessage"/>
            <p:dataTable id="histTable" value="#{cc.history}" var="item" rendered="#{cc.attrs.showHistory}">
                <!-- Table Output -->
            </p:dataTable>
        </p:panel>
    </h:form>

And the parent composite component looks something like this:

<cc:interface>
    <cc:attribute name="title" type="java.lang.String" required="true"/>
</cc:interface>

<cc:implementation>

    <h:outputScript name="containerpanel.js" library="js/components" target="head"/>

    <p:panel toggleable="true" styleClass="contentPanel" toggleSpeed="500" style="width:1100px;">
        <f:facet name="header">
            #{cc.attrs.title}
            <div class="ui-panel-titlebar-icon ui-corner-all ui-state-default contentSaveAll">
                Save All
            </div>
            <div class="ui-panel-titlebar-icon ui-corner-all ui-state-default contentExpandAll">
                ++
            </div>
            <div class="ui-panel-titlebar-icon ui-corner-all ui-state-default contentCollapseAll">
                - -
            </div>
        </f:facet>

        <cc:insertChildren>
                     <!-- Child components go here -->
        </cc:insertChildren>

    </p:panel>

</cc:implementation>

The implementation is supposed to allow for any number of child components inside the container component.

The child components send ajax requests and update their internal components perfectly if they are placed on a page outside of the Container component. In addition, if I use the same structure of the container, but not an actual composite component, everything works just fine.

As soon as I place the child components as children of the Container component, that's where things go wonky. No ajax calls in the child components fire properly (i.e. the save commandLink) Using firebug to help debug this, I've been able to determine the following:

  • The ajax post request appears to be being sent. The "begin" onevent is firing, and looking at the console in firebug shows a post being made to the server, with the proper component ids and values being sent back.
  • The action in the backing bean is not being called. I have a print statement as the first line and it's not printing.
  • The ajax request appears to be completing properly, the "success" onevent is firing.
  • Nothing is being updated on the page. The form does not refresh, nor does the p:messages object at the top of the page.

Obviously I can do this without the Container component, but I would prefer to be able to of course take advantage of code reuse. Am I missing something obvious here that I need to do to get the ajax to work properly?

2

2 Answers

2
votes

I am having the exact same problem with MyFaces 2.1.6. I can't make AJAX calls from nested Composite Components using ClientBehavior in the interface of the CC.

Like moneyT wrote the server is being notified of the events specified in the ajax tag, but when the lifecycle went to phase 5 - Invoke Application, the event was null and no listener was called. More and more it looks like a bug in the implementation of MyFaces.

EDIT: I have found another solution which is much better than hardcoding ajax in the composite component. I have refactored the outer composite component to be facelet. It loses some of the quirks of the composites but it gets the job done. Also it is reusable.

The only solution (more of a workaraund than solution) I found is to hardcode ajax in the composite component rather than to use ClientBehavior, like this:

   <composite:implementation>            
       <h:panelGroup layout="block" id="listBox" class="list-box-scroll-pane" style="height:#{cc.attrs.visibleLines*27 + 5}px; width:#{cc.attrs.width}px">
        <h:inputText id="submit-value" style="visibility: hidden; width:0px; height: 0px;" value="#{cc.attrs.property}">
                 <f:ajax event="change"
                  render="carModel"
                  listener="#{carDetailsMediator.changeCarMake}" 
                  onevent="initComboboxes"/>
        </h:inputText>
        <ui:repeat value="#{cc.attrs.items}" var="element" varStatus="loop">
            <input id="input-#{cc.attrs.customId}-#{loop.index}" type="hidden" 
                            value="#{element.value}" />
            <div id="div-#{cc.attrs.customId}-#{loop.index}" class="list-box-row">#{element.label}</div>
        </ui:repeat>    

        </h:panelGroup>
    </composite:implementation>

However this workaround is not very good, because the idea behind composite components is that you can reuse them with many different options. Hardcoding ajax in it kind of limits the options.

0
votes

I believe your problem is this:

<f:ajax execute="newOnTherapy newExemption newDoctor newCareDate" ...

The execute attribute can refer to the special keywords (Eg. @this, @form, etc...) or according to the documentation:

If a literal is specified, it must be a space-delimited String of component identifiers and/or one of the keywords.

When you are inserting this component as a child to the parent component then the component Ids are going to by dynamically determined and can't be referred to absolutely.