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 (modais / swaps / HTMX)

Nota: o Select3 não depende de HTMX. As requisições de busca dos widgets AJAX usam a Fetch API (AJAX) nativa do browser — não há nenhuma dependência de HTMX. Esta seção trata de compatibilidade: os widgets funcionam bem quando o seu app injeta HTML dinamicamente, seja via HTMX, modais ou swaps de qualquer biblioteca.

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, ou faz swap por qualquer outra ferramenta), 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.querySelectorAll('[name="<nome>"]') — pega todos os elementos com aquele name (não só input), coletando todos os valores.
    • 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.1.tar.gz (27.4 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.1-py3-none-any.whl (25.2 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: django_select3-0.1.1.tar.gz
  • Upload date:
  • Size: 27.4 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.1.tar.gz
Algorithm Hash digest
SHA256 2f0639a191952e589683db1d4514603b886147b23a47ad6d6c3392901dd69112
MD5 127ac75c0ce20496d1f76c78f3d37edd
BLAKE2b-256 d9c49c373d062900be2abcfb2a50dbb5752a437ea5179eff6e1557b65345cb6a

See more details on using hashes here.

File details

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

File metadata

  • Download URL: django_select3-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 25.2 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 687d8bd6bf0bed970abf5e51a066199b3b8ac4f8937fe45d0192097e27a0d23b
MD5 f6dec8b127f071db35261c9f91b96e63
BLAKE2b-256 e18b0b8028f67a0677fc51dbaf4a51d55376342453b1d9f5698d62125b429b43

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