2
votes

I'm trying to build a custom composite component using JSF 2. I have a dialog inside this component that I want to show when I submit the form, however the dialog is never shown when i use appendToBody="false" and not working when i use appendToBody="true".

My search component looks like this.

enter image description here

When user fills in some value to the inputText and presses the seach button, the dialog like this should pop up:

enter image description here

Now the only way I can make this work is having two buttons in my component instead of one. One for submitting the value and one for showing the dialog. This is when I use the dialog with appendToBody="false" property. So my testing component looks like this:

enter image description here

When I use dialog with appendToBody="true" I am able to submit and show the dialog at once, however the dialog has then other issues, like I'm not able to close the dialog. Data is not updated, etc.

Here is my code (lov.xhtml):

<ui:component xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:c="http://java.sun.com/jsp/jstl/core"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:p="http://primefaces.org/ui"
    xmlns:composite="http://java.sun.com/jsf/composite">

    <composite:interface componentType="lov">
        <composite:attribute name="value" required="true" />
        <composite:attribute name="definitionFile" required="true" />
        <composite:attribute name="definitionName" required="true" />
    </composite:interface>

    <composite:implementation>
        <h:outputStylesheet library="css" name="styles.css" />
        <p:inputText id="lovInputText" styleClass="lovInputText"
            value="#{cc.selectedValue}" binding="#{cc.lovInputText}" />
        <p:commandButton styleClass="lovButton" icon="ui-icon-search"
            onclick="searchAndSelect.show();" actionListener="#{cc.updateDialog}"
            update="@form" />
        <p:commandButton value="Open" icon="ui-icon-search"
            onclick="searchAndSelect.show();" />
        <h:outputLabel id="outputLabel" value="#{cc.selectedValue}" />
        <p:dialog id="searchAndSelectDialog" header="Search and Select"
            appendToBody="false" closable="false" resizable="false"
            widgetVar="searchAndSelect" showEffect="fade" hideEffect="fade"
            binding="#{cc.searchAndSelectDialog}">
            <p:panelGrid>
                <p:row>
                    <p:column>
                        <h:outputLabel value="Value: " />
                    </p:column>
                    <p:column>
                        <h:form id="sasInputTextForm" prependId="true">
                            <p:inputText id="sasInputText" value="#{cc.selectedValue}"
                                label="Value" binding="#{cc.sasInputText}" />
                        </h:form>
                    </p:column>
                </p:row>
                <p:row>
                    <p:column colspan="2" styleClass="searchAndResetColumn">
                        <h:form>
                            <p:commandButton value="Search" icon="ui-icon-search"
                                actionListener="#{cc.search}" />
                            <p:commandButton type="button" value="Reset"
                                icon="ui-icon-arrowrefresh-1-e" />
                        </h:form>
                    </p:column>
                </p:row>
                <p:row>
                    <p:column colspan="2">
                        <h:form>
                            <p:dataTable var="item" value="#{cc.data}">
                                <p:columns value="#{cc.columns}" var="column"
                                    columnIndexVar="colIndex" sortBy="#{item[column.property]}"
                                    filterBy="#{item[column.property]}">
                                    <f:facet name="header">#{column.header}</f:facet>  
                            #{item[column.property]}
                        </p:columns>
                            </p:dataTable>
                        </h:form>
                    </p:column>
                </p:row>
            </p:panelGrid>
            <f:facet name="footer">
                <p:commandButton type="button" value="OK" icon="ui-icon-check" />
                <p:commandButton type="button" value="Cancel" icon="ui-icon-cancel"
                    onclick="searchAndSelect.hide();" />
            </f:facet>
        </p:dialog>
    </composite:implementation>
</ui:component>

Here's the component backing bean (Lov.java):

@FacesComponent("lov")
public class Lov extends UIInput implements NamingContainer {

    private List<ColumnModel> columns = new ArrayList<ColumnModel>();

    private String definitionFile;

    private String definitionName;

    private String bean;

    private String attribute;

    private List<String> displayAttributes;

    private UIInput lovInputText;

    private UIInput sasInputText;

    private UIComponent searchAndSelectDialog;

    // Fields
    // -------------------------------------------------------------------------------------

    // Actions
    // ------------------------------------------------------------------------------------

    /**
     * Returns the component family of {@link UINamingContainer}. (that's just
     * required by composite component)
     */
    @Override
    public String getFamily() {
        return UINamingContainer.COMPONENT_FAMILY;
    }

