Tucano v0.37.2

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 HTMX

Busca 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)
AtributoPadrãoPara quê
data-url—Liga a busca no servidor
data-query-paramqNome do parâmetro do termo na URL
data-min-chars1Caracteres digitados antes de perguntar
data-debounce300Espera, em ms, depois da última tecla
data-page-parampageCarrega a próxima página ao rolar; false desliga
data-cachetrueGuarda 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  = htmx

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

TeclaAção
↓ Enter EspaçoAbre o painel
↑ ↓Anda entre as opções, pulando as desativadas
Home EndPrimeira e última opção, pulando as desativadas
EnterEscolhe a opção ativa
BackspaceCom a busca vazia, remove a última tag
Backspace DeleteNo simples, com a busca vazia, limpa o valor, como o X
EscFecha 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.

Marcação
select[data-tuc-select]
Em 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

Opções

OpçãoPadrãoPara 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