I might call this the negative side of perfectionism: wanting everything to be as efficient as possible even if it doesn’t matter. Oh well, I’m stubborn sometimes.
I took it as a personal challenge to be able to open a bootstrap modal window and…
- on close partially refresh the modal body in case an error occurred in order to show it
- on save partially refresh a different block and close the modal in case of no raised error
All of the above without wasting more than a server trip.
The Issues
Having to deal with the current IBM XPages framework limitations the challenge presents three distinct issues:
- there’s no out-of-the-box way to let the client side know whether any
ValidationException
was raised - same is true, say, if you want to add any
FacesMessage
error during an event handler action evaluation - there’s no out-of-the-box way to choose what block id to refresh as a consequence of the evaluation of point 1 and 2.
The fact is that points 1 and 2 deal with recoverable error situations. They are not throwing haltering exceptions and therefore they don’t trigger an eventual event handler onError
JavaScript method. So, errors can be raised but the framework can’t be bothered with, for it the ajax request has completed just fine and therefore only an eventual event handler onComplete
JavaScript method is invoked.
The Workaround
But there is something to hack. Each ajax request comes with a dojo.xhr object: request is performed and response is returned, it’s the same JavaScript object that will be worked on by the framework. However there’s nothing you can do with the response data itself; it’s kind of “sealed”. You can’t tamper with it in order to add some sort of flag in case of error somewhere. But fortunately enough response headers are totally hackable. So, the first puzzle piece is to create a method that will write a custom response header to return with the ajax response in case of induced error.
I have a non-instantiable class (enum) with static methods I keep around for things like this. So the method looks like the following:
public enum Helper {
;
...
public static void setErrorHeader(HttpServletResponse response, PhaseId phaseId) {
response.setHeader("Application-Error", phaseId.toString());
}
...
The method is just a personal interpretation of what header name and value I want to see. Application-Error
will be the header looked up by the JavaScript code to determine whether any soft-error occurred. PhaseId
for when it occurred; the second parameter choice is actually either overkill or too generic. Anyway, you can tweak the method parameter to be whatever thing you want it to be.
However, in case of validation exceptions in order to set the header I can’t rely on an eventual handler action: the application doesn’t get to it, so I don’t have any chance to set it from there. In order to fix this shortcoming I am going to write a small PhaseListener
class.
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.setErrorHeader((HttpServletResponse) facesContext.getExternalContext().getResponse(), getPhaseId());
}
}
}
In the after phase I check for the presence of any FacesMessage
. If any is found I set the error header.
Similarly, if an error is to be shown to the user as a consequence of the action method evaluation I will set the header from there. However, in the action method I’m going to do a little more than that. As I said before I want to refresh a different block in case the action performs successfully. All I need to know is which id to refresh; yes, I could hard code it in the method but I don’t like to surrender that to the method and lose flexibility. But first things first: I create an additional method – applySuccessRefreshId
– to my static class to quickly change the refresh id on the action side of things.
public enum Helper {
;
...
@SuppressWarnings("unchecked")
public static Map<String, String> getParameterScope(FacesContext facesContext) {
return (Map<String, String>) facesContext.getExternalContext().getRequestParameterMap();
}
public static void applySuccessRefreshId(FacesContext facesContext) {
if (!AjaxUtil.isAjaxPartialRefresh(facesContext)) {
throw new UnsupportedOperationException();
}
String refreshId = getParameterScope(facesContext).get("onSuccessId");
if (refreshId != null) {
((FacesContextEx) facesContext).setPartialRefreshId(refreshId);
}
}
...
What happens here are 2 things: with the first I make sure the method can be called only during an ajax call otherwise an exception will be thrown and with the second I look for the optional arbitrarily named onSuccessId
parameter that contains the block id to refresh in case of clear success.
Now, a hypothetical action should look like this:
public String myAction() {
// Business logic
FacesContext facesContext = FacesContext.getCurrentInstance();
if (iLikeWhatISee) {
Helper.applySuccessRefreshId(facesContext);
} else {
HttpServletResponse response = (HttpServletResponse) facesContext.getExternalContext().getResponse();
Helper.setErrorHeader(response, PhaseId.INVOKE_APPLICATION);
}
return null;
}
Now I need to stich up server side and client side. How do I do that? How do I even send the onSuccessId
parameter previously mentioned? By taking advantage of the XSP.partialRefreshPost
JavaScript method.
Again, a hypothetical modal save button should look like this:
<xp:button id="buttonModalSave" value="Save" styleClass="btn-primary">
<xp:eventHandler
id="myEventHandlerId"
event="onclick"
submit="false"
action="#{ctrl.myAction}"
script="XSP.partialRefreshPost('#{id:myModalBody}', {
execId: '#{id:whateverIdIKnowItsRight}',
params: {
'$$xspsubmitid' : '#{id:myEventHandlerId}',
'onSuccessId' : '#{id:whateverIdIKnowItsRight}'
},
onComplete : 'if (!isBadRequest(arguments[1].xhr)) { logicToCloseTheDialog() }',
onError : 'console.log(arguments[0])'
})"/>
</xp:button>
Points of attention:
- The
eventHandler
is given an id: in this casemyEventHandlerId
. That very same id is sent with theXSP.partialRefreshPost
$$xspsubmitid
parameter. It’s important because the request will know it will have to invoke the boundaction
method. '#{id:myModalBody}'
refers to the block id that will be refreshed once the response is received'onSuccessId' : '#{id:whateverIdIKnowItsRight}'
should contain the actual id you want to refresh in case the action has performed successfullyonComplete
contains the JavaScript method that will be called at the end. The method must be defined as string. That’s because it will be evaluated and inject two arguments: [0] the new HTML content that will be placed in the block id, [1] the dojo.xhr object that took care of everything which I’m greatly interested in for further inspection.
The onComplete
is where I will close the dialog. Granted, no soft-error must have occurred. The last missing piece is actually what the isBadRequest
does:
function isBadRequest(xhr) {
return xhr.getResponseHeader("Application-Error") !== null;
}
So, if there’s no error header the code goes ahead with closing the modal, otherwise it will stay open.
I put together a small video to show it’s working and achieving the goal with a single server trip:
It’s a very interesting way of handling errors. I would use this method if there’s a need for server-side error checking. However, for client-side error checks (like the one you show in the video: password matching), I would use client side error check without any trip to the server.
Granted on the client side validation. Actually it’s kind of hypocritical to want to save server trips and then do form validation server side. What generally holds me back is that, although remotely possible, client side validation can be fooled and therefore server side validation must be enforced anyway. At that point I’m asking myself: why do double work? But I know it’s just a weak and lazy excuse.