Tucano v0.37.2

Select

Enriquece el <select> nativo con búsqueda, etiquetas y búsqueda en el servidor. El elemento original sigue en el formulario guardando el valor: name, multiple, required y request.POST.getlist() funcionan como siempre.

Ejemplos

Todo lo de abajo es el mismo <select> con atributos distintos.

Grupos y opción desactivada

<optgroup> se convierte en título de grupo; disabled se respeta en la opción, en el grupo entero y en el propio <select>, que no abre ni limpia.

Límite de elecciones

data-max-items="3" se detiene en la tercera.

Etiquetas en varias líneas

data-wrap-tags="true" deja que el campo crezca en lugar de desplazarse.

Con error

aria-invalid="true" en el nativo — Django 5 ya lo escribe.

Elige un estado.

Cómo usar

Marca el <select> y se inicializa solo, incluso el que llegue después por HTMX. La primera <option value=""> se convierte en el texto de espera, y no en una opción.

<select name="state" data-tuc-select>
  <option value="">Selecciona...</option>
  <option value="SP">São Paulo</option>
</select>

<select name="tags" data-tuc-select multiple data-placeholder="Añade...">
  <option value="django">Django</option>
</select>

En el formulario de Django

class CustomerForm(forms.ModelForm):
    class Meta:
        model = Customer
        fields = ["state", "tags"]
        widgets = {
            "state": forms.Select(attrs={"data-tuc-select": ""}),
            "tags": forms.SelectMultiple(attrs={"data-tuc-select": "", "data-max-items": "3"}),
        }

En JavaScript

Para opciones que no existen como atributo, o para leer y escribir el valor.

const s = new Tucano.Select('#state', { search: true, maxItems: 3 });
s.getValue();            // 'SP' — un array en el múltiple
s.setValue(['SP', 'PR']);
s.refresh();             // vuelve a leer las <option> después de sustituirlas por HTMX

Búsqueda en el servidor

Para listas demasiado grandes para venir enteras en la página. A cada tecla el componente le pregunta al servidor, en lugar de filtrar lo que ya está cargado.

En esta página la respuesta se simula con 400 ms de espera; en tu proyecto viene de data-url.

<select name="customer" data-tuc-select data-url="{% url 'api-customers' %}"></select>
def customers(request):
    q = request.GET.get("q", "")
    qs = Customer.objects.filter(name__icontains=q)[:20]
    return JsonResponse([{"value": c.pk, "label": c.name} for c in qs], safe=False)
AtributoPor defectoPara qué
data-url—Activa la búsqueda en el servidor
data-query-paramqNombre del parámetro del término en la URL
data-min-chars1Caracteres escritos antes de preguntar
data-debounce300Espera, en ms, después de la última tecla
data-page-parampageCarga la página siguiente al desplazarse; false lo desactiva
data-cachetrueGuarda el resultado de cada término

Acepta la respuesta en varios formatos

Para que el servidor no tenga que cambiar por culpa de esto: [{value, label}], ["a", "b"], {results: [...]} de DRF y {id, text} de Select2. Para cualquier otro origen existe loadOptions.

Por qué JSON, y no HTMX, aquí

La lista vive dentro del panel que el componente redibuja. Si HTMX también escribiera ahí, habría dos dueños del mismo DOM. Y para una lista de opciones JSON es el contrato correcto: el HTML tendría que volver a interpretarse como objetos.

Valor y formulario

Quien guarda el valor es el <select> nativo, oculto al lado del control. Django recibe exactamente lo que recibiría sin el componente.

POST  state = SP
POST  tags  = django    # request.POST.getlist("tags")
POST  tags  = htmx

Cada elección dispara tucano:change en el propio <select>, y también el change nativo — así el hx-trigger="change" de HTMX funciona sin ajustes.

document.querySelector('#state').addEventListener('tucano:change', (e) => {
  e.detail.value;      // 'SP', o un array en el múltiple
  e.detail.instance;   // el Select
});

Teclado y accesibilidad

El control es un combobox con listbox, y anuncia abierto, cerrado y la opción activa. Llegar con Tab no abre el panel — es la regla del <select> nativo.

TeclaAcción
↓ Enter EspacioAbre el panel
↑ ↓Recorre las opciones, saltando las desactivadas
Home EndPrimera y última opción, saltando las desactivadas
EnterElige la opción activa
BackspaceCon la búsqueda vacía, quita la última etiqueta
Backspace DeleteEn el simple, con la búsqueda vacía, limpia el valor, como la X
EscCierra y devuelve el foco al campo

La búsqueda ignora los acentos

sao encuentra "São Paulo". Y el campo tiene altura fija a propósito: con etiquetas saltando de línea crecería y desalinearía el formulario — lo que sobra se desplaza en horizontal, salvo con data-wrap-tags.

API

Generada a partir del código en cada build — si algo no está aquí, no existe.

Marcado
select[data-tuc-select]
En JS
new Tucano.Select(alvo, opcoes)
Atributos
data-cache data-clearable data-close-on-select data-debounce data-empty-text data-max-items data-min-chars data-page-param data-query-param data-search data-short-circuit data-url data-wrap-tags
Métodos
getValue setValue clear refresh open close toggle destroy
Eventos
tucano:change

Opciones

Las notas de esta tabla salen de los comentarios del código fuente, que están escritos en portugués.

OpciónPor defectoPara qué
searchundefineddefault: liga a partir de 6 opcoes
searchMinItems6
placeholderundefineddefault: do atributo, da <option value=""> ou setTexts ("Selecione...")
searchPlaceholderundefineddefault: setTexts ("Buscar...")
emptyTextundefineddefault: setTexts ("Nenhum resultado")
clearabletrue
maxItemsnulllimite no modo multiplo
wrapTagsfalsetrue deixa o campo crescer em varias linhas
closeOnSelectundefineddefault: true em simples, false em multiplo
placement'bottom-start'
appendToundefined
urlnullcom url, a lista vem do servidor a cada digitacao
loadOptionsnull(termo) => Promise<[{value,label,disabled,group}]>
queryParam'q'
pageParam'page'paginacao ao rolar; null desliga
minChars1
debounce300
cachetrueguarda o resultado de cada termo
cacheSize60
shortCircuitfalsever _noChance()
loadingTextundefineddefault: setTexts ("Buscando...")
errorTextundefineddefault: setTexts ("Falha ao buscar")
onChangenull