0
votes

I have a div which is supposed to behave like a JQuery dialog. In this div I input some data, trying to validate it, and save it to database as a result. The dialog opens when the user clicks There are 3 problems I've encountered:

1. Selectors for JQuery ID

Basically we know that if we try to find

 <xp:div id="addingDialog"> 
 </xp:div> 
using a JQuery selector like this var dialog = $('#addingDialog') - it would give us no result since id's in XPages are computed dynamically. So I've decided to declare it using classes(styleClasses, if you like) like this

 <xp:div styleClass="addingDialog"> 
 </xp:div> 
JQuery is like this : var dialog = $('.addingDialog'). Seems to work, not nice though.

2. Clicking on the button which is supposed to validate it and make all the backend stuff doesn't work :(

Unfortunately, when I click "Add" button (the button which saves) nothing happens - dialog closes, even if validation fails and doesn't save anything, even if the input was correct.

So I found a solution - Don't use dialog and JQuery.

But this is not the correct solution, at least not for this case. But even here another problem appears - validation

There are 2 dialogs: one is for adding and another for editing (JQuery doesn't allow to have only one dialog with different buttons function), both have validation and I have to input something into editing in order to add new! Initially I thought that validation in XPages works like this - when the user clicks corresponsing button (Add or Edit) all the inputText in corresponding div checks and if it's right - validation succeedes and backend action takes place. The question is - how can I make it work like this? It turned out that every inputText on a page checks. I don't want it to work like this(Screenshot without using JQuery) This is what I see on a page. If I input whatever values in editing dialog and click "+Add Part" everything works fine. Maybe exactly because of this saving actions in the dialog don't occur? Because I have only one dialog open, but validation "sees" that other hidden's inputs are empty so the validation fails? Here's my code

    <xp:div styleClass="dialogAddPart">
    <xp:table>
                <xp:tr>
                <xp:td><xp:label value="Title:" /></xp:td>
                <xp:td>
                <xp:inputText 
                styleClass="doc_field_textinput" id="input_part_title" type="text" size="40" disableClientSideValidation="true" required="true">
                <xp:this.validators>
                <xp:validateRequired
                    message="#{javascript:return('This field is required')}">
                </xp:validateRequired>
                </xp:this.validators>
                </xp:inputText>
                <xp:message id="message15" for="input_part_title"
                                rendered="true" showDetail="false" showSummary="true"
                                style="font-style:italic;background_field-color:rgb(217,234,235);border-color:rgb(102,102,102)">
                </xp:message>
                </xp:td>
                </xp:tr>

                <xp:tr>
                <xp:td><xp:label value="Total:"/></xp:td>
                <xp:td>
                <xp:inputText id="input_tsnb_all"
                                disableClientSideValidation="true"
                                styleClass="doc_field_textinput" required="true" size="40" >
                <xp:this.validators>
                    <xp:validateRequired
                    message="#{javascript:return('This field is required')}">
                    </xp:validateRequired>
                </xp:this.validators>
                <xp:this.converter>
                        <xp:convertNumber pattern="0.000"></xp:convertNumber>
                </xp:this.converter>
                </xp:inputText>
                <xp:message id="message2" for="input_tsnb_all"
                                rendered="true" showDetail="false" showSummary="true"
                                style="font-style:italic;background_field-color:rgb(217,234,235);border-color:rgb(102,102,102)">
                </xp:message>
                </xp:td>
                </xp:tr>

                <xp:tr>
                <xp:td><xp:label value="build-works"/></xp:td>
                <xp:td>
                <xp:inputText type="text" size="40" id="input_tsnb_build_work"
                                disableClientSideValidation="true"
                                styleClass="doc_field_textinput" required="true">
                <xp:this.validators>
                                    <xp:validateRequired
                                        message="#{javascript:return('This field is required')}">
                                    </xp:validateRequired>
                </xp:this.validators>

                <xp:this.converter>
                    <xp:convertNumber pattern="0.000"></xp:convertNumber>
                </xp:this.converter>    

                </xp:inputText>

                    <xp:message id="message3"
                        for="input_tsnb_build_work" rendered="true" showDetail="false"
                        showSummary="true"
                        style="font-style:italic;background_field-color:rgb(217,234,235);border-color:rgb(102,102,102)">
                    </xp:message>

                </xp:td>
                </xp:tr>

                <xp:tr>
                <xp:td><xp:label value="equipment"/></xp:td>
                <xp:td>
                <xp:inputText id="input_tsnb_equipment"
                                disableClientSideValidation="true"
                                styleClass="doc_field_textinput" required="true" size="40" >
                <xp:this.validators>
                    <xp:validateRequired
                    message="#{javascript:return('This field is required')}">
                    </xp:validateRequired>
                </xp:this.validators>
                <xp:this.converter>
                        <xp:convertNumber pattern="0.000"></xp:convertNumber>
                </xp:this.converter>
                </xp:inputText>
                <xp:message id="message5" for="input_tsnb_equipment"
                                rendered="true" showDetail="false" showSummary="true"
                                style="font-style:italic;background_field-color:rgb(217,234,235);border-color:rgb(102,102,102)">
                </xp:message>
                </xp:td>
                </xp:tr>

                <xp:tr>
                <xp:td><xp:label value="other costs"/></xp:td>
                <xp:td>
                <xp:inputText type="text" size="40" id="input_tsnb_other_costs"
                                disableClientSideValidation="true"
                                styleClass="doc_field_textinput" required="true" >
                <xp:this.validators>
                    <xp:validateRequired
                        message="#{javascript:return('This field is required')}">
                    </xp:validateRequired>
                </xp:this.validators>

                <xp:this.converter>
                    <xp:convertNumber pattern="0.000"></xp:convertNumber>
                </xp:this.converter>

                </xp:inputText>

                    <xp:message id="message6"
                        for="input_tsnb_other_costs" rendered="true" showDetail="false"
                        showSummary="true"
                        style="font-style:italic;background_field-color:rgb(217,234,235);border-color:rgb(102,102,102)">
                    </xp:message>

                </xp:td>
                </xp:tr>

                <xp:tr>
                <xp:td><xp:label value="including tax"/></xp:td>
                <xp:td>
                <xp:inputText type="text" size="40" id="input_tsnb_pir"
                                disableClientSideValidation="true"
                                styleClass="doc_field_textinput" required="true">

                <xp:this.validators>
                        <xp:validateRequired
                                message="#{javascript:return('This field is required')}">
                        </xp:validateRequired>
                </xp:this.validators>

                <xp:this.converter>
                    <xp:convertNumber pattern="0.000"></xp:convertNumber>
                </xp:this.converter>                

                </xp:inputText>

                <xp:message id="message7" for="input_tsnb_pir"
                        rendered="true" showDetail="false" showSummary="true"
                        style="font-style:italic;background_field-color:rgb(217,234,235);border-color:rgb(102,102,102)">
                </xp:message>

                </xp:td>
                </xp:tr>

                <xp:tr>
                <xp:td><xp:label value="return sum"/></xp:td>
                <xp:td>

                <xp:inputText type="text" size="40"
                                id="input_tsnb_return"
                                disableClientSideValidation="true"
                                styleClass="doc_field_textinput" required="true">

                    <xp:this.validators>
                        <xp:validateRequired
                            message="#{javascript:return('This field is required')}">
                        </xp:validateRequired>
                    </xp:this.validators>

                    <xp:this.converter>
                        <xp:convertNumber pattern="0.000"></xp:convertNumber>
                    </xp:this.converter>

                </xp:inputText>

                <xp:message id="message8"
                    for="input_tsnb_return" rendered="true" showDetail="false"
                    showSummary="true"
                    style="font-style:italic;background_field-color:rgb(217,234,235);border-color:rgb(102,102,102)">
                </xp:message>

                </xp:td>
                </xp:tr>

<xp:tr>
            <xp:td colspan="2" style="padding-top: 15px">
             <xp:button id="save_part_btn" value="+Add part" style="float:right;">
             <xp:eventHandler event="onclick" submit="true"
                refreshMode="complete">
                <xp:this.action><![CDATA[#{javascript:
                            //Backend code  
}]]></xp:this.action>
             </xp:eventHandler>
             </xp:button>
            </xp:td>
            </xp:tr>
            </xp:table>
    </xp:div>

