
El caso del doble o N-clic puede ser un problema, imagina una aplicación que borra un registro de la base de datos o una aplicación que realiza el cobro de una tarjeta de crédito. Si el usuario tienen la oportunidad de hacer doble clic en el botón o enlace enviándo dos peticiones al servidor lo más probable es que solo una de ellas funcione, la segunda petición puede producir una excepción en el servidor o peor aún dependiendo de la acción un efecto indeseado. ¿Eres consciente y tienes en cuenta que puede pasar en tu aplicación si se da esta situación?
Una de las formas de evitar este problema en cierta medida es desactivando el botón o enlace una vez que se ha pulsado y el formulario se envía o se realiza la petición. Podemos hacer uso de un código javascript que en el momento de pulsar el botón o enlace impida que desencadene su acción por segunda vez, por ejemplo desactivándolo.
En Tapestry podemos usar un mixin para reutilizar este código en todos los sitios que necesitemos de una forma muy sencilla. En el contexto de Tapestry un mixin es un componente que no se usa de forma individual sino que se usa junto a otro componente y que añade cierta funcionalidad al componente asociado. En la aplicación de demostración JumpStart se puede ver el código del componente mixin ClickOnce que en ciertos escenarios puede evitar el problema del N-clic. Sin embargo, la solución del mixin ClickOnce es válido solo para peticiones no ajax ya que una vez que el botón o enlace es pulsado se desactiva completamente, en una petición ajax el botón debería volverse a activar una vez que la petición Ajax termina.
A continuación mostraré el código de un mixin que mejora el ClickOnce resolviendo el problema N-clic tanto para peticiones ajax como no ajax. Por una parte tendremos el código Java asociado al mixin.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package es.com.blogspot.elblogdepicodev.plugintapestry.mixins; | |
import org.apache.tapestry5.ClientElement; | |
import org.apache.tapestry5.ComponentResources; | |
import org.apache.tapestry5.annotations.InjectContainer; | |
import org.apache.tapestry5.ioc.annotations.Inject; | |
import org.apache.tapestry5.json.JSONObject; | |
import org.apache.tapestry5.services.javascript.JavaScriptSupport; | |
public class SubmitOne { | |
@Inject | |
private JavaScriptSupport support; | |
@InjectContainer | |
private ClientElement element; | |
@Inject | |
private ComponentResources resources; | |
public void afterRender() { | |
JSONObject spec = new JSONObject(); | |
spec.put("elementId", element.getClientId()); | |
support.require("app/submitOne").invoke("init").with(spec); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
define("app/submitOne", ["jquery"], function($) { | |
var SubmitOne = function(spec) { | |
this.spec = spec; | |
this.timeout = null; | |
var element = $('#' + this.spec.elementId); | |
this.blocked = false; | |
var _this = this; | |
$(document).ajaxStart(function() { | |
_this.onAjaxStart(); | |
}); | |
$(document).ajaxStop(function() { | |
_this.onAjaxStop(); | |
}); | |
if (element.is('form')) { | |
element.on('submit', function(event) { | |
return _this.onSubmit(event); | |
}); | |
} else { | |
element.on('click', function(event) { | |
return _this.onSubmit(event); | |
}); | |
} | |
} | |
SubmitOne.prototype.onSubmit = function(event) { | |
if (this.isBlocked()) { | |
event.preventDefault(); | |
return false; | |
} else { | |
this.block(); | |
return true; | |
} | |
} | |
SubmitOne.prototype.onAjaxStart = function() { | |
this.block(); | |
} | |
SubmitOne.prototype.onAjaxStop = function() { | |
this.unblock(); | |
} | |
SubmitOne.prototype.isBlocked = function() { | |
return this.blocked; | |
} | |
SubmitOne.prototype.block = function() { | |
this.blocked = true; | |
} | |
SubmitOne.prototype.unblock = function() { | |
this.blocked = false; | |
} | |
function init(spec) { | |
new SubmitOne(spec); | |
} | |
return { | |
init: init | |
}; | |
}); |
Una vez desarrollado el mixin podemos usarlo en un botón o enlace de la siguiente forma, a partir de este momento dar solución a este problema nos cuesta 20 caracteres en cada enlace o botón, los necesarios para aplicar el mixin con el atributo t:mixins, pero no solo eso, más importante aún que el número de caracteres, aplicarlos no nos ofusca el código:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div class="row"> | |
<div class="col-md-4"> | |
<h5><b>Con</b> mixin en <br/> form (ajax)</h5> | |
<form t:id="submitOneForm2" t:type="form" t:zone="submitOneZone" t:mixins="submitOne"> | |
<input t:type="submit" value="Sumar 1"/> | |
</form> | |
</div> | |
<div class="col-md-4"> | |
<h5><b>Con</b> mixin en <br/> botón (ajax)</h5> | |
<form t:id="submitOneForm3" t:type="form" t:zone="submitOneZone"> | |
<input t:type="submit" value="Sumar 1" t:mixins="submitOne"/> | |
</form> | |
</div> | |
<div class="col-md-4"> | |
<h5><b>Con</b> mixin en <br/> enlace (ajax)</h5> | |
<a t:type="eventlink" t:event="sumar1CuentaAjaxSubmitOne" t:zone="submitOneZone" t:mixins="submitOne">Sumar 1</a> | |
</div> | |
</div> |
Esta es la sección de la aplicación del ejemplo funcionando donde puede probarse el mixin y ver la diferencia del comportamiento sin el mixin aplicado.
En este ejemplo también puede verse como con tapestry se pueden actualizar varias zonas (la zona de la cuenta) de una página de forma simultánea de una página web con una única petición Ajax.
Si te ha parecido interesante esta entrada puedes descargar el libro PlugIn Tapestry en el que explico más en detalle como desarrollar aplicaciones web en Tapestry y en el que descubrirás como resolver problemas comunes como este en las aplicaciones web de una forma tan buena como esta.
Si quieres probarlo en tu equipo lo puedes hacer de forma muy sencilla con los siguientes comandos y sin instalar nada. Si no dispones de git para clonar mi repositorio de GitHub puedes obtener el código fuente del repositorio en un archivo zip.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$ git clone git://github.com/picodotdev/elblogdepicodev.git | |
$ cd elblogdepicodev/PlugInTapestry | |
$ ./gradlew tomcatRun | |
# Abrir en el navegador http://localhost:8080/PlugInTapestry/ |
Libro PlugIn Tapestry
http://jumpstart.doublenegative.com.au/jumpstart/examples/javascript/creatingmixins1
http://stackoverflow.com/questions/17830171/mixins-in-tapestry5