Monday, December 3, 2012

Preventing double clicks / duplicate submits on JSF h:commandButtons

For some time now I was getting sparse exception reports which all happened after form submits. Of course, the users did never do anything unusual (or so they told me) and I kept struggling finding the cause for the exceptions. Being frustrated after another report today, I started randomly opening and submitting some forms until I accidentally clicked twice on the submit buttons before the request was completed and... BAM! There it was.

In most cases, I have three kinds of submit buttons on my forms: submit, submit and return (to previous view) and cancel and return. The latter both call an action which ends the current conversation and redirects to the last view of the previous one, which also invalidates the current bean containing the actions. So when first clicking one of those terminating buttons, a second click would cause an action on a bean/conversation which was invalidated just before.

The obvious solution is to prevent duplicate form submissions, which is more complicated than thought in JSF. Seam provides the s:token-tag which should also be able to prevent multiple submissions but it seems to be bugged at the moment and only produced NPEs after double clicks (see https://issues.jboss.org/browse/JBSEAM-4717).

Some other sources suggested disabling the buttons via JavaScript using the onclick event. This, of course, is client side code and not that robust than s:token should be, but in my application's case JavaScript is mandatory and expected to be enabled. Most suggestions, though, only contained onclick="this.disabled = true;" which firstly only works for one button and secondly does not work at all because disabling the button also prevents the form from being submitted. Others noted that adding a delayed execution using JS-timers might help, which, to make it short, is as crappy and fragile as it sounds.

So I came up with my one variant of this solution. I introduced a global boolean variable (yeah, hate me and feel free to add some form based scope) which is toggled after the first submit button is clicked. All subsequent click events on the submit buttons are caught and stopped using event.preventDefault(); which seems to work fine for now. Notably, form submission still works if the user indeed has JavaScript disabled - of course multiple times then.

<script>
var formSubmitted = false;
function onSubmitButton() { 
    if (!formSubmitted) {
        formSubmitted = true;
    } else {
        // disable event propagation if form is already submitted
        event.preventDefault();
    }
}
</scrip>
<h:commandButton action="#{bean.store}" value="#{msg.store}" onclick="onSubmitButton();"/>
<h:commandButton action="#{bean.storeAndRedirect}" value="#{msg.store_and_back}" onclick="onSubmitButton();"/>
<h:commandButton action="#{bean.endAndRedirect}" value="#{msg.cancel}" immediate="true" onclick="onSubmitButton();"/>