The code for editing is just the same, expect values are retrieved using corresponding part (backend) and ids have prefix _edit. Save changes button doesn't have any backend action yet.

My JQuery is:

$(document).ready(function() {
/*
Ignore it
  $('.partTableContent').hide();
  $('.expandButton').click(function() {
    // .parent() selects the A tag, .next() selects the P tag
    $(this).closest('tr').next(' tr').find('div.partTableContent').slideToggle(750);
  }); 
*/   

  var dialogAddPartDiv = $('.dialogAddPart'); 

  $('.addButton').click(function() 
  {
    dialogAddPartDiv.dialog('open');
  });

  dialogAddPartDiv.dialog(
  {
  create: function (event, ui) {


                $(".ui-corner-all").css('border-bottom-right-radius','8px');
                $(".ui-corner-all").css('border-bottom-left-radius','8px');
                $(".ui-corner-all").css('border-top-right-radius','8px');
                $(".ui-corner-all").css('border-top-left-radius','8px');

                $(".ui-dialog").css('border-bottom-left-radius','0px');
                $(".ui-dialog").css('border-bottom-right-radius','0px');
                $(".ui-dialog").css('border-top-left-radius','0px');
                $(".ui-dialog").css('border-top-right-radius','0px');

                $('.ui-dialog-titlebar-close').css('margin', '-25px -20px 0px 0px').css('border', 'solid 2px').css('border-radius', '15px').css('border-color', '#05788d');
                $('.ui-dialog-titlebar-close').css('width', '25px').css('height', '25px');
            },

    autoOpen: false,
    modal: true,
    beforeClose : function(event) 
    {
        if(!confirm("Part won't be saved. Continue"))
        {
        return false;
        }
        else 
        {

        }
    },
    width:600,
    resizable: false
  });


  var dialogEditPartDiv = $('#dialogEditPart'); 

  $('.editButton').click(function() 
  {
      dialogEditPartDiv.dialog('open');
  });

  dialogEditPartDiv.dialog(
  {
  create: function (event, ui) {

                $(".ui-corner-all").css('border-bottom-right-radius','8px');
                $(".ui-corner-all").css('border-bottom-left-radius','8px');
                $(".ui-corner-all").css('border-top-right-radius','8px');
                $(".ui-corner-all").css('border-top-left-radius','8px');

                $(".ui-dialog").css('border-bottom-left-radius','0px');
                $(".ui-dialog").css('border-bottom-right-radius','0px');
                $(".ui-dialog").css('border-top-left-radius','0px');
                $(".ui-dialog").css('border-top-right-radius','0px');

                $('.ui-dialog-titlebar-close').css('margin', '-25px -20px 0px 0px').css('border', 'solid 2px').css('border-radius', '15px').css('border-color', '#05788d');
                $('.ui-dialog-titlebar-close').css('width', '25px').css('height', '25px');
    },
    autoOpen: false,
    modal: true,
    beforeClose : function(event) 
    {
        if(!confirm("Changes won't be saved. Continue?"))
        {
        return false;
        }
        else 
        {

        }
    },
    width:600,
    resizable: false
  });

});

Hope the question is clear. I just want the dialog open up, validate input and eventually execute backend code. Indeed I want it to be hidden and be dialog, instead of being just seeable ugly div on the page. Thanks

1
There’s a dialogue in the extlib. Any reason not to use that?stwissel

1 Answers

1
votes

What you are asking is not small feat and requires a good deal of JSF understanding - phase listeners, partial executions, partial refreshes, faces-config.xml, JavaScript, etc.

If you want to achieve it you have to bear a lot and keep on reading...

Phase Listener

Phase listeners are configured through faces-config.xml. The faces-config.xml will look like this:

<faces-config>
    <lifecycle>
        <phase-listener>demo.ValidationPhaseListener
        </phase-listener>
    </lifecycle>
    ...

Before getting to that we first create a Helper class in order to share a common method - and a couple of others - that will be used there and elsewhere later on:

package demo;

public enum Helper {
    ;

    private static final String ON_SUCCESS_REFRESH_ID_PARAM = "onSuccessRefreshId";

    public static void setResponseErrorHeader(FacesContext facesContext, PhaseId phaseId) {
        HttpServletResponse response = (HttpServletResponse) facesContext.getExternalContext().getResponse();

        response.setHeader("Application-Error", phaseId.toString());
    }

    public static void applyOnSuccessRefreshId(FacesContext facesContext) {
        if (!AjaxUtil.isAjaxPartialRefresh(facesContext)) {
            throw new UnsupportedOperationException();
        }

        String refreshId = (String) facesContext.getExternalContext().getRequestParameterMap().get(ON_SUCCESS_REFRESH_ID_PARAM);

        if (refreshId != null) {
            ((FacesContextEx) facesContext).setPartialRefreshId(refreshId);
        }
    }

