Accessing JSF nested composite component elements

2019-08-09 03:54发布

问题:

I am trying to DRY up popup windows in my JSF 2 project using composite components.

This code base uses Icefaces 3.3.0 (with their 1.8.2 compatibility layer for historical reasons), Mojarra 2.2.7, and Glassfish 4.1.

I have input.xhtml which provides a text input and uses a 2-button popup (ok/cancel), which in turn builds on the basic popup.

input.xhtml:

<composite:interface>
    <!-- ... -->
    <composite:editableValueHolder name="forInput" targets="theInput"/>
</composite:interface>
<composite:implementation>
    <my:popup2Buttons>
        <ice:inputText id="theInput" value="..."/>
        <script>setInputFocus("#{cc.clientId}:theInput");</script>
    </my:popup2Buttons>
</composite:implementation>         

popup2buttons.xhtml:

<composite:interface>
    <!-- ... -->
</composite:interface>
<composite:implementation>
    <my:popup>
        <composite:insertChildren/>
        <ice:commandButton id="OkButton"
             value="Ok"
             actionListener="..."/>
        <ice:commandButton id="CancelButton"
              value="Cancel"
              actionListener="..."/>
    </my:popup>                    
</composite:implementation>

popup.xhtml:

<composite:interface>
    <!-- ... -->
</composite:interface>
<composite:implementation>
    <script>
    function setInputFocus(id) {
        document.getElementById(id).focus();
    }
    </script>

    <ice:panelPopup>
        <f:facet name="body">
            <h:panelGroup>
                <composite:insertChildren/>
            </h:panelGroup>
        </f:facet>
    </ice:panelPopup>
</composite:implementation>

The popup works mostly as expected, i.e., I can enter something, the ok and cancel buttons work, and validation works as well.

What does not work is my JavaScript code that tries to focus the input when the popup opens.

When I look at the page in Firebug, I see that the input's ID is MyForm:j_idt63:j_idt64:j_idt67:theInput, but the JavaScript code tries to focus an element with the ID MyForm:j_idt63:theInput.

Why is #{cc.clientId} in input.xhtml not the correct ID that the input ends up getting later? What do I need to do to make this work?

I've seen BalusC's hint on adding a binding but I don't want a binding so that the composite component can be independent of any backing beans.

Is there something else I am missing here?

回答1:

Composite components are implicitly naming containers. I.e. they prepend their ID to the client ID of the children. This makes it possible to use multiple of them in the same view without their children causing duplicate IDs in generated HTML output.

In your specific case, you wrapped the input field in another composite which is in turn wrapped in again another composite. If you're absolutely positive that you don't need multiple naming containers wrapping in each other in this specific composition, then those (popup2buttons.xhtml and popup.xhtml) probably shouldn't be composites, but rather <ui:decorate> templates or <ui:composition> tagfiles. See also When to use <ui:include>, tag files, composite components and/or custom components?

Coming back to the technical problem, it's caused because the #{cc.clientId} does not refer the ID of the nested composite component, but of the current composite component. And thus this would be off. As to the potential solution with binding, the answer which you found does nowhere tell that you should use a backing bean for this. The binding="#{foo}" code in the answer was as-is. It really works that way, without a bean property, see also JSF component binding without bean property. However, this construct would indeed fail when you include the same composite multiple times in the same view and thus multiple components share the same binding="#{foo}". It indeed isn't supposed to be shared by multiple components, see also What is component binding in JSF? When it is preferred to be used?

To solve this without a backing bean, you can use a so-called backing component.

com.example.InputComposite

@FacesComponent("inputComposite")
public class InputComposite extends UINamingContainer {

    private UIInput input;

    // +getter+setter.
}

input.xhtml

<cc:interface componentType="inputComposite">
    ...
</cc:interface>
<cc:implementation>
    ...
    <h:inputText binding="#{cc.input}" ... />
    <script>setInputFocus("#{cc.input.clientId}");</script>
    ...
</cc:implementation>

The alternative is to rework them into templates or tagfiles. Be careful that you don't overvalue/overuse composites.