Componente select múltiple en Apache Tapestry

Escrito por el .
blog-stack java javascript planeta-codigo programacion tapestry
Comentarios

Apache Tapestry
Java

El framework basado en componentes Apache Tapestry incorpora una amplia colección de componentes que nos bastarán en la mayoría de casos que necesitemos. Para los casos en que deseemos un componente con un comportamiento específico podemos construir uno completamente nuevo basado en otros existentes incluyendo los propios nuestros, de una librería o incluidos en Tapestry.

En Tapestry hay múltiples componentes con los que construir formularios para que el usuario pueda introducir datos, ser enviados por el navegador y procesados en el servidor. Hay componentes de formulario desde checkboxes, radios, select, inputs, … con soporte para HTML 5.

Observando en detalle la lista de componentes ofrecidos nos daremos cuenta de que está un componente Select pero que solo se puede utilizar para que el usuario seleccione una única opción, sin embargo, en el estándar de HTML los campos de selección pueden utilizarse para seleccionar múltiples opciones. Como se no ofrece un componente select para seleccionar múltiples opciones a la vez si lo necesitamos deberemos implementar uno que nos ofrezca esta funcionalidad. Con el codigo fuente de Tapestry la tarea es mucho más sencilla y prácticamente es copiar y pegar, el código completo del componente MultiSelect será lo que muestre a continuación.

Todo componente de formulario en este framework hereda de AbstracField en el que básicamente deberemos proporcionar una implementación del método processSubmission() donde procesaremos los datos recibidos en este caso con un método equivalente al tradicional en Java EE ServletRequest.getParameterValues. La otra parte que deberemos implementar es la generación de etiquetas HTML del componente en el método de ciclo de vida beginRender() que en gran parte nos servirá lo implementado en el código fuente del componente Select de Tapestry pero incluyendo el atributo multiple que requiere HTML para los selects de múltiples opciones.

Esta sería una implementación de un componente select múltiple. La mayor diferencia entre el componente Select y este MultiSelect está en la propiedad value que en el primero es de tipo Object donde se guardará el dato seleccionado y selected en el segundo que es un List de objetos donde se guardarán los datos seleccionados.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
package io.github.picodotdev.plugintapestry.components;

import org.apache.tapestry5.*;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.SupportsInformalParameters;
import org.apache.tapestry5.corelib.base.AbstractField;
import org.apache.tapestry5.internal.util.SelectModelRenderer;

import java.util.Collection;

@SupportsInformalParameters
public class MultiSelect extends AbstractField {

    /**
     * A ValueEncoder used to convert server-side objects (provided from the
     * "source" parameter) into unique client-side strings (typically IDs) and
     * back. Note: this component does NOT support ValueEncoders configured to
     * be provided automatically by Tapestry.
     */
    @Parameter
    private ValueEncoder<Object> encoder;

    /**
     * Model used to define the values and labels used when rendering.
     */
    @Parameter(required = true, allowNull = false)
    private SelectModel model;

    /**
     * The list of selected values from the {@link SelectModel}. This will be updated when the form
     * is submitted. If the value for the parameter is null, a new list will be created, otherwise the existing list
     * will be cleared. If unbound, defaults to a property of the container matching this component's id.
     * <p>
     * Prior to Tapestry 5.4, this allowed null, and a list would be created when the form was submitted. Starting
     * with 5.4, the selected list may not be null, and it need not be a list (it may be, for example, a set).
     */
    @Parameter(required = true, autoconnect = true, allowNull = false)
    private Collection<Object> selected;

    /**
     * The object that will perform input validation. The validate binding prefix is generally used to provide
     * this object in a declarative fashion.
     *
     * @since 5.2.0
     */
    @Parameter(defaultPrefix = BindingConstants.VALIDATE)
    private FieldValidator<Object> validate;

    public final Renderable mainRenderer = new Renderable() {
        public void render(MarkupWriter writer) {
            SelectModelRenderer visitor = new SelectModelRenderer(writer, encoder, false) {
                @Override
                protected boolean isOptionSelected(OptionModel optionModel, String clientValue) {
                    return selected.contains(optionModel.getValue());
                }
            };

            model.visit(visitor);
        }
    };

    @Override
    protected void processSubmission(String controlName) {
        String[] values = request.getParameters(controlName);
        values = (values == null) ? new String[0]: values;

        // Use a couple of local variables to cut down on access via bindings

        Collection<Object> selected = this.selected;

        selected.clear();

        ValueEncoder encoder = this.encoder;

        for (String value : values) {
            Object objectValue = toValue(value);

            selected.add(objectValue);
        }

        putPropertyNameIntoBeanValidationContext("selected");

        try {
            fieldValidationSupport.validate(selected, resources, validate);

            this.selected = selected;
        } catch (final ValidationException e) {
            validationTracker.recordError(this, e.getMessage());
        }

        removePropertyNameFromBeanValidationContext();
    }