    public static void applyOnSuccessRefreshId(FacesContext facesContext, ActionEvent event) {
        if (!AjaxUtil.isAjaxPartialRefresh(facesContext)) {
            throw new UnsupportedOperationException();
        }

        Parameter param = getComponentParam(event.getComponent(), ON_SUCCESS_REFRESH_ID_PARAM);

        if (param != null) {
            ((FacesContextEx) facesContext).setPartialRefreshId(param.getValue());
        }
    }
}

The validation phase listener class will take care of setting a response header value that we can look up in case the validation phase fails. From the framework's standpoint a validation error doesn't correspond to a 500 error, but always 200. Without this distinction we wouldn't know whether to close the dialog or not.

package demo;

public class ValidationPhaseListener implements PhaseListener {

    private static final long serialVersionUID = 1L;

    @Override
    public PhaseId getPhaseId() {
        return PhaseId.PROCESS_VALIDATIONS;
    }

    @Override
    public void beforePhase(PhaseEvent phaseEvent) {

    }

    @Override
    public void afterPhase(PhaseEvent phaseEvent) {
        FacesContext facesContext = phaseEvent.getFacesContext();

        if (facesContext.getMessages().hasNext()) {
            Helper.setResponseErrorHeader(facesContext, getPhaseId());
        }
    }

}

JavaScript helper library

To encourage re-use and good organization it's best to set up a static library that will be loaded with each XPage that makes use of dialogs. In this particular case I'm using bootstrap modal so the logic works best with that. The JS file goes like this:

