Anil Arora
Anil Arora

Reputation: 11

JSF events not propagating from composite component with backing component

all

I've been working on a composite component for a date range. Essentially, my composite component uses two Richfaces 4.3 calendar components underneath to capture the individual date values, generate a date range (a pair of LocalDate objects). I found this blog entry which was the basis for my custom component that combines the two submitted values on the calendar into one pair value.

Everything seems to work fine and the values are getting updated. However, I'm trying to figure out how to propagate the change event to the using xhtml page for a partial render of another component, and I've been unsuccessful. I've tried everything I could think of, but I think I'm missing something.

The page:

<rich:panel>
    <f:facet name="header">Calendar Date Range Component</f:facet>
    <h:outputText id="out1" binding="#{calendarDateRangeTestBean.component1}"
                 value="#{calendarDateRangeTestBean.dateRange}" converter="localDatePairConverter" /><br/>
    <h:outputText id="out2" value="#{calendarDateRangeTestBean.dateRange}" converter="localDatePairConverter" /><b>NOT WORKING</b>
    <yxp:calendarDateRange id="calendarDateRange" value="#{calendarDateRangeTestBean.dateRange}"
        dataModel="#{calendarDateRangeTestBean}"
            valueChangeListener="#{calendarDateRangeTestBean.processValueChange}">
        <f:ajax execute="@all" listener="#{calendarDateRangeTestBean.processBehaviorEvent}"/>   

        <!-- This doesn't seem to work???? -->
        <f:ajax execute="@all" render="out2" />
    </yxp:calendarDateRange>
</rich:panel>

My test managed bean:

@ViewScoped
@ManagedBean
public class CalendarDateRangeTestBean extends AbstractCalendarDateRangeDataModel implements
        ValueChangeListener, Serializable {

   private static Logger logger = LoggerFactory.getLogger(CalendarDateRangeTestBean.class);

   private Pair<LocalDate> dateRange = Pair.of(LocalDate.now(), LocalDate.now().plusDays(7));

   private UIComponent component1;

   public UIComponent getComponent1() {
       return component1;
   }

   public LocalDateRange getDateRange() {
       return dateRange;
   }

   public void processBehaviorEvent(final javax.faces.event.AjaxBehaviorEvent event) {

       logger.info("processing event " + event + ": " + event.getBehavior());

       final FacesContext context = FacesContext.getCurrentInstance();
       logger.info("Setting render to " + component1.getClientId(context));

       // This seems to cause a rerender of the first component
       context.getPartialViewContext().getRenderIds().add(component1.getClientId(context));

   }

   @Override
   public void processValueChange(final ValueChangeEvent event) throws AbortProcessingException {
       logger.info(this.toString() + ": processing value change event " + event + ": ["
            + event.getOldValue() + ":" + event.getNewValue() + "]");
   }

   public void setComponent1(final UIComponent component1) {
       this.component1 = component1;
   }

   public void setDateRange(final Pair<LocalDate> dateRange) {
       logger.info("Setting date range to " + dateRange);
       this.dateRange = dateRange;
   }

}

My composite component:

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:a4j="http://richfaces.org/a4j"
xmlns:rich="http://richfaces.org/rich"
xmlns:composite="http://java.sun.com/jsf/composite">

<!-- Methods exposed on rich:component are available in the __proto__ object. -->

<composite:interface componentType="com.yieldex.platform.ui.CalendarDateRange">
    <composite:attribute name="value" required="true" type="demo.Pair"/>
    <composite:attribute name="dataModel" required="false" type="demo.Pair" />
    <composite:clientBehavior name="change" event="change" targets="startCalendar endCalendar" default="true"/>
</composite:interface>