    void beginRender(MarkupWriter writer) {
        writer.element("select", "name", getControlName(), "id", getClientId(), "multiple", "multiple", "disabled", getDisabledValue(), "class", cssClass);

        putPropertyNameIntoBeanValidationContext("selected");

        validate.render(writer);

        removePropertyNameFromBeanValidationContext();

        resources.renderInformalParameters(writer);

        decorateInsideField();

        mainRenderer.render(writer);
    }

    void afterRender(MarkupWriter writer) {
        writer.end();
    }

    ValueEncoder defaultEncoder() {
        return defaultProvider.defaultValueEncoder("selected", resources);
    }

    /**
     * Computes a default value for the "validate" parameter using
     * {@link org.apache.tapestry5.services.FieldValidatorDefaultSource}.
     */
    Binding defaultValidate() {
        return this.defaultProvider.defaultValidatorBinding("selected", this.resources);
    }

    String toClient(Object value) {
        return encoder.toClient(value);
    }

    Object toValue(String clientValue) { return ((Collection) encoder.toValue(clientValue)).toArray()[0]; }

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

    public String getDisabledValue() {
        return disabled ? "disabled" : null;
    }
}

Esta implementación del componente no necesita de una plantilla tml sino que todo el HTML se generará desde el código Java. El componente soporta parámetros informales como indicamos con la anotación @SupportsInformalParameters que son parámetros que se añadirán a la etiqueta select incluidos tal cual se indican en su uso que por ejemplo podemos utilizar para variar el número de opciones visibles, personalizar los textos y otras funcionalidades de bootstrap-select.

1
2
3
4
5
6
7
8
...
<div class="form-group">
  <t:multiselect selected="coloresSelect" model="coloresSelectModel" class="selectpicker show-menu-arrow" 
      size="3" data-none-selected-text="Nada seleccionado" data-actions-box="true" data-select-all-text="Todos"
      data-deselect-all-text="Ninguno" data-selected-text-format="count > 2" data-count-selected-text="{0} de {1} seleccionados"
      data-show-tick="true"/>
</div>
...
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package io.github.picodotdev.plugintapestry.pages;

...

public class Index {

  ...

    public SelectModel getColoresSelectModel() {
        return new AbstractSelectModel() {
            @Override
            public List<OptionGroupModel> getOptionGroups() {
                return null;
            }

            @Override
            public List<OptionModel> getOptions() {
                OptionModel rojo = new AppOptionModel("Rojo", false, "rojo", Collections.EMPTY_MAP);
                OptionModel azul = new AppOptionModel("Azul", false, "azul", Collections.EMPTY_MAP);
                OptionModel verde = new AppOptionModel("Verde", false, "verde", Collections.EMPTY_MAP);
                return Arrays.asList(rojo, azul, verde);
            }
        };
    }
}

El código HTML generado por el componente es el siguiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
...
<select data-actions-box="true" size="3" data-select-all-text="Todos"
    data-deselect-all-text="Ninguno" data-count-selected-text="{0} de {1} seleccionados"
    data-selected-text-format="count &gt; 2" data-show-tick="true" data-none-selected-text="Nada seleccionado"
    class="form-control selectpicker show-menu-arrow"
    multiple="multiple" id="multiselect" name="multiselect">
  <option value="rojo">Rojo</option>
  <option value="azul">Azul</option>
  <option value="verde">Verde</option>
</select>
...

Este sería el aspecto del select múltiple con sus botones para seleccionar todas las opciones y deseleccionar todas, además de personalizados los textos y una pequeña flecha en el desplegable hacia el componente select, todo esto configurado principalmente a través de atributos data y clases CSS.

Componente MultiSelect con bootstrap-select

Si queremos seleccionar múltiples opciones usando checkboxes Tapestry ofrece el componente Checklist y usando selects otra opción es el componente Palette pero seguramente no sea lo que deseamos.

Componentes Checklist y Palette

En un artículo anterior comenté como adaptar el componente Select y este MultiSelect para añadirle funcionalidades de la librería bootstrap-select como cuadro de búsqueda, búsqueda por palabras clave, divisores, etc que consiste en añadir a las etiquetas HTML select, optiongrp y option ciertos atributos con sus correspondientes valores con una combinación de parámetros informales y personalización de la clase SelectModel. En el siguiente artículo comentaré como añadir botones selectores de opciones además de los que incorpora bootstrap-select de Todos y Ninguno.

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradlew run.

Portada libro: PlugIn Tapestry

Libro PlugIn Tapestry

Si te interesa Apache Tapestry descarga gratis el libro de más de 300 páginas que he escrito sobre este framework en el formato que prefieras, PlugIn Tapestry: Desarrollo de aplicaciones y páginas web con Apache Tapestry, y el código de ejemplo asociado. En el libro comento detalladamente muchos aspectos que son necesarios en una aplicación web como persistencia, pruebas unitarias y de integración, inicio rápido, seguridad, formularios, internacionalización (i18n) y localización (l10n), AJAX, ... y como abordarlos usando Apache Tapestry.