Search code examples
jsfprimefacescomposite-componentfacetconditional-rendering

Why are validations from my JSF composite facet being done when the facet is not rendered


I have a problem that validations from a composite's facet are being fired even when I do not render the composite.

I stripped the problem down to the following barebones code.

Here is the composite entityDetailPanel:

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
            xmlns:ui="http://java.sun.com/jsf/facelets"
            xmlns:h="http://java.sun.com/jsf/html"
            xmlns:composite="http://java.sun.com/jsf/composite"
            xmlns:p="http://primefaces.org/ui"
            xmlns:common="http://java.sun.com/jsf/composite/common">

<composite:interface>
    <composite:attribute name="prefix" required="true" />
    <composite:facet name="lowerPanel"/>
</composite:interface>

<composite:implementation>

    <h:form id="#{cc.attrs.prefix}entityDetailForm2" 
            styleClass="#{cc.attrs.prefix}EntityDetailPanelForm #{cc.attrs.prefix}Listener" >

        <p:messages id="#{cc.attrs.prefix}messages" autoUpdate="true" closable="true"/>

        <p:commandButton 
            value="SAVE" 
            update="@(.#{cc.attrs.prefix}Listener), @(.#{cc.attrs.prefix}EntityDetailPanelForm}"/>

        <composite:renderFacet name="lowerPanel" rendered="false"/>
    </h:form>

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

And here is the invocation:

<?xml version="1.0" encoding="UTF-8"?>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
            xmlns:ui="http://java.sun.com/jsf/facelets"
            xmlns:f="http://java.sun.com/jsf/core"
            xmlns:p="http://primefaces.org/ui"
            xmlns:common="http://xmlns.jcp.org/jsf/composite/common">

    <common:entityDetailPanel id="foo" prefix="Instruments">

        <f:facet name="lowerPanel">
            <!--  <p:inputText id="assetClassPrompt" required="true"  requiredMessage="Why do we get this message?"/>-->

            <p:selectOneMenu id="assetClassPrompt" required="true"  requiredMessage="Why do we get this message?"
                             value="#{instrumentController.selectedData.assetClass}">
                 <f:selectItem itemLabel="foo" itemValue="foo"/>
                 <f:selectItem itemLabel="bar" itemValue="bar"/>
            </p:selectOneMenu>
        </f:facet>
    </common:entityDetailPanel>

</ui:composition>

The combobox does NOT show on the screen (because it's not rendered), but why would I be getting a validation for something that's not rendered?

This is what I see when I click the SAVE button:

enter image description here

Stranger yet, is that I see THIS validation error even on other invocations of the composite that do NOT have that combobox.

I also noticed that if I do not include a unique ID on the <messages> tag, the message from one use of the composite will show up in other uses of the composite.

Is this a PrimeFaces or JSF bug, or am I missing something?

You might notice that I have a commented out <inputText> tag. It's worth mentioning that when I replace the <selectOneMenu> and replace it with the <inputText> I no longer see the problem.


I thought it might help to elucidate a bit on the larger problem I'm trying to solve.

I want to create something akin to a <p:layout> that has both fixed elements (for all uses of the composite) and non-fixed elements/panels which are passed in parametrically (for EACH use of the component).

Here is a screenshot where the items indicated in read are things that vary with each invocation of the composite. Everything else is always present in all invocations of the composite.

As you can see, the parameters are:

  1. A button panel (buttons vary depending on context)
  2. Some additional fields to add to the end of a form (which might contain validations
  3. An entire lower panel (which might contain validations)

Sample of the composite

It's worth mentioning that all these things are validated together (for "SAVE" buttons), so it's desirable to have the <form> tag be within the composite output (which includes the panels passed in as parameters).


Solution

  • This problem is two-fold.

    First, the <cc:renderFacet> is never designed to work this way. It does not support the rendered attribute. That it somehow works is because the facet is internally re-interpreted as an UIPanel component and all attributes are (incorrectly) automatically inherited from the tag. You should not rely on that. The rendered attribute is incorrectly considered during render response, causing confusing behavior that it "works". This is technically a bug in the JSF implementation. The attributes are (correctly) not inherited during the postback, causing the trouble you observed. The components are still decoded and validated "in spite of" that they are not rendered.

    Second, the <p:inputText> extends from UIInput which checks before validation if there's any submitted value. A submitted value of null is interpreted as complete absence of the input field in the form, so it's skipped. A submitted value of an empty string is interpeted as an empty value, so it's validated. The <p:selectOneMenu>, however, has overriden the standard UIInput behavior and considers null the same way as an empty string. Even when the submitted value is null (which means that the input field wasn't in the form at all), it's still being validated. This is technically a bug in PrimeFaces side.

    Your intent is at least clear: conditionally render a facet. The <cc:xxx> tags are evaluated during Facelets compile time (which is a step before view build time), so conditionally building the <cc:renderFacet> using JSTL <c:if> will also not ever work.

    Your best bet is redefining "render lower panel" as a composite attribute, and create a backing component to explicitly copy this attribute into the facet after it's being added to the view.

    <cc:interface componentType="entityDetailPanelComposite">
        ...
        <cc:facet name="lowerPanel" />
        <cc:attribute name="renderLowerPanel" type="java.lang.Boolean" default="false" />
    </cc:interface>
    <cc:implementation>
        <f:event type="postAddToView" listener="#{cc.init}" />
        ...
        <cc:renderFacet name="lowerPanel" />
        ...
    </cc:implementation>
    

    @FacesComponent("entityDetailPanelComposite")
    public class EntityDetailPanelComposite extends UINamingContainer {
    
        public void init() {
            UIComponent lowerPanel = getFacets().get("lowerPanel");
            ValueExpression renderLowerPanel = getValueExpression("renderLowerPanel");
    
            if (renderLowerPanel != null) {
                lowerPanel.setValueExpression("rendered", renderLowerPanel); // It's an EL expression.
            } else {
                lowerPanel.getAttributes().put("rendered", getAttributes().get("renderLowerPanel")); // It's a literal value, or the default value.
            }
        }
    
    }
    

    This has the additional benefit you can specify it from client on.

    <my:entityDetailPanel ... renderLowerPanel="true" />