Skip to main content

Framework-free Django form widgets: combobox and multiselect, with static and AJAX search.

Project description

django-select3

Widgets Django Forms para os componentes "Select3" — combobox e multiselect com busca AJAX. Sem dependências de front-end (nada de Alpine.js, jQuery ou Tailwind em runtime).

Use Select3 em qualquer forms.Form/forms.ModelForm apenas trocando o widget=....

O app registra CSS e JS próprios, inicializando via data-select3.

Nome de distribuição no PyPI: django-select3. O pacote Python importável é django_select3 (pip install django-select3import django_select3).

O CSS é autossuficiente e escopado (todas as classes têm prefixo s3- e ficam sob .s3-wrapper/.s3-panel), então não vaza reset/estilos para o resto da sua aplicação.

O que vem pronto

Widgets disponíveis (4 variações):

  • Select3ComboboxWidget: single + opções estáticas
  • Select3ComboboxAjaxWidget: single + busca AJAX
  • Select3MultiSelectWidget: multi + opções estáticas
  • Select3MultiSelectAjaxWidget: multi + busca AJAX

Arquivos importantes:

  • CSS interno: static/select3/select3-bundle.css
  • JS interno: static/select3/select3-widgets.js (namespace window.select3Widgets)
  • Templates: templates/select3/widgets/*.html

Instalação / ativação

Instalando via pip

pip install django-select3

Adicione django_select3 ao INSTALLED_APPS do seu projeto Django.

Durante o desenvolvimento local (a partir deste repo), instale em modo editável:

pip install -e .

Implementação em um projeto Django

O Select3 é uma biblioteca de widgets, basta adicionar o app, usar os widgets no form e renderizar o {{ form.media }}.

Durante o desenvolvimento local (a partir deste repo), use instalação editável:

python -m pip install -e .
  1. Garanta que django_select3 esteja no INSTALLED_APPS.

  2. Garanta que seu template renderize os assets dos widgets.

O jeito recomendado é usar o {{ form.media }} (ou {{ form.media.css }} e {{ form.media.js }}), porque os widgets declaram Media.

Exemplo (em um template qualquer onde o form aparece):

<form method="post">
  {% csrf_token %}

  {{ form.media }}
  {{ form.as_p }}

  <button type="submit">Salvar</button>
</form>

Assets carregados pelos widgets

Os widgets carregam sempre os assets internos da biblioteca:

  • select3/select3-bundle.css
  • select3/select3-widgets.js

Sobrescrever cores (tema)

O CSS do Select3 é themeável por CSS variables. A principal é --s3-primary.

Por padrão, ela é definida como:

  • --s3-primary: var(--color-primary, #3b82f6);

Ou seja: você pode definir --s3-primary diretamente, ou (se preferir) definir --color-primary no seu design system.

Exemplo (no seu CSS global da aplicação):

:root {
  --s3-primary: #16a34a;
  /* alternativa: --color-primary: #16a34a; */
}

Além da cor primária, outras variáveis podem ser sobrescritas (todas com valores padrão sensatos): --s3-bg, --s3-text, --s3-muted, --s3-border, --s3-border-hover, --s3-hover-bg, --s3-danger, --s3-radius, --s3-height, --s3-font-size.

Se quiser aplicar apenas em uma área da página (escopo), basta definir a variável em um contêiner ancestral:

.minha-area {
  --s3-primary: #9333ea;
}
<div class="minha-area">
  {{ form.media }}
  {{ form.as_p }}
</div>

Traduções / textos da interface (i18n)

Os textos padrão dos widgets são em inglês.

  • No lado Python, os placeholders padrão usam gettext_lazy, então respeitam o LANGUAGE_CODE/traduções do Django. Você também pode passar placeholder=... diretamente em cada widget.
  • No lado JS, os textos (mensagens de "sem resultados", "carregando", etc.) podem ser sobrescritos definindo window.select3WidgetsConfig.i18n antes de carregar o script:
<script>
  window.select3WidgetsConfig = {
    i18n: {
      noResults: "Nenhum resultado encontrado",
      noOptions: "Nenhuma opção disponível",
      searching: "Buscando...",
      minChars: "Digite pelo menos {n} caracteres para buscar",
      loadingMore: "Carregando mais...",
      scrollForMore: "Role para carregar mais...",
      remove: "Remover",
      loading: "Carregando...",
      selectPlaceholder: "Selecione uma opção",
      searchPlaceholder: "Busque...",
      multiPlaceholder: "Digite para buscar...",
    },
  };
</script>

Conteúdo dinâmico (HTMX / modais / swaps)

O JS dos widgets inicializa automaticamente qualquer elemento com data-select3:

  • no carregamento da página (DOMContentLoaded)
  • e também quando novos elementos são inseridos no DOM (via MutationObserver)

Ou seja: se você renderiza forms via HTMX (ou injeta HTML via modal), os widgets devem “subir” sem precisar de snippet extra.

