Añadir botones selectores de opciones a select múltiple de bootstrap-select

Escrito por el , actualizado el .
java javascript planeta-codigo programacion tapestry
Enlace permanente Comentarios

Apache Tapestry

Java

La librería bootstrap-select nos permite crear elementos select enriquecidos con más funcionalidades que las propias ofrecidas por el navegador para seleccionar una única opción o para seleccionar múltiples opciones. Usando esta librería y añadiendo algunos atributos a las etiquetas HTML select, optgroup y option añadirá varias funcionalidades interesantes como comento en Componente select de Apache Tapestry con funcionalidades adicionales usando bootstrap-select.

Una de las opciones que añade es poniendo en la etiqueta select el atributo data-actions-box son dos botones para seleccionar todas las opciones o para deseleccionar todas las opciones.

Este es el aspecto de un elemento de selección con los botones de seleccionar todas las opciones y ninguna que son muy útiles para el usuario evitándose seleccionar una a una cada una de las opciones.

Multiselect con opciones Todos y Ninguno Opciones seleccionadas de Multiselect

MultiSelect con opciones Todos y Ninguno

A pesar de todas las opciones adicionales que añade bootstrap-select incluidas los botones de seleccionar todas las opciones y ninguna aún quizá queramos personalizar más el comportamiento, por ejemplo, permitir seleccionar con botones adicionales un grupo de opciones relacionadas. Supongamos que tenemos un componente de selección múltiple de países y queremos seleccionar los países de Europa, América o Asia además de las opciones que bootstrap-select de todos y ninguno.

Un atributo que usa bootstrap-select para la opción de filtrado es el atributo data-tokens, si el valor introducido en el filtro coincide con este atributo la opción se muestra y en las que no coincide se oculta. Para no añadir más atributos usaré este atributo para asociar a la opción a los grupos que pertenecen o los tokens que tiene asociados. Dada una serie de tokens para los que queremos botón de filtrado en el atributo data-tokens-selectors.

Este sería un ejemplo de código JavaScript junto con el uso del componente de Tapestry en la aplicación que podríamos emplear para añadir al elemento selector este comportamiento de selección de grupos de opciones que básicamente añade de forma dinámica un pequeño trozo de HTML similar al que el componente utiliza para mostrar los botones de todos y ninguno. Cuando se haga clic en un botón de selección con el evento loaded.bs.select se buscan las opciones que contiene el token asociado al botón y se seleccionan teniendo en cuenta también las opciones que estén deshabilitadas. Además, cuando todas las opciones de un botón selector están seleccionadas queda como pulsado como indicativo, lógica que se realiza en el evento change.

1
2
3
4
5
<t:multiselect selected="paises" model="paisesSelectModel" class="selectpicker show-menu-arrow"
  data-size="6" 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"
  data-tokens-selectors="europa,america,asia"/>
Index.tml
 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
