viernes, 15 de junio de 2012

Componente selector de fecha para Tapestry 5

Apache Tapestry
Tapestry tiene un montón de componentes que en la mayoría de los casos serán suficientes para implementar toda la funcionalidad que necesitemos, también disponemos los de librerías de terceros. Sin embargo, quizá en alguna ocasión no encontremos justo lo que necesitemos. Si ese es el caso no nos quedará más remedio que crear uno nuevo a nuestra medida. Si el nuevo componente es sencillo podemos partir desde cero para crearlo pero si el componente el algo más complejo es mejor partir de uno parecido a la funcionalidad que necesitemos, en cualquier caso es recomendable ver el código fuente de algunos componentes para conocer la forma de hacer las cosas, aprederemos nuevas y mejores formas de hacerlos. Esta es una de las ventajas de software libre u código abierto ¡aprovechemosla!.

En mi caso me ha pasado con el componente DateField que permite mostrar un calendario y seleccionar una fecha, también denominado date picker. Su misión principal es recoger un objeto Date del cliente. En el navegador proporciona una interfaz gráfica simple, perfectamente funcional y que cumple con su misión pero que en mi caso no me convence. A pesar de su antiguedad y que ya no está mantenido por su autor el antiguo jsCalendar es uno de los mejores javascripts que he encontrado que proporcionan esta funcionalidad. Tiene una licencia LGPL y por tanto podremos utilizarla en proyectos no-GPL. Posee una interfaz personalizable con skins y hay varios de ellos disponibles, también permite selecciona la hora y minutos y seleccionar los meses y años fácilmente.

Como se trata de un componente complejo en el ejemplo he partido del DateField del propio Tapestry y modificándolo para que incluya las referencias a los javascripts y estilos que necesita, el resto es muy parecido al original. En negrita marco la diferencias con respecto al original.

package es.com.blogspot.elblogdepicodev.tapestry.components;

import java.text.DateFormat;
import java.text.ParseException;
import java.util.Date;
import java.util.Locale;

import org.apache.commons.lang3.StringUtils;
import org.apache.tapestry5.Asset;
import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.EventConstants;
import org.apache.tapestry5.FieldValidationSupport;
import org.apache.tapestry5.FieldValidator;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.ValidationException;
import org.apache.tapestry5.ValidationTracker;
import org.apache.tapestry5.annotations.Environmental;
import org.apache.tapestry5.annotations.Events;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.corelib.base.AbstractField;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.json.JSONObject;
import org.apache.tapestry5.services.AssetSource;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;

@Events(EventConstants.VALIDATE)
public class FechaField extends AbstractField {
 /**
  * The value parameter of a DateField must be a {@link java.util.Date}.
  */
 @Parameter(required = true, principal = true, autoconnect = true)
 private Date value;

 @Parameter(required = true, allowNull = false, defaultPrefix = BindingConstants.LITERAL)
 private DateFormat format;

 @Parameter(required = true, allowNull = false, defaultPrefix = BindingConstants.LITERAL)
 private String ifFormat;

 @Parameter
 private boolean hideTextField;

 @Parameter(defaultPrefix = BindingConstants.VALIDATE)
 @SuppressWarnings("unchecked")
 private FieldValidator<object> validate;

 @Parameter(defaultPrefix = BindingConstants.ASSET, value = "calendar.png")
 private Asset icon;
 
 @Environmental
 private JavaScriptSupport javascriptSupport;

 @Environmental
 private ValidationTracker tracker;

 @Inject
 private ComponentResources resources;
 
 @Inject 
    private AssetSource assetSource;

 @Inject
 private Request request;

 @Inject
 private Locale locale;

 @Inject
 private FieldValidationSupport fieldValidationSupport;

 void beginRender(MarkupWriter writer) {  
  javascriptSupport.importStylesheet(assetSource.getClasspathAsset("classpath:com/evandti/ticketbis/tapestry/components/calendar-system.css"));
  javascriptSupport.importJavaScriptLibrary(assetSource.getClasspathAsset("classpath:com/blogspot/elblogdepicodev/tapestry/components/calendar.js"));
  javascriptSupport.importJavaScriptLibrary(assetSource.getClasspathAsset("classpath:com/blogspot/elblogdepicodev/tapestry/components/calendar-setup.js"));
  javascriptSupport.importJavaScriptLibrary(assetSource.getClasspathAsset("classpath:com/blogspot/elblogdepicodev/tapestry/components/calendar/calendar-" + locale.getLanguage() + ".js"));
  javascriptSupport.importJavaScriptLibrary(assetSource.getClasspathAsset("classpath:com/blogspot/elblogdepicodev/tapestry/components/FechaField.js"));

  String value = tracker.getInput(this);

  if (value == null)
   value = formatCurrentValue();

  String clientId = getClientId();
  String triggerId = clientId + "-trigger";

  // Input
  writer.element("input", "type", hideTextField ? "hidden" : "text", "name", getControlName(), "id", clientId, "value", value);

  writeDisabled(writer);

  putPropertyNameIntoBeanValidationContext("value");

  validate.render(writer);

  removePropertyNameFromBeanValidationContext();

  resources.renderInformalParameters(writer);

  decorateInsideField();

  writer.end();

  // Now the trigger icon.
  writer.element("img", "id", triggerId, "class", "t-fechafield-trigger", "src", icon.toClientURL(), "alt", resources.getMessages().get("Mostrar_calendario"), "title", resources.getMessages().get("Mostrar_calendario"));
  writer.end();

  JSONObject spec = new JSONObject();

  spec.put("inputField", clientId);
  spec.put("button", triggerId);
  spec.put("ifFormat", ifFormat);

  javascriptSupport.addInitializerCall("fechaField", spec);
 }