Opt-out do observer

Se você preferir controlar manualmente (por performance ou previsibilidade), desabilite o observer antes de carregar o JS:

<script>
  window.select3WidgetsConfig = { observe: false };
</script>

E chame manualmente quando precisar:

window.select3Widgets.initAll(containerElement)

Cleanup (quando remover elementos)

O JS expõe helpers para limpar listeners e dropdowns criados no document.body:

window.select3Widgets.destroy(el)       // um widget root
window.select3Widgets.destroyAll(scope) // scope/container

Uso (exemplos)

Exemplo completo com as 4 variações:

from django import forms

from django_select3.widgets import (
    Select3ComboboxAjaxWidget,
    Select3ComboboxWidget,
    Select3MultiSelectAjaxWidget,
    Select3MultiSelectWidget,
)


class ExampleForm(forms.Form):
    status = forms.ChoiceField(
        label="Status",
        choices=[("A", "Ativo"), ("I", "Inativo")],
        required=False,
        widget=Select3ComboboxWidget(
            placeholder="Selecione...",
            allow_clear=True,
        ),
    )

    state = forms.CharField(
        label="Estado",
        required=False,
        widget=Select3ComboboxAjaxWidget(
        ajax_url="myapp:states_autocomplete",  # ou "/api/states/"
            placeholder="Busque estado...",
            min_search_length=0,
            allow_clear=True,
            initial_label="",
        ),
    )

    city = forms.CharField(
        label="Cidade",
        required=False,
        widget=Select3ComboboxAjaxWidget(
        ajax_url="myapp:cities_autocomplete",  # ou "/api/cities/"
            placeholder="Busque cidade...",
            min_search_length=2,
            allow_clear=True,
            forward={"state": "state"},
            initial_label="",
        ),
    )

    tags = forms.MultipleChoiceField(
        label="Tags",
        choices=[("1", "VIP"), ("2", "Atraso")],
        required=False,
        widget=Select3MultiSelectWidget(
            placeholder="Selecione...",
            allow_clear=True,
        ),
    )

    services = forms.Field(
        label="Serviços",
        required=False,
        widget=Select3MultiSelectAjaxWidget(
        ajax_url="myapp:services_autocomplete",  # ou "/api/services/"
            placeholder="Digite para buscar...",
            min_search_length=2,
            forward={"city": "city"},
        ),
    )

“Cláusulas” (args) dos widgets

Esta seção documenta os argumentos suportados no construtor de cada widget (os “kwargs” que você passa no widget=...).

Args comuns (todos os widgets)

  • label: str | None

    • Controla o label exibido no próprio template do widget.
    • Se você já renderiza labels por fora (ou usa {{ form.as_p }}/as_crispy_field), pode deixar None.
  • placeholder: str | None

    • Texto do placeholder visível no input.
    • Se omitido, cada widget usa um default (“Selecione…”, “Busque…”, etc.).
  • allow_clear: bool

    • Mostra/esconde o botão de limpar (ícone de “x”).
    • Observação: hoje isso é puramente UX (front-end). Se o campo for obrigatório, considere allow_clear=False.
  • required: bool | None

    • Se None (padrão): o widget herda field.required.
    • Se True/False: força o estado “obrigatório” no template (exibe asterisco).
    • Observação: isso não faz validação. A validação de obrigatório continua sendo do Field.
  • attrs: dict | None

    • Atributos HTML padrão do Django Widget.
    • O id vindo de attrs (ou gerado pelo Django) é usado nos inputs/labels do widget.

Select3ComboboxWidget (single + estático)

Construtor: Select3ComboboxWidget(..., options_element_id=None)

  • options_element_id: str | None
    • Alternativa para passar as opções via um elemento no HTML.
    • Uso recomendado quando a lista de opções é grande, para evitar HTML com data-options-json="..." muito pesado/escapado.

Formato esperado no DOM:

<script type="application/json" id="my_options">
  [{"value": "A", "label": "Ativo"}, {"value": "I", "label": "Inativo"}]
</script>

Depois, no widget:

Select3ComboboxWidget(options_element_id="my_options")

Select3ComboboxAjaxWidget (single + AJAX)

