Update 7/3/2012
Primefaces p:ajax partial refreshes can also be blocked by returning false to the onclick event. In this case, an If statement needs to be used, so that “return false” is done to block the refresh, but no return at all is done to allow the refresh. This allows the Primefaces ppr logic to execute – returning true explicitly in the onclick actually stops the ppr/ajax logic from executing at all:-
onclick="if (!confirmAction(Arguments)) {return false};"
The nice feature of this method is that it works in an identical fashion for both full page and partial refreshes, so that is is not necessary to mess with logic which couples both the onstart and onclick attributes based on the ajax setting.
The downside is that as before we are adding bare Javascript logic into an element attribute. In defence, this situation has already been forced on us by the JSF/Primefaces design – the interface already requires us to return false to block a request, so whilst this might be distasteful from a purist Javascript point of view, it is a fact of life we have to live with.
Original Post
Confirmation dialogs are typically used when for example a form is being edited and a navigation to another page is attempted.
Traditionally, this might be done by using the Javascript confirm function which pops a browser dialog, and returns true or false depending on the user choice. However, modern web apps shy away from using browser popup dialogs. Their styling is browser dependant and they cannot be restyled. A modern app will use custom styled dialogs with features like fade in/out and lightboxing to give a more subtle appearance and to allow the dialog to be displayed in-page to avoid popup blocking problems. Also, component libraries such as Primefaces allow custom themes and theme switching for an application, which would therefore allow their own custom dialogs to be dynamically restyled.
The fundamental complication when using such a custom dialog is that unlike the built-in Javascript confirm function, it does not operate synchronously. This immediately raises major issues when using it to confirm navigation, as it cannot be used inline in a navigation confirmation event on a button or link. This post looks at how to get around this issue and use a Primefaces p:dialog to implement a confirmation dialog.
Allowing or blocking navigation attempts
The first point to address is how we can actually allow or block a navigation attempt on a Primefaces/JSF button or link. There are two options available, which are straightforward and applicable to all buttons and links:-
- For full page refreshes, returning false from the onclick event aborts the navigation. This is a standard JSF feature which is described here. It is also mentioned in Core Javaserver Faces Edition III on p573, although it must be said that it is rather hidden near the back of the book and hard to dig out.
- For Primefaces p:ajax partial refreshes, returning false from the onstart event aborts the ajax call.
Note that in both cases, it is important to prefix the function call with the return statement, otherwise the function return value is not correctly returned by the event call. For example, using the traditional Javascript confirm function:-
- onclick=”return confirm(‘Pending edits will be cancelled, do you want to continue?’);”
- onstart=”return confirm(‘Pending edits will be cancelled, do you want to continue?’);”
When applying a default case, it is also possible to just return true or false without calling anything, e.g. using onclick=”return true;”
Using the p:dialog to allow/block the navigation
This is a little tricky to get right, but actually turns out to be fairly straightforward, and in particular, we can still initiate the whole process from a single function call in the onclick or onstart event as above. The idea makes use of the fact that we can issue a soft click on an HTML (or in this case jQuery) element using the .click() call. The steps are as follows :-
- The user clicks on a button, which causes our own Javascript confirm(elementId) function to be called by say the onstart event. Importantly, the clientId of the target element which was clicked is passed and stored, as it may be needed later.
- if edit mode is inactive, the function just returns true to allow the action.
- If edit mode is active, then the function sets an internal confirmInProgress flag and shows the confirmation p:dialog via its widget show() function. The function then returns false to (initially) block the Ajax action.
- When the dialog is displayed and the user clicks No, a Javascript cancelAction() function is called which hides the dialog and clears the confirmInProgress flag. The Ajax action is not then performed.
- When the dialog is displayed and the user clicks Yes, a Javascript performAction() function is called which hides the dialog and forces a second (software) click on the button, using the previously stored elementId. This results in confirm() being called a second time.
- When confirm() is called the second time via the software click (because the user clicked yes to confirm), we can detect this as the confirmInProgress flag will be set. The function then clears the confirmInProgress flag and returns true to allow the Ajax action to proceed.
Implementation Points
- When implementing this, my confirmation dialog is a facelets custom tag. Within the tag, I create a Javascript object (whose precise name is passed by the caller) which contains the above logic and manages the confirmation process.
- The Javascript object is created via the new keyword – see my other post here with links on this topic.
- I then pass the confirm function call used on this object as a text attribute value to any tags, buttons or links that need to implement confirmation. For a high level tag I would pass it as an onNavigate attribute, and within the tag it would be passed on to any links or buttons to which it applies.
- In order to do the software click, the clientId of the element that was clicked is passed to the confirm function and stored on the Javascript object. To do this, the confirm function call is passed as a MessageFormat style string with a placeholder which will take the clientId, as this must be added by the target button typically via a reference to #{component.clientId}. In this way the final target could add additional parameters if required, but is nicely decoupled from the actual Javascript call. The only interface contract is the placeholder parameter index, and the fact that a boolean must be returned to permit/deny navigation. I typically add MessageFormat.format (in addition to string concatenation) as custom EL java functions declared in a tag library – in the example below, these functions have the el: prefix.
- In our case, the actual element clientId is a constant string as far as the Javascript call is concerned, so it must have single quotes added. These are added in the context of the target element – they are not passed in the MessageFormat string. This way, the caller does not decide whether or not a parameter is a Javascript String – this is decided in the target element environment. For example, if an additional argument such as a JavaScript reference such as this was passed in the target element environment, it would not need to be quoted, and the caller’s MessageFormat string containing the Javascript call would not have to know. This maintains good decoupling.
- The Javascript object also has an internal confirmMode property which indicates whether confirmation mode is actually on, i.e. if edit mode is active. If not, it just permits everything as per the above logic steps. This JS property would normally be set from an edit mode bean property on the actual edit form which triggers the confirm. To do this, I pass the JS property reference in to the tag handling the edit form. and it calls a standard utility tag to issue an inline script statement to assign the property reference from the bean. This assignment needs to be within a div which is updated by ajax when the edit mode changes, to ensure the property is reassigned.
Example Code Fragments
Facelets Page declaring the Confirmation Dialog
<util2:confirmActionDialog tagId="confirmDialog" widgetVar="confirmWidget" />
<cheep:campaignTreeBrowser id="campaignTreeBrowser" idPath="#{form}" currentNodeChange="#{form}:pnlCampaignDetail"
currentNodeChangeOnComplete="ss_PanelExpand(widgetVarPnlCampaignDetail)"
controller="#{campaignsPage.campaignTreeBrowser}" edit="#{true}"
onNavigate="confirmWidget.confirm({0})"/>
<cheep:campaignDetailPanel tagId="pnlCampaignDetail" idPath="#{form}" editModeFlag="confirmWidget.confirmMode"
controller="#{campaignsPage.campaignDetailPanel}"/>
confirmActionDialog.xhtml custom tag
<!DOCTYPE HTML>
<html xmlns="http://www.w3c.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:p="http://primefaces.prime.com.tr/ui"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:fn="http://java.sun.com/jsp/jstl/functions"
xmlns:util="http://java.sun.com/jsf/composite/uk.co.salientsoft/util">
<ui:composition>
<h:outputScript library="uk.co.salientsoft/util" name="confirmActionDialog.js" target="head"/>
<script type="text/javascript">#{widgetVar} = new uk.co.salientsoft.ConfirmActionDialog();</script>
<p:confirmDialog id="#{tagId}" widgetVar="#{widgetVar}.dialogWidget" styleClass="ss-confirmaction-dialog"
message="#{empty confirmMessage ? mainMsg.confirmEditCancel : confirmMessage}"
showEffect="fade" hideEffect="fade"
header="#{empty confirmTitle ? mainMsg.confirmEditCancelTitle : confirmTitle}" severity="alert">
<util:iconTextButton id="cmdAbort" image="ui-icon ui-icon-close"
label="#{mainMsg.optionNo}" title="#{mainMsg.optionTitleNo}" onclick="#{widgetVar}.cancelAction()" />
<util:iconTextButton id="cmdConfirm" image="ui-icon ui-icon-check"
label="#{mainMsg.optionYes}" title="#{mainMsg.optionTitleYes}" onclick="#{widgetVar}.performAction()" />
</p:confirmDialog>
</ui:composition>
</html>
Setting the EditMode flag in the campaignDetailPanel edit form
<!– Set the edit mode state in the specified Javascript flag used by the navigation confirmation dialog when in edit mode –>
<util2:setScriptVar name="#{editModeFlag}" value="#{controller.editMode}" defaultValue="false" />
Custom tag util2:setScriptVar
<!DOCTYPE HTML>
<html xmlns="http://www.w3c.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:el="http://salientsoft.co.uk/EL">
<ui:composition>
<!– Set the specified Javascript variable/property from the given EL expression –>
<c:if test="#{not empty name}">
<script type="text/javascript">#{name} = #{empty value ? defaultValue : value};</script>
</c:if>
</ui:composition>
</html>
Code within a link which adds the actual onstart event (this example is taken from a composite component)
<p:commandLink id="link" ajax="#{cc.attrs.ajax}" async="#{cc.attrs.async}" disabled="#{cc.attrs.disabled}"
action="#{cc.attrs.controller.iconButtonAction(cc.attrs.eventHandler, cc.attrs.actionMethod,
cc.attrs.actionParam, cc.attrs.passController, cc.attrs.passActionParam)}"
actionListener="#{cc.attrs.controller.iconButtonActionListener}"
process="#{cc.attrs.process}" update="#{cc.attrs.update}" immediate="#{cc.attrs.immediate}"
href="#{cc.attrs.href}" title="#{cc.attrs.title}" tabindex="#{cc.attrs.tabIndex}"
onstart="return #{empty cc.attrs.onNavigate ? ‘true’ : el:format1(cc.attrs.onNavigate, el:concat3(‘\”, component.clientId, ‘\”))}"
oncomplete="#{cc.attrs.oncomplete}" onsuccess="#{cc.attrs.onsuccess}" onerror="#{cc.attrs.onerror}">
…
</p:commandLink>
Javascript object ConfirmActionDialog.js
/*
* This object handles the logic for confirming Primefaces Ajax actions via a confirmation dialog.
* It is typically used to pop a confirmation when in edit mode on a form and e.g. a navigation link/button has been clicked.
* A navigation link/button should call confirm() in its onstart Ajax event. The following then happens:-
*
* 1/ if EditMode is inactive, confirm() just returns true to allow the action.
* 2/ If EditMode is active, then confirm() pops the confirm dialog and returns false to (initially) block the Ajax action.
* 3/ When the dialog is popped and the user clicks No, hide the dialog and return to the idle state (Ajax action not performed).
* 4/ When the dialog is popped and the user clicks Yes, we hide the dialog and force a software click on the Ajax link/button.
* 5/ When confirm() is called the second time via the software click (because the user clicked yes to confirm),
* return to the idle state and return true to allow the Ajax action.
*/
var uk=uk||{}; uk.co=uk.co||{}; uk.co.salientsoft=uk.co.salientsoft||{};
uk.co.salientsoft.ConfirmActionDialog = function () {
this.confirmInProgress = false; //initialise to the idle state
this.confirmMode = false; //set no confirmation needed initially (e.g. not in edit mode)
};
uk.co.salientsoft.ConfirmActionDialog.prototype.show = function() {
this.dialogWidget.show();
};
uk.co.salientsoft.ConfirmActionDialog.prototype.hide = function() {
this.dialogWidget.hide();
};
uk.co.salientsoft.ConfirmActionDialog.prototype.confirm = function(elementId) {
if (this.confirmMode && !this.confirmInProgress) {
var escapedId = ‘#’ + elementId.replace(/:/g, ‘\\3A ‘);
this.element = jQuery(escapedId);
this.dialogWidget.show();
this.confirmInProgress = true;
return false; //first time in, cancel the Primefaces Ajax action until confirmed
}
else {
/*
* Either we are not in confirm mode (e.g. edit mode is off),
* or we are in confirm mode and this is the second time in, called as a result of a confirm.
* Either way we allow the Primefaces Ajax action, and return to the idle state.
*/
this.confirmInProgress = false;
return true;
}
};
uk.co.salientsoft.ConfirmActionDialog.prototype.performAction = function() {
this.dialogWidget.hide();
this.element.click(); //This will cause confirm() to be called again; this time the Ajax action will be allowed.
};
uk.co.salientsoft.ConfirmActionDialog.prototype.cancelAction = function() {
this.dialogWidget.hide();
this.confirmInProgress = false; //we will not be performing the Ajax action, so return to the idle state
};