var Helper = {
    isBadRequest : function(xhr) {
        return xhr.getResponseHeader("Application-Error") !== null;
    },
    jquery : function(query) {
        if (typeof query !== "string") {
            return query;
        }

        return $(query.replace(/:/g, "\\3A"));
    },
    modal : function(id, options, additionalOptions) {
        var m = this.jquery(id);
        var addOpts = additionalOptions || {};

        if ("hide" === options) {
            var successRefreshId = m.data("success-cid");

            if (addOpts.eventId && addOpts.execId && successRefreshId) {
                XSP
                        .partialRefreshPost(
                                addOpts.refreshId || null,
                                {
                                    execId : addOpts.execId,
                                    params : {
                                        "$$xspsubmitid" : addOpts.eventId,
                                        "onSuccessRefreshId" : successRefreshId
                                    },
                                    onComplete : "if (!Helper.isBadRequest(arguments[1].xhr)) { hub.modal('"
                                            + id
                                            + "', 'hide', { hide: false }) }",
                                    onError : 'console.log(arguments[0])'
                                });

                return;
            }
        } else {
            if (addOpts.successRefreshId) {
                m.data("success-cid", addOpts.successRefreshId);
            }

            var eventId = m.data("reset-eid");

            if (eventId) {
                var componentId = m.data("reset-cid");

                addOpts.hide = function() {
                    XSP.partialRefreshPost(componentId || null, {
                        immediate : true,
                        execId : eventId,
                        params : {
                            "$$xspsubmitid" : eventId
                        }
                    });
                };
            }
        }

        for (addOpt in addOpts) {
            switch (addOpt) {
            case "show":
            case "shown":
            case "hide":
            case "hidden":
                var evName = addOpt + ".bs.modal";

                if (addOpts[addOpt] instanceof Function) {
                    m.one(evName, addOpts[addOpt]);
                } else {
                    m.off(evName);
                }

                break;
            }
        }

        m.modal(options);
    }
};

I know, there's a lot going on in the modal method. It's complex in order to cover particular edge cases - at least for the way I designed it. You don't need to focus too much on it. What you want to know is that it works hand in hand with the custom control I defined as my modal template on the XPages side of things.

The modal custom control

The modal is defined through a custom control for "easy" re-use:

<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

    <xp:div
        styleClass="modal inmodal ${empty compositeData.animationClass ? 'fade' : 'in'}"
        role="dialog">
        <xp:this.attrs>
            <xp:attr name="id" value="#{compositeData.clientId}" />
            <xp:attr name="data-reset-eid" value="#{compositeData.resetEventId}"
                rendered="#{not empty compositeData.resetEventId}" />
            <xp:attr name="data-reset-cid" value="#{compositeData.resetComponentId}"
                rendered="#{not empty compositeData.resetComponentId}" />
            <xp:attr name="tabindex" value="-1" />
            <xp:attr name="aria-hidden" value="true" />
        </xp:this.attrs>
        <div class="modal-dialog ${compositeData.size}">
            <div
                class="modal-content ${empty compositeData.animationClass ? '' : compositeData.animationClass}">
                <xp:div styleClass="modal-header">
                    <xp:callback facetName="header" rendered="${compositeData.headerCustom}" />

                    <xp:text rendered="${not compositeData.headerCustom}">
                        <button type="button" class="close" data-dismiss="modal">
                            <span aria-hidden="true">&#215;</span>
                            <span class="sr-only">
                                <xp:text value="${msg.app.close}" />
                            </span>
                        </button>

                        <xp:text tagName="i"
                            styleClass="fa #{compositeData.headerIcon} modal-icon" rendered="#{not empty compositeData.headerIcon}" />

                        <xp:panel tagName="h4" styleClass="modal-title"
                            rendered="#{not empty compositeData.headerTitle}">
                            <xp:text value="#{compositeData.headerTitle}" />
                        </xp:panel>

                        <xp:panel tagName="small" styleClass="font-bold"
                            rendered="#{not empty compositeData.headerDescription}">
                            <xp:text value="#{compositeData.headerDescription}" />
                        </xp:panel>
                    </xp:text>
                </xp:div>

                <div class="modal-body">
                    <xp:callback facetName="body" />
                </div>

                <xp:div styleClass="modal-footer">
                    <xp:callback facetName="footer" />
                </xp:div>
            </div>
        </div>
    </xp:div>

</xp:view>

Its .xsp-config definition:

<?xml version="1.0" encoding="UTF-8"?>
<faces-config>
    <faces-config-extension>
        <namespace-uri>http://www.ibm.com/xsp/custom</namespace-uri>
        <default-prefix>xc</default-prefix>
    </faces-config-extension>
    <composite-component>
        <component-type>layoutModal</component-type>
        <composite-name>layoutModal</composite-name>
        <composite-file>/layoutModal.xsp</composite-file>
        <composite-extension>
            <designer-extension>
                <in-palette>true</in-palette>
            </designer-extension>
        </composite-extension>
        <property>
            <property-name>clientId</property-name>
            <property-class>string</property-class>
            <property-extension>
                <required>true</required>
            </property-extension>
        </property>
        <property>
            <property-name>size</property-name>
            <property-class>string</property-class>
            <property-extension>
                <designer-extension>
                    <editor>com.ibm.workplace.designer.property.editors.comboParameterEditor</editor>
                    <editor-parameter>modal-sm&#xd;
modal-lg</editor-parameter>
                </designer-extension>
            </property-extension>
        </property>
        <property>
            <property-name>animationClass</property-name>
            <property-class>string</property-class>
        </property>
        <property>
            <property-name>headerIcon</property-name>
            <property-class>string</property-class>
        </property>
        <property>
            <property-name>headerTitle</property-name>
            <property-class>string</property-class>
        </property>
        <property>
            <property-name>headerDescription</property-name>
            <property-class>string</property-class>
        </property>
        <property>
            <property-name>headerCustom</property-name>
            <property-class>boolean</property-class>
        </property>
        <property>
            <property-name>resetEventId</property-name>
            <property-class>string</property-class>
        </property>
        <property>
            <property-name>resetComponentId</property-name>
            <property-class>string</property-class>
        </property>
    </composite-component>
</faces-config>

Apart from a bunch of properties, what it defines is a series of patterns that you want to use depending on the complexity of the modal. If you are able to spot it the id I define for a modal is of the fixed kind - and not dynamic. It's not an encouragement to use fixed over dynamic but by evaluating all cases I realized that the fixed id helps a lot in, again, some edge cases.

The link and the modal

It's time to put things together by using the custom control on the page. Let's start with the link.

We use the link to "prepare" the modal. First, it's important to work with surgical precision in order to be both efficient and have less to worry about when working with modals. JavaScript happens on the client side and, unless status gets synced with the server side in some way, the server side doesn't know that. This is important to understand because unless specifically told an event handler would perform a complete refresh of the page as consequence of an action - that means that the whole page gets rebuild like it was the first time - at least from the JavaScript standpoint, thus losing track of the open modal.

So, whenever you want to work with a modal, and you need to prepare its content, it's important that partial execution and partial refresh get used. Actually if you never use partial refresh and execution you should seriously ask yourself why.

<xp:link id="linkOpenDialog">
    <xp:eventHandler event="onclick" submit="true"
        execMode="partial" refreshMode="partial" refreshId="modalDemoBody"
        action="#{myBean.myAction}"
        onComplete="Helper.modal('#modal-account', { backdrop: 'static', keyboard: false })" />
</xp:link>

If you look at the example above what I set up reads 1) execMode="partial" when executing the action ignore every other evaluation on the page, 2) refreshMode="partial" refreshId="modalDemoBody" when the action is carried out the only elements that will be updated on the page is not the whole page but the modalDemoBody and all of its children (that is the modal body content), 3) action="#{myBean.myAction}" my methdod would likely prepare my modal before display, 4) onComplete="Helper.modal('#modal-account'...)" the JavaScript method that will be invoked - it's in the static library, remember? - that will actually fire the modal. If you don't need to prepare the modal you can skip all of these steps and just put move the method in onComplete to the onclick event of the link (eg. <xp:link id="linkOpenDialog" onclick="Helper...")

Now the code for the modal on the page. We use the custom component and we define its dynamic id, that I actually won't use, and its clientId, the one that we referenced in the link. There are other optional properties that you can define like, the header title - or you could completely revisit the whole area by specifying a custom facet, and, in the case of the example the resetEventId (this one call for the eventHandler at the bottom and it's a helper to clear the state of the form in cases where, for example, 1) you opened the modal, 2) you submitted the content, 3) validation failed, 4) you closed the modal thus leaving the form on the server side in an inconsistent state that could bite you depending on what action you will click on the page next. (this action will be fired only if the modal is not closed by the save action)

Then there's a body facet - xp:key="body" - and a footer - xp:key="footer" - to define your buttons.

<xc:layoutModal id="modalDemo" clientId="modal-demo"
    headerTitle="Demo" resetEventId="#{id:eventResetForm}">
    <xp:this.facets>
        <xp:panel xp:key="body" binding="#{modalDemoBody}" id="modalDemoBody">

            <!-- Here goes your content -->

        </xp:panel>
        <xp:text xp:key="footer" disableOutputTag="true">
            <button type="button" class="btn btn-white"
                data-dismiss="modal">
                <xp:text value="Close" />
            </button>
            <xp:button id="buttonSaveModal" value="Save" styleClass="btn-primary">
                <xp:eventHandler event="onclick" submit="true"
                    execMode="partial" execId="modalDemo" refreshMode="partial"
                    refreshId="modalDemoBody" actionListener="#{myBean.saveModalDemo}"
                    onComplete="if (!Helper.isBadRequest(arguments[1].xhr)) { hub.modal('#modal-demo', 'hide', { hide: false }) }">
                    <xp:this.parameters>
                        <xp:parameter name="onSuccessRefreshId"
                            value="#{id:allIsWell}" />
                    </xp:this.parameters>
                </xp:eventHandler>
            </xp:button>

            <xp:eventHandler id="eventResetForm"
                submit="false"
                action="#{javascript:myBean.resetForm(modalDemoBody)}" />
        </xp:text>
    </xp:this.facets>
</xc:layoutModal>

<xp:div id="allIsWell">

</xp:div>

In the save button event handler we carefully choose the behaviour. Again we use partial execution and refresh. Partial execution set with execMode="partial" execId="modalDemo", that means that validation will happen only within the modal portion of the page. Partial refresh set with refreshMode="partial" refreshId="modalDemoBody", because we don't want to reload the whole page trashing the modal state but it's specific enough to return the form with possible validation errors if the occurred.

Now, instead of the action param I actually exploited the actionListener one because it allows me to send in with the request additional parameters (I think it could be done differently anyway, by using the javascript signature for example and then passing the parameter there). What I define here is a conditional behaviour for the id I want to be refreshed as a consequence of the action, that is, if validation fails refresh the body of the modal, but if it succeeds, refresh some other component of the page (in the example it's <xp:div id="allIsWell">). <xp:parameter name="onSuccessRefreshId", its name, matches the static string ON_SUCCESS_REFRESH_ID_PARAM in the Helper class. Now, what would happen in the action is to change the refresh id on the fly if our method also evaluates without errors. If not we will also invalidate the action in a way that will be understandable for JavaScript later on.

public void saveModalDemo(ActionEvent event) {
    // Your logic goes here
    boolean allIsWell = false;

    if (allIsWell) {
        Helper.applyOnSuccessRefreshId(facesContext, event);
    } else {
        Helper.setResponseErrorHeader(facesContext, event.getPhaseId());
    }
}

Helper.applyOnSuccessRefreshId will read the onSuccessRefreshId param and re-route the original refreshId defined in the eventHandler with the new one. There you go, you have a conditional refresh!

Then, and only then, we take care of closing the dialog by using the onComplete="if (!Helper.isBadRequest(arguments[1].xhr)) { hub.modal('#modal-demo', 'hide', { hide: false }) }" defined parameter, which makes sure of one thing, whether it can actually close the dialog or not. If it finds the response header that says there's an error - like in the case of validation error - it won't close the dialog, otherwise it will.

I wrote more code than explanation. I should have actually wrote a blog post about it. Anyway you might get some more insight regarding the process by checking this link out. However the answer here is an improvement upon that first attempt at solving the problem.