define("app/multiselect", ["jquery"], function($) {

    function Multiselect(spec) {
        this.select = $('#' + spec.clientId);

        var tokensSelectors = this.select.data('tokens-selectors');
        if (tokensSelectors == null || tokensSelectors.length == 0) {
            return;
        }
        this.tokensSelectors = tokensSelectors.split(',');
        var that = this;

        this.select.on('loaded.bs.select', function(event) {
            var buttons = '';
            $.each(that.tokensSelectors, function(i, it) {
                buttons += (i % 2 == 0) ? '<div class="btn-group btn-group-sm btn-block">' : '';
                buttons += '<button type="button" data-select-custom-token="' + it + '" class="actions-btn-custom bs-select-custom btn btn-default">' + it.substr(0, 1).toUpperCase() + it.substr(1) + '</button>'
                buttons += (i % 2 == 1 || i + 1 == tokensSelectors.length) ? '</div>' : '';
            });
            $('button[data-id="' + spec.clientId + '"] + div[role="combobox"] div.bs-actionsbox').append(buttons);
	    that.select.trigger('change');

            $('button[data-id="' + spec.clientId + '"] + div[role="combobox"] div.bs-actionsbox').on('click', 'button.bs-select-custom', function (event) {
                event.preventDefault();
                event.stopPropagation();

                var token = $(this).attr("data-select-custom-token");
                var values = that.select.find('[data-tokens~=' + token + ']').filter(':not([disabled])').map(function() {
                    return $(this).val();
                }).get();
                values = values.concat(that.select.val());
                that.select.selectpicker('val', values);
                that.select.trigger('change');
            });
        });

        this.select.on('change', function(event, clickedIndex, newValue, oldValue) {
            $.each(that.tokensSelectors, function(i, it) {
                var values = that.select.find('[data-tokens~=' + it + ']').filter(':not([disabled])').map(function() {
                    return $(this).val();
                }).get();
                var active = values.every(function(value) {
                    return that.select.val() !== null && that.select.val().includes(value);
                });
                $('button[data-id="' + spec.clientId + '"] + div[role="combobox"] div.bs-actionsbox')
                    .find('[data-select-custom-token="' + it + '"]').toggleClass('active', active);
            });
        });
    }

    function init(spec) {
        new Multiselect(spec);
    }
	
    return {
        init: init
    }
});
multiselect.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
...
<div class="bs-actionsbox">
  <div class="btn-group btn-group-sm btn-block">
    <button type="button" class="actions-btn bs-select-all btn btn-default">Todos</button>
    <button type="button" class="actions-btn bs-deselect-all btn btn-default">Ninguno</button>
  </div>
  <div class="btn-group btn-group-sm btn-block">
    <button type="button" data-select-custom-token="europa" class="actions-btn-custom bs-select-custom btn btn-default">Europa</button>
    <button type="button" data-select-custom-token="america" class="actions-btn-custom bs-select-custom btn btn-default">America</button>
  </div>
  <div class="btn-group btn-group-sm btn-block">
    <button type="button" data-select-custom-token="asia" class="actions-btn-custom bs-select-custom btn btn-default">Asia</button>
  </div>
</div>
...
Index.html

Y este sería el aspecto de componente en el navegador.

Multiselect con opciones Europa Multiselect con opciones Asia

MultiSelect con opciones Europa, America y Asia

Si usásemos el componente de selección múltiple con Apache Tapetstry y bootstrap-select sería el propio componente MultiSelect el que se encargaría de incluir el JavaScript en la página cuando en ella se usase lo que nos evita incluir el JavaScript de forma global en la aplicación y en todas las páginas cuando realmente no se usa.

 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
package io.github.picodotdev.plugintapestry.components;

...

@SupportsInformalParameters
public class MultiSelect extends AbstractField {

    ...
    void beginRender(MarkupWriter writer) {
        ...

        JSONObject spec = new JSONObject();
        spec.put("clientId", getClientId());
        javaScriptSupport.require("app/multiselect").invoke("init").with(spec);
    }
    ...
    public SelectModel getPaisesSelectModel() {
        return new AbstractSelectModel() {
            @Override
            public List<OptionGroupModel> getOptionGroups() {
                Map<String,String> europe = new HashMap<String, String>();
                Map<String,String> america = new HashMap<String, String>();
                Map<String,String> asia = new HashMap<String, String>();
                europe.put("data-tokens", "europa");
                america.put("data-tokens", "america");
                asia.put("data-tokens", "asia");

                OptionModel espana = new AppOptionModel("España", false, "espana", europe);
                OptionModel francia = new AppOptionModel("Francia", false, "francia", europe);
                OptionModel alemania = new AppOptionModel("Alemania", false, "alemania", europe);

                OptionModel eeuu = new AppOptionModel("EEUU", false, "eeuu", america);
                OptionModel mexico = new AppOptionModel("Mexico", false, "mexico", america);
                OptionModel argentina = new AppOptionModel("Argentina", false, "argentina", america);

                OptionModel china = new AppOptionModel("China", false, "china", asia);
                OptionModel japon = new AppOptionModel("Japón", false, "japon", asia);
                OptionModel india = new AppOptionModel("India", true, "india", asia);

                OptionGroupModel europaGroup = new AppOptionGroupModel("Europa", false, Collections.EMPTY_MAP, Arrays.asList(espana, francia, alemania));
                OptionGroupModel americaGroup = new AppOptionGroupModel("América", false, Collections.EMPTY_MAP, Arrays.asList(eeuu, mexico, argentina));
                OptionGroupModel asiaGroup = new AppOptionGroupModel("Asia", false, Collections.EMPTY_MAP, Arrays.asList(china, japon, india));
                return Arrays.asList(europaGroup, americaGroup, asiaGroup);
            }

            @Override
            public List<OptionModel> getOptions() {
                return null;
            }
        };
    }
    ...
}
MultiSelect.java
Terminal

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 siguiente 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.



Comparte el artículo: