Sunday, May 8, 2011

Prevent multiple clicks in JSF

Preventation of multiple clicks on buttons / links is a common and important task during web development. There are many discussions about double click problem in JSF I will try to cover all relevant approaches.

1) Server-side solution.
Server-side solutions are normally based on token concept, phase listeners, filters or other complex tricks. Here is just one example for the server-side preventation of double-form submit Server-side solutions have disadvantages. Every request is handled by a special phase listener, filter or something near it, even if we are not interesting in multiple clicks preventation. There are problems in a cluster environment if session data get replicated. This a real problem I faced many years ago - our server-side solution didn't work proper if the web application was running in a cluster environment. Client-side solutions seems to be simpler and are more reliable.

2) Disable button / link on click.
The simplest client-side solution consists in writing this.disabled=true in onclick. An example:
 
<h:commandbutton action="..." onclick="this.disabled=true;" value="...">
 
I had kinda troubles with this solution and ajaxified buttons like PrimeFaces' p:commandButton - it didn't work. Furthermore the entire button gets frozen and doesn't respond anymore. We want to have a still responding button or link if user clicks on it multiple times.

3) Ajax queue and request delaying.
This is probably the best solution at all. Many popular component libraries already have this solution for the multiple clicks preventation. OpenFaces has e.g. a nice concept in order to avoid frequent requests. OpenFaces' Ajax framework sends only one request in case of several events during the short period of time specified with the "delay" attribute. RichFaces has a4j:queue and a4j:attachQueue. These components have an attribute "requestDelay". This attribute specifies the number of milliseconds to wait before sending a request in order to combine similar requests. The greater the value the fewer requests will be sent when fast typing. Similar requests in the queue are "combined" while waiting for the request delay. This feature results in just one request from a component being sent. For more details see RichFaces showcase and try to do double clicks to see they are "combined" to just one. Unfortunately, but PrimeFaces doesn't have similar feature what would prevent sending of miltiple requests from the same component like buttons / links, datatable filtering on keyup if user types very fast (important for large tables), etc.

4) Using jQuery's bind / unbind methods.
Ajax queue prevents double clicks, but doesn't ensure sending of slow multiple clicks. I have implemented my own solution for PrimeFaces' buttons / links. The idea is to bind onlick handler bind('click', function() {..}) and call unbind('click') within the handler :-) I have also tried jQuery .one() method but it didn't work as desired. We should prepare the renderer class at first. I have these lines there (only short outline for button renderer to get the idea):
writer.write(button.resolveWidgetVar() + 
    " = new PrimeFaces.widget.ExtendedCommandButton('" + clientId + "', {");
...
writer.write("});");
if (button.isPreventMultiplyClicks()) {
    writer.write(button.resolveWidgetVar() + ".bindOneClick('" + clientId + "', function(){" + onclick + "});");
}
ExtendedCommandButton is an extended widget based on the PrimeFaces' one. bindOneClick is a method in this widget and onclick is an onlick handler generated by PrimeFaces for Ajax / non Ajax requests. The extension looks as follows:
PrimeFaces.widget.ExtendedCommandButton = function(id, cfg) {
    PrimeFaces.widget.CommandButton(id, cfg);
}

jQuery.extend(PrimeFaces.widget.ExtendedCommandButton.prototype, PrimeFaces.widget.CommandButton.prototype);

PrimeFaces.widget.ExtendedCommandButton.prototype.bindOneClick = function(clientId, commandFunc) {
    var buttonEl = jQuery(PrimeFaces.escapeClientId(clientId));
    buttonEl.data('commandFunc', commandFunc);
    buttonEl.unbind('click');

    buttonEl.bind('click', function() {
        buttonEl.unbind('click');
        commandFunc();
    });
}
One problem still exists - if user stays on the same page (in case of Ajax updates) and the button is not updated, it doesn't respond more by reason of unbind. We should "reinitialize" it. Therefore I added this method too:
PrimeFaces.widget.ExtendedCommandButton.prototype.reinitialize = function(clientId) {
    var buttonEl = jQuery(PrimeFaces.escapeClientId(clientId));
    buttonEl.unbind('click');

    buttonEl.bind('click', function() {
        buttonEl.unbind('click');
        buttonEl.data('commandFunc')();
    });
}
So, if we navigate to another page (a non-Ajax request), we can write now:
 
<p:commandButton preventMultiplyClicks="true" ajax="false" ...>
 
If user stays on the same page (an Ajax request), a call PrimeFaces.widget.ExtendedCommandButton.reinitialize(clientId) is necessary in "oncomplete" to get multi-click preventation working again. Like this
 
<p:commandButton id="myButton" preventMultiplyClicks="true" ...
                oncomplete="PrimeFaces.widget.ExtendedCommandLink.reinitialize('myButton')">
 
Or you can try to include the button Id to the "update" attribute
 
<p:commandButton preventMultiplyClicks="true" update="@this,..." ...>
 
p:commandLink will be updated and reinitialized again. We can also extend the button renderer a little bit and reinitialize it automatically (if it's not in the Ajax update region). Result: the button / link will be a quite usually PrimeFaces' button / link with a new attribute for multiple clicks. No differences otherwise.
 
<p:commandLink "general PF attributes" preventMultiplyClicks="true">
 

3 comments:

  1. Why not? It works.

    ReplyDelete
  2. (p:commandButton ... onclick="this.disabled=true;" oncomplete="this.disabled=false;" /)

    It works.

    ReplyDelete
  3. How renderer part should be done with newer primefaces. For example 5.1.

    ReplyDelete

Note: Only a member of this blog may post a comment.