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