Construtor: Select3ComboboxAjaxWidget(ajax_url=..., min_search_length=0, forward=None, initial_label=None)

  • ajax_url: str (obrigatório)

    • Pode ser:
      • um nome de URL (o widget tenta reverse(ajax_url))
      • uma URL absoluta/relativa (quando contém / ou começa com http:///https://)
  • min_search_length: int (padrão 0)

    • 0 significa “carregar/mostrar dropdown mesmo sem digitar”.
    • >0 significa “só buscar quando tiver pelo menos N caracteres”.
    • UX: quando q tem menos de N caracteres, o dropdown mostra a mensagem pedindo mais caracteres.
  • forward: dict[str, str] | None

    • Mapa {chave_no_forward: nome_do_input_no_form}.
    • O JS lê o valor atual via document.querySelector('input[name="<nome>"]').
    • Exemplo: forward={"state": "state"} envia { "state": <valor do input name=state> }.
  • initial_label: str | None

    • Usado para “modo edição”: quando existe um value inicial, você também precisa passar o texto (label) para exibir na UI.
    • Sem isso, o widget sabe o “id” (valor) mas não sabe o “text” (label) até você buscar.

Select3MultiSelectWidget (multi + estático)

Construtor: Select3MultiSelectWidget(...)

Detalhe importante: o widget posta múltiplos valores repetindo inputs hidden com o mesmo name. Por isso, ele implementa value_from_datadict() usando QueryDict.getlist(name).

Isso significa que ele funciona bem com MultipleChoiceField, ModelMultipleChoiceField, etc.

Select3MultiSelectAjaxWidget (multi + AJAX)

Construtor: Select3MultiSelectAjaxWidget(ajax_url=..., min_search_length=2, forward=None)

Args:

  • ajax_url: str (obrigatório): mesmo comportamento do combobox AJAX.
  • min_search_length: int (padrão 2): mesma regra do combobox AJAX.
  • forward: dict[str, str] | None: mesma regra do combobox AJAX.

Observação: como as opções vêm dinamicamente do endpoint, é comum usar esse widget com forms.Field ou com uma limpeza/validação customizada no servidor. Se você usar MultipleChoiceField, precisa garantir que os choices válidos existam no momento da validação.

Contrato do endpoint AJAX

O JS envia requisições GET com estes parâmetros:

  • q: string digitada
  • page: número da página quando há paginação/infinite scroll
  • forward: JSON url-encoded (opcional)

Resposta esperada (contrato JSON):

{
  "results": [
    {"id": "BR", "text": "Brasil"}
  ],
  "pagination": {
    "more": false
  }
}

Para cada item em results, o JS usa id/text e também aceita value/label como alternativa.

Também é aceito retornar uma lista direta em vez de um objeto, por exemplo:

[
  {"id": "BR", "text": "Brasil"}
]

Paginação (opcional):

  • pagination.more: boolean
  • next_page: number | null
  • next: string | null (URL pronta para a próxima página)
  • page + total_pages
  • count + page + page_size

Se nenhum metadado de paginação vier, o JS usa um fallback por tamanho:

  • Assume page_size=20 (ou use page_size no JSON, se você retornar)
  • Continua buscando enquanto cada página vier “cheia”
  • Para quando results vier vazio ou com menos itens que o page_size

Esse fallback pode causar 1 request extra no final quando o total é múltiplo exato do page_size.

Exemplo de view simples:

from django.http import JsonResponse


def my_autocomplete(request):
    q = (request.GET.get("q") or "").strip()
    forward_raw = request.GET.get("forward") or ""
    # forward_raw é JSON (string); se precisar, faça json.loads(forward_raw)

    results = []
    if q:
        results = [
            {"id": "1", "text": f"Resultado para: {q}"},
        ]

    return JsonResponse({"results": results})

Forward (dependências/cascata)

O forward existe para encadear selects (ex.: País → Estado → Cidade).

Como funciona:

  • Você configura forward={"state": "state"} no widget “filho” (Cidade).
  • O JS inclui forward na querystring.
  • Seu endpoint usa isso para filtrar os resultados.

Múltiplos valores no “pai”:

  • Se o “pai” tiver vários inputs com o mesmo name (caso comum de multi), o forward coleta todos os valores. Quando há mais de um, o valor enviado para aquela chave vira um array JSON; com um único valor, vai como string. Trate os dois formatos no seu endpoint.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_select3-0.1.0.tar.gz (26.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_select3-0.1.0-py3-none-any.whl (25.0 kB view details)

Uploaded Python 3

File details

Details for the file django_select3-0.1.0.tar.gz.

File metadata

  • Download URL: django_select3-0.1.0.tar.gz
  • Upload date:
  • Size: 26.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for django_select3-0.1.0.tar.gz
Algorithm Hash digest
SHA256 b90815dcb3bc8c240b17b15b6bc851c964fd7169217adf5ddd39fcbfa47a1da8
MD5 aac7ccbff0c86bce616b521240cfda8c
BLAKE2b-256 8e3c2b50ab8bddded77cfa54b24222f15ebcd8df5f2dc4d5d112d92df9514ec3

See more details on using hashes here.

File details

Details for the file django_select3-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: django_select3-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 25.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for django_select3-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ed60542cb3ee03b2b42c1cc316172fe7486547062d114384be5440b972a1b753
MD5 3f496a5f2ade104514e066e9025ad517
BLAKE2b-256 ad77a25910ab675ffe22dbcd64ddf9d4b8c992dfe1272e1ba247fb50dbdd898d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page