<composite:implementation>

    <h:outputStylesheet library="yieldex/platform" name="css/yieldex-platform.css" target="head" />

    <div id="#{cc.clientId}" class="yxp-calendar-date-range">
        <rich:calendar id="startCalendar" 
            binding="#{cc.startCalendar}"
            styleClass="yxp-start-date-range" 
            converter="localDateConverter" mode="ajax"
            dataModel="#{not empty cc.attrs.dataModel ? cc.attrs.dataModel.startCalendarDataModel : standardCalendarDateRangeDataModel.startCalendarDataModel}"
            monthLabels="#{dateRangeMessages.monthNames}"
            weekDayLabelsShort="#{dateRangeMessages.weeksShort}"
            monthLabelsShort="#{dateRangeMessages.monthNames}" popup="false"
            showInput="false" showFooter="false" showWeeksBar="false"
            showWeekDaysBar="true" showApplyButton="false"
            buttonIcon="#{resource['yieldex/platform:img/1x1-transparent.png']}"
            buttonDisabledIcon="#{resource['yieldex/platform:img/1x1-transparent.png']}">

            <f:facet name="weekDays"></f:facet>
            <f:ajax immediate="true" execute="@all" render="@this endCalendar"/>
        </rich:calendar>
        <rich:calendar id="endCalendar" 
            binding="#{cc.endCalendar}"
            styleClass="yxp-end-date-range" 
            converter="localDateConverter" mode="ajax"
            dataModel="#{not empty cc.attrs.dataModel ? cc.attrs.dataModel.endCalendarDataModel : standardCalendarDateRangeDataModel.endCalendarDataModel}"
            monthLabels="#{dateRangeMessages.monthNames}"
            weekDayLabelsShort="#{dateRangeMessages.weeksShort}"
            monthLabelsShort="#{dateRangeMessages.monthNames}" popup="false"
            showInput="false" showFooter="false" showWeeksBar="false"
            showWeekDaysBar="true" showApplyButton="false"
            buttonIcon="#{resource['yieldex/platform:img/1x1-transparent.png']}"
            buttonDisabledIcon="#{resource['yieldex/platform:img/1x1-transparent.png']}">

            <f:facet name="weekDays"></f:facet>
            <f:ajax immediate="true" execute="@all" render="startCalendar @this"/>
        </rich:calendar>
    </div>

</composite:implementation>
</ui:composition>

My backing component:

@FacesComponent("com.yieldex.platform.ui.CalendarDateRange")
public class YXCalendarDateRange extends UIInput implements NamingContainer {

    private UICalendar startCalendarComponent;
    private UICalendar endCalendarComponent;

    @Override
    public void encodeBegin(final FacesContext context) throws IOException {

        final Pair<LocalDate> value = (Pair<LocalDate>) this.getValue();
        if (value == null) {
            startCalendarComponent.setValue(null);
            endCalendarComponent.setValue(null);
        } else {
            startCalendarComponent.setValue(value.getStart());
            endCalendarComponent.setValue(value.getEnd());
        }

        super.encodeBegin(context);
    }

    @Override
    protected Object getConvertedValue(final FacesContext context, final Object submittedValue) {

        final LocalDate startDate = (LocalDate) startCalendarComponent.getConverter().getAsObject(context,
                startCalendarComponent, (String) this.startCalendarComponent.getSubmittedValue());

        final LocalDate endDate = (LocalDate) endCalendarComponent.getConverter().getAsObject(context,
                endCalendarComponent, (String) this.endCalendarComponent.getSubmittedValue());

       if (startDate == null || endDate == null) {
            return null;
       } else {

            if (startDate.isAfter(endDate)) {
                final FacesMessage message = new FacesMessage();
                message.setSeverity(FacesMessage.SEVERITY_ERROR);
                message.setSummary("start date cannot be after end date");
                message.setDetail("start date cannot be after end date");
                throw new ConverterException(message);
            }

            return Pair.of(startDate, endDate);
        }
    }

    public UICalendar getEndCalendar() {
        return this.endCalendarComponent;
    }

    @Override
    public String getFamily() {
        return UINamingContainer.COMPONENT_FAMILY;
    }

    public UICalendar getStartCalendar() {
        return this.startCalendarComponent;
    }

    @Override
    public Object getSubmittedValue() {
       return this;
    }

    public void setEndCalendar(final UICalendar endCalendarComponent) {
        this.endCalendarComponent = endCalendarComponent;
    }

    public void setStartCalendar(final UICalendar startCalendarComponent) {
        this.startCalendarComponent = startCalendarComponent;
    }
}

What I see is that the valueChangedEvent is coming though. I also see my processBehaviorEvent being called, and the first outputText being rerendered as I'm calling that programmatically. But the second one doesn't seem to get rerendered. I am trying to figure out if this is a bug in Mojarra 2.1.25 or is there something fundamentally wrong with my approach. Any suggestions would be greatly appreciated.

Upvotes: 1

Views: 1574

Answers (1)

BalusC
BalusC

Reputation: 1109745

Any client ID in <f:ajax render> is evaluated relative to the parent naming container of the component it has been attached to. In this construct, the <f:ajax> ends up being attached inside the composite component, which is by itself a naming container. However, there's no component with ID out2 inside the composite, which is the problem.

To solve it, specify the absolute client ID. For example, when it's inside a <h:form id="formId"> element:

<f:ajax execute="@all" render=":formId:out2" />

If it's more complicated, binding the component to the view and refer to its client ID dynamically:

<h:outputText id="out2" binding="#{out2}" ... />
...
<f:ajax execute="@all" render=":#{out2.clientId}" />

See also:

Upvotes: 1

Related Questions