    /**
     * Set the selected and available values of the day, month and year fields
     * based on the model.
     */
    @Override
    public void encodeBegin(FacesContext context) throws IOException {
        System.out.println("Called encodeBeing method...");
        System.out.println("getValue: "+ getValue().toString());
        setSelectedValue(getValue().toString());
        definitionFile = getAttributeValue("definitionFile", null);
        definitionName = getAttributeValue("definitionName", null);

        try {
            parseXml();
            queryData();
        } catch (Exception e) {
            e.printStackTrace();
        }

        createDynamicColumns();

        super.encodeBegin(context);
    }

    /**
     * Returns the submitted value in dd-MM-yyyy format.
     */
    @Override
    public Object getSubmittedValue() {
        System.out.println("====================================================");
        System.out.println("getSubmittedValue method called...");
        System.out.println("submittedValue: " + lovInputText.getSubmittedValue());
        System.out.println("localValue: " +lovInputText.getLocalValue());
        return lovInputText.getSubmittedValue();
    }

    /**
     * Converts the submitted value to concrete {@link Date} instance.
     */
    @Override
    protected Object getConvertedValue(FacesContext context,
            Object submittedValue) {
        return super.getConvertedValue(context, submittedValue);
    }

    public void search(ActionEvent actionEvent) {
        System.out.println("Search method called...");
        try {
            queryData();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Update the available days based on the selected month and year, if necessary.
     */
    public void updateDialog(ActionEvent actionEvent) {
        System.out.println("==========================================================");
        System.out.println("updateDialog method called...");
        System.out.println("getValue: "+getValue().toString());
        setSelectedValue(getValue().toString());
        /*FacesContext context = FacesContext.getCurrentInstance(); // Update dialog
        context.getPartialViewContext().getRenderIds().add(searchAndSelectDialog.getClientId(context));*/
    }

    // Helpers
    // ------------------------------------------------------------------------------------

    /**
     * Return specified attribute value or otherwise the specified default if
     * it's null.
     */
    @SuppressWarnings("unchecked")
    private <T> T getAttributeValue(String key, T defaultValue) {
        T value = (T) getAttributes().get(key);
        return (value != null) ? value : defaultValue;
    }

    /**
     * Create an integer array with values from specified begin to specified
     * end, inclusive.
     */
    private static Integer[] createIntegerArray(int begin, int end) {
        int direction = (begin < end) ? 1 : (begin > end) ? -1 : 0;
        int size = Math.abs(end - begin) + 1;
        Integer[] array = new Integer[size];

        for (int i = 0; i < size; i++) {
            array[i] = begin + (i * direction);
        }

        return array;
    }

    protected void parseXml() throws ParserConfigurationException,
            SAXException, IOException, XPathExpressionException,
            URISyntaxException {
        DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
        dbFactory.setNamespaceAware(true);
        DocumentBuilder builder = dbFactory.newDocumentBuilder();
        ServletContext servletContext = (ServletContext) FacesContext
                .getCurrentInstance().getExternalContext().getContext();
        InputStream is = servletContext.getResourceAsStream(definitionFile);
        Document doc = builder.parse(is);
        XPathFactory factory = XPathFactory.newInstance();
        XPath xpath = factory.newXPath();

        XPathExpression expr = xpath.compile(String.format(
                "/definitions/definition[@name='%s']/bean/@name",
                definitionName));
        Object result = expr.evaluate(doc, XPathConstants.STRING);
        bean = result.toString();
        setBean(bean);
        System.out.println("bean: " + bean);

        expr = xpath
                .compile(String
                        .format("/definitions/definition[@name='%s']/attributes/attribute/@name",
                                definitionName));
        result = expr.evaluate(doc, XPathConstants.STRING);
        attribute = result.toString();
        setAttribute(attribute);
        System.out.println("attribute: " + attribute);

        expr = xpath
                .compile(String
                        .format("/definitions/definition[@name='%s']/displayAttributes/attribute",
                                definitionName));
        result = expr.evaluate(doc, XPathConstants.NODESET);
        displayAttributes = new ArrayList<String>();
        NodeList nodes = (NodeList) result;
        for (int i = 0; i < nodes.getLength(); i++) {
            NamedNodeMap attrs = nodes.item(i).getAttributes();
            for (int j = 0; j < attrs.getLength(); j++) {
                String nodeName = attrs.item(j).getNodeName();
                if (nodeName.equalsIgnoreCase("name")) {
                    displayAttributes.add(attrs.item(j).getNodeValue());
                }
            }
        }
        setDisplayAttributes(displayAttributes);
        System.out.println("displayAttributes: " + displayAttributes.toString());
    }

    protected void queryData() throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, SecurityException, NoSuchMethodException {
        FilterableBean bean = (FilterableBean) findBean(getBean());
        if (lovInputText != null) {
            System.out.println("lovInputText exists");
        }
        System.out.println("queryData value: "+getValue().toString());
        setData(bean.get(getSelectedValue()));
        System.out.println("data: "+ getData().toString());
    }

    @SuppressWarnings("unchecked")
    public static <T> T findBean(String beanName) {
        FacesContext context = FacesContext.getCurrentInstance();
        return (T) context.getApplication().evaluateExpressionGet(context, "#{" + beanName + "}", Object.class);
    }

    protected void createDynamicColumns() {
        if (displayAttributes != null) {
            columns.clear();

            for (String displayAttribute : displayAttributes) {
                String key = displayAttribute.trim();
                columns.add(new ColumnModel(key.toUpperCase(), key));
            }
        }
    }

    // Getters/setters
    // ----------------------------------------------------------------------------

    @SuppressWarnings("rawtypes")
    public List getData() {
        return (List) getStateHelper().get("data");
    }

    @SuppressWarnings("rawtypes")
    public void setData(List data) {
        getStateHelper().put("data", data);
    }

    private String hello = "hello";

    public String getHello() {
        return hello;
    }

    public void setHello(String hello) {
        this.hello = hello;
    }

    public List<ColumnModel> getColumns() {
        return columns;
    }

    public String getSelectedValue() {
        return (String) getStateHelper().get("selectedValue");
    }

    public void setSelectedValue(String selectedValue) {
        getStateHelper().put("selectedValue", selectedValue);
    }

    public UIInput getLovInputText() {
        return lovInputText;
    }

    public void setLovInputText(UIInput lovInputText) {
        this.lovInputText = lovInputText;
    }

    public UIInput getSasInputText() {
        return sasInputText;
    }

    public void setSasInputText(UIInput sasInputText) {
        this.sasInputText = sasInputText;
    }

    public UIComponent getSearchAndSelectDialog() {
        return searchAndSelectDialog;
    }

    public void setSearchAndSelectDialog(UIComponent searchAndSelectDialog) {
        this.searchAndSelectDialog = searchAndSelectDialog;
    }

    public String getBean() {
        return (String) getStateHelper().get("bean");
    }

    public void setBean(String bean) {
        getStateHelper().put("bean", bean);
    }

    public String getAttribute() {
        return (String) getStateHelper().get("attribute");
    }

    public void setAttribute(String attribute) {
        getStateHelper().put("attribute", attribute);
    }

    @SuppressWarnings("unchecked")
    public List<String> getDisplayAttributes() {
        return (List<String>) getStateHelper().get("displayAttributes");
    }

    public void setDisplayAttributes(List<String> displayAttributes) {
        getStateHelper().put("displayAttributes", displayAttributes);
    }

    static public class ColumnModel implements Serializable {

        private String header;
        private String property;

        public ColumnModel(String header, String property) {
            this.header = header;
            this.property = property;
        }

        public String getHeader() {
            return header;
        }

        public String getProperty() {
            return property;
        }
    }
}

Here is how I use the component:

<h:form>
  <my:lov value="#{testBean.region}" definitionFile="/resources/xml/lov/definitions/country.xml" definitionName="LOV_Region"/>
  <p:messages />
</h:form>

I was following BalusC's article to create this component. Here's the link:

How can I use a dialog in a custom composite component, that will be updated with the values and shown at once?

1

1 Answers

4
votes

The culprit is here:

    <p:commandButton ...
        onclick="searchAndSelect.show();" ...
        update="@form" />

The dialog never shows up because you're updating the @form after opening it. Updating the form brings the form back to its default state, which is thus with a closed dialog. That it "worked" with another button is because the other button doesn't update the form. That it "worked" with appendToBody="true" is because it doesn't sit in the same form anymore (and thus in turn need its own form in order to successfully process the submit).

You have 2 options:

  • Open it after updating the form.

    <p:commandButton ... update="@form" oncomplete="searchAndSelect.show();" />
    
  • Don't update the whole form, update only the parts which really need to be updated, such as dialog's body.

    <p:commandButton ... update="dialogContent" oncomplete="searchAndSelect.show();" />
    ...
    <p:dialog ...>
        <h:panelGrid id="dialogContent">
    

    (note that you can continue using onclick here, but I kept the oncomplete there, otherwise the user may see the dialog's content being refreshed in realtime directly after opening it which is disturbing and potentially confusing)

You've by the way another problem with this composite component, but it will expose only if you use multiple instances of the composite component in the same view. Fix the widgetVar to be unique throughout the view, otherwise multiple composite components would override each other's widgetVar until the last one.

You can use composite's own ID to ensure uniqueness:

<p:commandButton ... oncomplete="searchAndSelect_#{cc.id}.show();" />
...
<p:dialog ... widgetVar="searchAndSelect_#{cc.id}">