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 HTMXBú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)| Atributo | Por defecto | Para qué |
|---|---|---|
data-url | — | Activa la búsqueda en el servidor |
data-query-param | q | Nombre del parámetro del término en la URL |
data-min-chars | 1 | Caracteres escritos antes de preguntar |
data-debounce | 300 | Espera, en ms, después de la última tecla |
data-page-param | page | Carga la página siguiente al desplazarse; false lo desactiva |
data-cache | true | Guarda 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 = htmxCada 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.
| Tecla | Acción |
|---|---|
↓ Enter Espacio | Abre el panel |
↑ ↓ | Recorre las opciones, saltando las desactivadas |
Home End | Primera y última opción, saltando las desactivadas |
Enter | Elige la opción activa |
Backspace | Con la búsqueda vacía, quita la última etiqueta |
Backspace Delete | En el simple, con la búsqueda vacía, limpia el valor, como la X |
Esc | Cierra 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.
select[data-tuc-select]new Tucano.Select(alvo, opcoes)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-tagsgetValue setValue clear refresh open close toggle destroytucano:changeOpciones
Las notas de esta tabla salen de los comentarios del código fuente, que están escritos en portugués.
| Opción | Por defecto | Para qué |
|---|---|---|
search | undefined | default: liga a partir de 6 opcoes |
searchMinItems | 6 | |
placeholder | undefined | default: do atributo, da <option value=""> ou setTexts ("Selecione...") |
searchPlaceholder | undefined | default: setTexts ("Buscar...") |
emptyText | undefined | default: setTexts ("Nenhum resultado") |
clearable | true | |
maxItems | null | limite no modo multiplo |
wrapTags | false | true deixa o campo crescer em varias linhas |
closeOnSelect | undefined | default: true em simples, false em multiplo |
placement | 'bottom-start' | |
appendTo | undefined | |
url | null | com url, a lista vem do servidor a cada digitacao |
loadOptions | null | (termo) => Promise<[{value,label,disabled,group}]> |
queryParam | 'q' | |
pageParam | 'page' | paginacao ao rolar; null desliga |
minChars | 1 | |
debounce | 300 | |
cache | true | guarda o resultado de cada termo |
cacheSize | 60 | |
shortCircuit | false | ver _noChance() |
loadingText | undefined | default: setTexts ("Buscando...") |
errorText | undefined | default: setTexts ("Falha ao buscar") |
onChange | null |