viernes, 9 de septiembre de 2011

Componente AjaxSpinner para Tapestry 5

Apache Tapestry
Hoy en día las aplicaciones web que hace peticiones de forma asíncrona con AJAX son la mayoría ya que es una técnica que permite obtener únicamente los datos que se necesitan sin tener que hacer una cargar entera de la página web en el navegador. Esta técnica hace que las peticiones al devolver únicamente los datos necesarios (y no toda la página) y los recursos para procesar la petición sean menos, también hace que el resultado de las peticiones sean más pequeños con lo que el tiempo de carga también se reduce al generar menos tŕafico de red, asi mismo el usuario nota un aumento de tiempo de respuesta. También hace que en ciertos casos las aplicaciones sean más fáciles de desarrollar por no tener que tratar en el servidor en cada petición el estado concreto de cada cliente.

Por la naturaleza de las peticiones, asíncronas y ejecutandose en segundo plano, es recomendable mostrar al usuario algún tipo de notificación que informe a este de que hay una petición AJAX en curso de tal forma que pueda saber que se está haciendo algo y que se ha completado.

En el siguiente ejemplo voy a desarrollar un componente para Apache Tapestry 5 que implemente esta funcionalidad de tal forma que siempre que se esté realizando una petición AJAX se muestre un pequeño tooltip del mismo estilo del que existe en gmail o google reader pero con los puntos suspentivos finales animados (de «.» a «..» a «...» a «....» y otra vez al inicio de «.»). En la vesión 5.2 de Tapestry aún se usa prototype y las peticiones AJAX que inicia Tapestry las realiza con esta librería. Por suerte siempre que Prototype inicia una petición o alguna finaliza lanza dos eventos informando de ello. Por lo que nuestra tarea será registrar un par de funciones propias para estos dos eventos con la lógica que queremos implementar.

El código Java del componente es el siguiente, lo importante está en el método afterRender donde se genera el código javascript de inicialización que crea la instancia del objeto javascript AjaxSpinner cuando se carga la página:

package com.blogspot.elblogdepicodev.tapestry.components;

import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.annotations.Environmental;
import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;

@Import(stylesheet = { "AjaxSpinner.css" }, library = { "AjaxSpinner.js" })
public class AjaxSpinner {

    @Environmental
    private JavaScriptSupport javaScriptSupport;
    
    @Inject
    private ComponentResources componentResources;

    protected void afterRender(MarkupWriter writer) {
        String id = componentResources.getId();

        JSONObject spec = new JSONObject();
        spec.put("id", id);
        
        javaScriptSupport.addInitializerCall("ajaxSpinner", spec);
    }
}

La plantilla del componente se encarga de generar el html con las capas que van a contener los tooltips, uno para indicar que se está realizando una petición, otro para indicar que aún está en funcionamiento si la petición tarda un rato y otro para indicar que la petición está tardando más de lo normal. Se mostrará una capa u otra dependiendo del tiempo que lleve realizandose. El código de la plantilla AjaxSpinner.tml del componente es:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<t:container xmlns:p="tapestry:parameter" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd" xmlns="http://www.w3.org/1999/xhtml">
 <div t:type="any" t:id="div1" id="${componentResources.id}1" class="ajax-spinner">
  Cargando...
 </div>
 
 <div t:type="any" t:id="div2" id="${componentResources.id}2" class="ajax-spinner">
  Aún en funcionamiento. Cargando...
 </div>
 
 <div t:type="any" t:id="divX" id="${componentResources.id}X" class="ajax-spinner ajax-spinner-tardando">
  Está tardando demasiado...
 </div>
</t:container>

El código javascript (AjaxSpinner.js) para el componente es:

function AjaxSpinner(spec) {
 this.id = spec.id;
 this.t1 = null;
 this.t2 = null;
 this.tX = null;
 this.peZ = null;

 this.d1 = $('#' + this.id + '1');
 this.d2 = $('#' + this.id + '2');
 this.dX = $('#' + this.id + 'X');

 this.d1.css('margin-left', -1 * (this.d1.width() / 2));
 this.d2.css('margin-left', -1 * (this.d2.width() / 2));
 this.dX.css('margin-left', -1 * (this.dX.width() / 2));

 var _this = this;

 $(document).ajaxStart(function() {
  _this.clear();
  _this.t1 = setTimeout(function() {
   _this.timeout1();
  }, 500);
  _this.t2 = setTimeout(function() {
   _this.timeout2();
  }, 15000);
  _this.tX = setTimeout(function() {
   _this.timeoutX();
  }, 45000);
  _this.peZ = setInterval(function() { 
   _this.timeoutZ();
  }, 1000);
 }).ajaxStop(function() {
  _this.clear();
 });
}

