Skip to content

mupisystems/django_select3

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages