Select
Enriquece o <select> nativo com busca, tags e busca no servidor. O elemento
original continua no formulário guardando o valor: name, multiple,
required e request.POST.getlist() funcionam como sempre.
Exemplos
Tudo abaixo é o mesmo <select> com atributos diferentes.
Grupos e opção desativada
<optgroup> vira título de grupo; disabled é respeitado na opção, no grupo inteiro e no próprio <select>, que não abre nem limpa.
Limite de escolhas
data-max-items="3" para no terceiro.
Tags em várias linhas
data-wrap-tags="true" deixa o campo crescer em vez de rolar.
Com erro
aria-invalid="true" no nativo — o Django 5 já escreve.
Escolha um estado.
Como usar
Marque o <select> e ele inicializa sozinho, inclusive o que chegar depois por HTMX.
A primeira <option value=""> vira o texto de espera, e não uma opção.
<select name="state" data-tuc-select>
<option value="">Selecione...</option>
<option value="SP">São Paulo</option>
</select>
<select name="tags" data-tuc-select multiple data-placeholder="Adicione...">
<option value="django">Django</option>
</select>No formulário do 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"}),
}Em JavaScript
Para opções que não existem como atributo, ou para ler e escrever o valor.
const s = new Tucano.Select('#state', { search: true, maxItems: 3 });
s.getValue(); // 'SP' — um array no múltiplo
s.setValue(['SP', 'PR']);
s.refresh(); // relê as <option> depois de trocá-las por HTMXBusca no servidor
Para listas grandes demais para vir inteiras na página. A cada digitação o componente pergunta ao servidor, em vez de filtrar o que já está carregado.
Nesta página a resposta é simulada com 400 ms de espera; no seu projeto ela vem do 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 | Padrão | Para quê |
|---|---|---|
data-url | — | Liga a busca no servidor |
data-query-param | q | Nome do parâmetro do termo na URL |
data-min-chars | 1 | Caracteres digitados antes de perguntar |
data-debounce | 300 | Espera, em ms, depois da última tecla |
data-page-param | page | Carrega a próxima página ao rolar; false desliga |
data-cache | true | Guarda o resultado de cada termo |
Aceita a resposta em vários formatos
Para o servidor não ter de mudar por causa daqui: [{value, label}], ["a", "b"], {results: [...]} do DRF e {id, text} do Select2. Para qualquer outra origem existe loadOptions.
Por que JSON, e não HTMX, aqui
A lista vive dentro do painel que o componente redesenha. Se o HTMX também escrevesse ali, seriam dois donos do mesmo DOM. E para uma lista de opções JSON é o contrato certo: HTML teria de ser reinterpretado de volta em objetos.
Valor e formulário
Quem guarda o valor é o <select> nativo, escondido ao lado do controle. O Django recebe
exatamente o que receberia sem o componente.
POST state = SP
POST tags = django # request.POST.getlist("tags")
POST tags = htmxCada escolha dispara tucano:change no próprio <select>, e também o change nativo — então o hx-trigger="change" do HTMX funciona sem ajuste.
document.querySelector('#state').addEventListener('tucano:change', (e) => {
e.detail.value; // 'SP', ou um array no múltiplo
e.detail.instance; // o Select
});Teclado e acessibilidade
O controle é um combobox com listbox, e anuncia aberto, fechado e a opção ativa.
Chegar de Tab não abre o painel — é a regra do <select> nativo.
| Tecla | Ação |
|---|---|
↓ Enter Espaço | Abre o painel |
↑ ↓ | Anda entre as opções, pulando as desativadas |
Home End | Primeira e última opção, pulando as desativadas |
Enter | Escolhe a opção ativa |
Backspace | Com a busca vazia, remove a última tag |
Backspace Delete | No simples, com a busca vazia, limpa o valor, como o X |
Esc | Fecha e devolve o foco ao campo |
A busca ignora acentos
sao encontra "São Paulo". E o campo tem altura fixa de propósito: com tags quebrando linha ele cresceria e desalinharia o formulário — o excesso rola na horizontal, a não ser com data-wrap-tags.
API
Gerada do código a cada build — se algo não está aqui, não 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:changeOpções
| Opção | Padrão | 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 |