AjaxSpinner.prototype.timeout1 = function() {
 this.d1.show();
 this.d2.hide();
 this.dX.hide();
}

AjaxSpinner.prototype.timeout2 = function() {
 this.d1.hide();
 this.d2.show();
 this.dX.hide();
}

AjaxSpinner.prototype.timeoutX = function() {
 this.d1.hide();
 this.d2.hide();
 this.dX.show();
}

AjaxSpinner.prototype.timeoutZ = function() {
 var e = $('div[id^=' + this.id + ']:visible');
 var t = e.html().trim();
 if (t.endsWith('....')) {
  t = t.substring(0, t.length - 3);
 } else {
  t += '.';
 }
 e.html(t);
}

AjaxSpinner.prototype.clear = function() {
 if (this.t1)
  clearTimeout(this.t1);
 if (this.t2)
  clearTimeout(this.t2);
 if (this.tX)
  clearTimeout(this.tX);
 if (this.peZ)
  clearTimeout(this.peZ);
 this.d1.hide();
 this.d2.hide();
 this.dX.hide();
}

Tapestry.Initializer.ajaxSpinner = function(spec) {
 new AjaxSpinner(spec);
}

Y los estilos CSS (AjaxSpinner.css) para dar a los tooltips el mismo aspecto que los de gmail y google reader:

div.ajax-spinner {
 display: none;
 position: fixed;
 padding: 5px;
 top: 0px;
 left: 50%;
 text-align: center;
 background-color: #fff1a8;
 border-bottom-left-radius: 5px;
 border-bottom-right-radius: 5px;
 z-index: 9999;
 vertical-align: middle;
}

div.ajax-spinner-tardando {
 background-color: #f8baba;
}

Una vez desarrollado el componente lo único que tendríamos que hacer es hacer uso del componente en la plantilla de la página donde queramos usarlo, por ejemplo:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd" xmlns:p="tapestry:parameter">
<head>
    <title>${titulo}</title>

    <link rel="stylesheet" type="text/css" href="http://fonts.googleapis.com/css?family=Ubuntu:regular,italic,bold,bolditalic"></link>
    <link t:type="any" rel="shortcut icon" href="context:/images/favicon.ico"></link>
    <meta name="description" content="${message:descripcion-app}"/>
    <meta name="keywords" content="${message:keywords-app}"/>
    
    <t:extension-point id="head">
    </t:extension-point>
</head>
<body>
    <t:ajaxspinner t:id="principal"/>

    <div class="body">
        <t:extension-point id="cabecera">
        </t:extension-point>
        
        <t:extension-point id="menu">
        </t:extension-point>
        
        <t:extension-point id="cuerpo">
        </t:extension-point>

        <t:extension-point id="pie">
        </t:extension-point>
    </div>
</body>

El resultado cuando se hace un petición AJAX que tarda bastante tiempo es:

Tal vez te preguntes... ¿solo hace falta poner <t:ajaxspinner t:id="principal"/> para usarlo? ¿y el archivo .js con el código javascript no hay que meterlo en el head de la plantilla? ¿y la instanciación del AjaxSpinner en javascript? ¿y el CSS tampoco hay que meterlo en el head? No, no hace falta saber que el componente hace uso de un archivo de javscript ni la hora de estilos que necesita ni saber como se inicializa de eso ya tiene conocimiento tapestry por la definición del componente (mediante la anotación Import) y se encarga de incluir todo y de hacerlo eficientemente agregando el contenido de los archivos en uno solo. También el propio componente se encarga de generar el código javascript que necesita para inicalizarse.

Esta es una de las grandes características de los componentes tapestry ya que como véis lo único que hace falta es usarlo y pasarle los parametros necesarios (aunque en esta caso no tiene ninguno), no hace falta saber como funciona internamente, los estilos y que javascript necesita. Los componentes de tapestry permite abstraernos enormemente de sus detalles y eso es bueno, muy bueno ¿no crees?.

Referencia:
Documentación sobre Apache Tapestry