 private void writeDisabled(MarkupWriter writer) {
  if (isDisabled())
   writer.attributes("disabled", "disabled");
 }

 private String formatCurrentValue() {
  if (value == null)
   return "";

  return format.format(value);
 }

 @Override
 protected void processSubmission(String elementName) {
  String value = request.getParameter(elementName);

  tracker.recordInput(this, value);

  // Parsear el valor
  Date parsedValue = null;

  try {
   if (StringUtils.isNotBlank(value))
    parsedValue = format.parse(value);
  } catch (ParseException ex) {
   tracker.recordError(this, resources.getMessages().format("date-value-not-parseable", value));
   return;
  }

  // Validar el valor
  putPropertyNameIntoBeanValidationContext("value");
  try {
   fieldValidationSupport.validate(parsedValue, resources, validate);

   this.value = parsedValue;
  } catch (ValidationException ex) {
   tracker.recordError(this, ex.getMessage());
  }

  removePropertyNameFromBeanValidationContext();
 }

 void injectResources(ComponentResources resources) {
  this.resources = resources;
 }

 @Override
 public boolean isRequired() {
  return validate.isRequired();
 }
}

El codigo javascript de inicialización:

function FechaField(spec) {
    Calendar.setup({
        inputField     :    spec.inputField,
        button         :    spec.button,
        ifFormat       :    spec.ifFormat
    });
}

Tapestry.Initializer.fechaField = function(spec) {
 new FechaField(spec);
}

Y su uso será tan simple como:

<t:fechafield t:id="fechaNacimiento" value="persona.fechaNacimiento" format="dd/MM/yyyy" ifFormat="%d/%m/%Y" validate="required" label="Fecha de nacimiento" placeholder="Fecha de nacimiento"/>

Una captura de pantalla con la versión original y la personalizada en la que el componente se usa varias veces en una misma página:



Hay que hacer notar algunas características de este componente (y por extensión de los componentes Tapestry). Alguna ya le he comentado en otra entrada pero las vuelvo a repetir porque son interesantes y definen sus características. Una importante es que como usuarios del componente no trabajamos con parámetros de la request (Strings) como haríamos con otros frameworks/lenguajes, trabajamos directamente con objetos de un tipo ya convertido a lo que necesitamos, en este caso un objeto java.util.Date, Tapestry se encargar de hacer la conversión del String que se recibe en el servidor en primera instancia al Date y de dejarlo en la propiedad fechaNacimiento del objeto persona tal y como se indica en el parámetro value, también se encarga de validarlo y de mostrar el error cuando la validación sea incorrecta. Al usar un componente tampoco tenemos que preocuparnos de que librerías javascript o estilos necesita en la página o cual es el código javascript para inicializarlo en el navegador, además, los estilos y javascript necesarios solo se incluirán si se usa en la página un componente FechaField sino no se incluirán, así las páginas serán un poco más eficientes. De lo anterior debió preocuparse el creador del componente y Tapestry proporciona las facilidades para ello y se encarga de incluir lo que se necesite en la página según los componetes que se usen en ella. Estas características nos hacen la vida más sencilla como desarrolladores y evitan que tengamos que incluir el mismo código una y otra vez para hacer lo mismo o de forma global aunque luego no se use en la página, tampoco es necesario que conozcamos como usuarios que necesita el componente para que funcione. Algo tan común como esto se puede usar varias veces en una misma página, en diferentes páginas o proyectos (se pueden crear librerías de componentes, en un archivo jar, que incluyen todo, imágenes, css, javascript, clases), lo que simplifica el mantenimiento y es la forma en que conseguimos aumentar la productividad (entre otras cosas, esta es solo una de ellas). Son motivos por los que darle una oportunidad a Apache Tapestry. ¡Bienvenido!.

Comentar que este componente ya esta hecho en la librería tapx de @hlship pero si no quieres tener una dependencia más para usar solo un componente esto puede ser suficiente.

Si quieres conocer más revisa la documentación sobre Apache Tapestry que he ido recopilando sobre este framework.

Referencia:
http://tapestry.apache.org/current/tapestry-core/ref/
http://tapestry.apache.org/current/tapestry-core/ref/org/apache/tapestry5/corelib/components/DateField.html
http://www.gnu.org/licenses/lgpl-3.0.html
http://es.wikipedia.org/wiki/GNU_Lesser_General_Public_License