SearchableDropdown

Uma biblioteca Django completa para criar dropdowns pesquisáveis e customizáveis com funcionalidades avançadas.
Sobre a Biblioteca
O SearchableDropdown é uma biblioteca Django que oferece componentes de dropdown avançados com funcionalidades de busca em tempo real, integração nativa com formulários Django e suporte a múltiplas seleções.
Características Principais
- Dropdowns Pesquisáveis: Busca em tempo real nas opções
- Integração Django: Widgets e campos de formulário nativos
- Suporte AJAX: Busca dinâmica em modelos Django
- Múltipla Seleção: Suporte a seleção de múltiplas opções
- Informações Adicionais: Exibição de dados extras sobre opções selecionadas
- Customizável: Temas e configurações flexíveis
- Responsivo: Funciona em dispositivos móveis
- Acessível: Suporte a navegação por teclado e leitores de tela
Status do Projeto
- Versão: 1.0.0 (Estável)
- Testes: 110 testes passando (100% cobertura)
- Compatibilidade: Django 2.2+ e Python 3.7+
- Documentação: Completa com exemplos práticos
- Licença: MIT (Open Source)
- PyPI: Disponível para instalação via pip
Instalação
Opção 1: Instalar via pip (Recomendado para produção)
pip install django_searchable_dropdown
E então adicionar ao INSTALLED_APPS:
INSTALLED_APPS = [
'recoveredperispirit.django.django_searchable_dropdown',
]
Opção 2: Integrar em Projeto Existente
1. Adicionar ao projeto
cp -r recoveredperispirit/django/django_searchable_dropdown/ /path/to/your/django/project/
2. Configurar settings.py
INSTALLED_APPS = [
'recoveredperispirit.django.django_searchable_dropdown',
]
SEARCHABLE_DROPDOWN_CONFIG = {
'default_placeholder': 'Selecione uma opção',
'default_search_placeholder': 'Digite para buscar...',
'default_no_results_text': 'Nenhum resultado encontrado',
'ajax_timeout': 5000,
'min_search_length': 1,
'max_results': 50,
}
3. Coletar arquivos estáticos
python manage.py collectstatic
Guia de Uso - Widgets Disponíveis
1. SearchableDropdownWidget (Básico)
O widget básico para dropdowns pesquisáveis com opções estáticas ou de modelos Django.
Exemplo Básico
from recoveredperispirit.django.django_searchable_dropdown.widgets import SearchableDropdownWidget
from django import forms
class CategoriaForm(forms.Form):
categoria = forms.ModelChoiceField(
queryset=Categoria.objects.all(),
widget=SearchableDropdownWidget(
placeholder="Selecione uma categoria",
search_placeholder="Digite para buscar categorias...",
dropdown_type="categoria",
min_search_length=1,
max_results=20
),
required=False
)
Como Funciona
- Renderização: O widget cria um dropdown customizado com campo de busca
- Busca: Usuário digita no campo de busca e as opções são filtradas em tempo real
- Seleção: Usuário clica em uma opção para selecioná-la
- Validação: O valor é validado pelo Django normalmente
Resultado Visual
┌─────────────────────────────────────┐
│ Selecione uma categoria ▼ │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Digite para buscar categorias... │
├─────────────────────────────────────┤
│ ✓ Tecnologia │
│ Esportes │
│ Música │
│ Livros │
└─────────────────────────────────────┘
2. SearchableDropdownMultipleWidget (Múltipla Seleção)
Widget para selecionar múltiplas opções simultaneamente.
Exemplo de Uso
from recoveredperispirit.django.django_searchable_dropdown.widgets import SearchableDropdownMultipleWidget
class ProdutoForm(forms.Form):
tags = forms.ModelMultipleChoiceField(
queryset=Tag.objects.all(),
widget=SearchableDropdownMultipleWidget(
placeholder="Selecione tags (máximo 5)",
search_placeholder="Digite para buscar tags...",
dropdown_type="tags",
max_selections=5,
allow_clear=True
),
required=False
)
Como Funciona
- Múltipla Seleção: Usuário pode selecionar várias opções
- Contador: Mostra quantas opções foram selecionadas
- Limite: Pode definir um número máximo de seleções
- Remoção: Opções selecionadas podem ser removidas individualmente
Resultado Visual
┌─────────────────────────────────────┐
│ ✓ Tecnologia, Esportes (2/5) ▼ │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Digite para buscar tags... │
├─────────────────────────────────────┤
│ ✓ Tecnologia [×] │
│ ✓ Esportes [×] │
│ Música │
│ Livros │
└─────────────────────────────────────┘
3. SearchableDropdownAjaxWidget (Busca AJAX)
Widget para busca dinâmica via AJAX em grandes datasets.
Exemplo de Uso
from recoveredperispirit.django.django_searchable_dropdown.widgets import SearchableDropdownAjaxWidget
class ClienteForm(forms.Form):
cliente = forms.ModelChoiceField(
queryset=Cliente.objects.none(),
widget=SearchableDropdownAjaxWidget(
placeholder="Digite para buscar clientes...",
search_placeholder="Nome ou email do cliente...",
dropdown_type="cliente_ajax",
ajax_url="/api/clientes/search/",
min_search_length=2,
max_results=20,
delay=300,
allow_clear=True
),
required=False
)
Como Funciona
- Busca Dinâmica: Dados são carregados via AJAX conforme o usuário digita
- Delay: Aguarda o usuário parar de digitar antes de fazer a requisição
- Filtros: Aplica filtros no servidor para melhor performance
- Cache: Pode implementar cache para melhorar performance
Resultado Visual
┌─────────────────────────────────────┐
│ Digite para buscar clientes... ▼ │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ joão... │
├─────────────────────────────────────┤
│ Buscando... │
├─────────────────────────────────────┤
│ João Silva (joao@email.com) │
│ João Santos (joao.s@email.com) │
│ João Oliveira (joao.o@email.com) │
└─────────────────────────────────────┘
API Endpoint Necessário
from django.http import JsonResponse
from django.db.models import Q
def api_clientes_search(request):
query = request.GET.get('q', '')
if len(query) < 2:
return JsonResponse({'results': []})
clientes = Cliente.objects.filter(
Q(nome__icontains=query) | Q(email__icontains=query)
)[:20]
results = [
{
'id': cliente.id,
'text': f"{cliente.nome} ({cliente.email})"
}
for cliente in clientes
]
return JsonResponse({'results': results})
4. SearchableDropdownWithInfoWidget (Com Informações)
Widget que mostra informações detalhadas sobre a opção selecionada.
Exemplo de Uso
from recoveredperispirit.django.django_searchable_dropdown.widgets import SearchableDropdownWithInfoWidget
class ProdutoDetalhadoForm(forms.Form):
produto = forms.ModelChoiceField(
queryset=Produto.objects.all(),
widget=SearchableDropdownWithInfoWidget(
placeholder="Selecione um produto para ver detalhes",
search_placeholder="Digite para buscar produtos...",
dropdown_type="produto_info",
info_url="/api/produtos/info/",
info_container_id="produto-info-container"
),
required=False
)
Como Funciona
- Seleção: Usuário seleciona uma opção no dropdown
- Carregamento: Informações detalhadas são carregadas via AJAX
- Exibição: Dados são exibidos em um container separado
- Atualização: Informações são atualizadas a cada nova seleção
Resultado Visual
┌─────────────────────────────────────┐
│ iPhone 13 Pro ▼ │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Digite para buscar produtos... │
├─────────────────────────────────────┤
│ iPhone 13 Pro │
│ iPhone 12 │
│ Samsung Galaxy S21 │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Informações do Produto │
├─────────────────────────────────────┤
│ Nome: iPhone 13 Pro │
│ Código: PROD001 │
│ Preço: R$ 8.999,00 │
│ Estoque: 15 unidades │
│ Categoria: Smartphones │
│ Marca: Apple │
│ Descrição: iPhone 13 Pro 256GB... │
└─────────────────────────────────────┘
API Endpoint Necessário
def api_produtos_info(request):
produto_id = request.GET.get('id')
if not produto_id:
return JsonResponse({'error': 'ID do produto não fornecido'}, status=400)
try:
produto = Produto.objects.get(id=produto_id)
info = {
'id': produto.id,
'nome': produto.nome,
'codigo': produto.codigo,
'preco': str(produto.preco),
'estoque': produto.estoque,
'categoria': produto.categoria.nome,
'marca': produto.marca.nome,
'descricao': produto.descricao or 'Sem descrição'
}
return JsonResponse(info)
except Produto.DoesNotExist:
return JsonResponse({'error': 'Produto não encontrado'}, status=404)
Template HTML Necessário
<div class="row">
<div class="col-md-8">
{{ form.produto }}
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h6>Informações do Produto</h6>
</div>
<div class="card-body" id="produto-info-container">
<p class="text-muted">Selecione um produto para ver as informações</p>
</div>
</div>
</div>
</div>
Customização Avançada
Configurações por Tipo de Dropdown
from recoveredperispirit.django.django_searchable_dropdown.utils import dropdown_config
dropdown_config.register_type('activity', {
'placeholder': 'Selecione uma atividade',
'search_placeholder': 'Digite o nome da atividade...',
'no_results_text': 'Nenhuma atividade encontrada',
'min_search_length': 2,
'max_results': 20,
'allow_clear': True,
'allow_create': False,
})
class ActivityForm(forms.Form):
activity = forms.ModelChoiceField(
queryset=Activity.objects.all(),
widget=SearchableDropdownWidget(dropdown_type='activity')
)
Estilos CSS Customizados
.searchable-dropdown.custom-theme {
--dropdown-bg: #ffffff;
--dropdown-border: #e0e0e0;
--dropdown-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--option-hover-bg: #f8f9fa;
--option-selected-bg: #007bff;
--option-selected-color: #ffffff;
--search-input-bg: #f8f9fa;
--search-input-border: #dee2e6;
}
.searchable-dropdown[data-type="produto"] {
--option-selected-bg: #28a745;
}
.searchable-dropdown[data-type="cliente"] {
--option-selected-bg: #17a2b8;
}
JavaScript Customizado
document.addEventListener('dropdown:change', function(e) {
const dropdown = e.target;
const value = e.detail.value;
const text = e.detail.text;
console.log('Dropdown alterado:', { value, text });
if (dropdown.getAttribute('data-type') === 'produto') {
updateProductInfo(value);
}
});
const customDropdown = new SearchableDropdown(element, {
placeholder: 'Selecione uma opção',
searchPlaceholder: 'Digite para buscar...',
onSelect: function(value, text) {
console.log('Opção selecionada:', value, text);
},
onSearch: function(query) {
console.log('Buscando:', query);
}
});
Exemplos Práticos Completos
Exemplo 1: Sistema de Agendamento
class Activity(models.Model):
name = models.CharField(max_length=100)
description = models.TextField()
duration = models.IntegerField(help_text="Duração em minutos")
max_participants = models.IntegerField()
def __str__(self):
return self.name
class Schedule(models.Model):
activity = models.ForeignKey(Activity, on_delete=models.CASCADE)
date = models.DateField()
time = models.TimeField()
participants = models.ManyToManyField('User', blank=True)
def __str__(self):
return f"{self.activity.name} - {self.date} {self.time}"
class ScheduleForm(forms.ModelForm):
activity = forms.ModelChoiceField(
queryset=Activity.objects.all(),
widget=SearchableDropdownWithInfoWidget(
dropdown_type='activity',
info_url='/api/activities/info/',
info_container_id='activity-info',
placeholder='Selecione uma atividade'
)
)
participants = forms.ModelMultipleChoiceField(
queryset=User.objects.all(),
widget=SearchableDropdownMultipleWidget(
dropdown_type='users',
placeholder='Selecione participantes',
max_selections=10
),
required=False
)
class Meta:
model = Schedule
fields = ['activity', 'date', 'time', 'participants']
def api_activities_info(request):
activity_id = request.GET.get('id')
try:
activity = Activity.objects.get(id=activity_id)
return JsonResponse({
'name': activity.name,
'description': activity.description,
'duration': f"{activity.duration} minutos",
'max_participants': activity.max_participants
})
except Activity.DoesNotExist:
return JsonResponse({'error': 'Atividade não encontrada'}, status=404)
Exemplo 2: E-commerce com Produtos
class Category(models.Model):
name = models.CharField(max_length=100)
description = models.TextField()
def __str__(self):
return self.name
class Product(models.Model):
name = models.CharField(max_length=200)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.IntegerField()
description = models.TextField()
tags = models.ManyToManyField('Tag', blank=True)
def __str__(self):
return self.name
class ProductSearchForm(forms.Form):
category = forms.ModelChoiceField(
queryset=Category.objects.all(),
widget=SearchableDropdownWidget(
dropdown_type='category',
placeholder='Todas as categorias',
allow_clear=True
),
required=False
)
products = forms.ModelMultipleChoiceField(
queryset=Product.objects.all(),
widget=SearchableDropdownMultipleWidget(
dropdown_type='products',
placeholder='Selecione produtos para comparar',
max_selections=5
),
required=False
)
featured_product = forms.ModelChoiceField(
queryset=Product.objects.all(),
widget=SearchableDropdownWithInfoWidget(
dropdown_type='product_info',
info_url='/api/products/info/',
info_container_id='product-details',
placeholder='Selecione um produto para ver detalhes'
),
required=False
)
Configurações Avançadas
Configurações Globais
SEARCHABLE_DROPDOWN_CONFIG = {
'default_placeholder': 'Selecione uma opção',
'default_search_placeholder': 'Digite para buscar...',
'default_no_results_text': 'Nenhum resultado encontrado',
'ajax_timeout': 5000,
'min_search_length': 1,
'max_results': 50,
'allow_clear': True,
'allow_create': False,
'delay': 300,
'aria_label': 'Dropdown pesquisável',
'aria_describedby': 'dropdown-help',
}
Configurações por Ambiente
SEARCHABLE_DROPDOWN_CONFIG = {
'debug': True,
'ajax_timeout': 10000,
'delay': 500,
}
SEARCHABLE_DROPDOWN_CONFIG = {
'debug': False,
'ajax_timeout': 3000,
'delay': 200,
'cache_results': True,
}
Deploy e Performance
Otimizações para Produção
SEARCHABLE_DROPDOWN_CONFIG = {
'cache_results': True,
'cache_timeout': 300,
'ajax_timeout': 3000,
'max_results': 20,
'min_search_length': 2,
}
from django.core.cache import cache
def api_search_view(request):
query = request.GET.get('q', '')
cache_key = f"search_{query}"
cached_results = cache.get(cache_key)
if cached_results:
return JsonResponse(cached_results)
results = perform_search(query)
cache.set(cache_key, results, 300)
return JsonResponse(results)
Troubleshooting
Problemas Comuns e Soluções
1. Dropdown não abre
console.log('SearchableDropdown:', typeof SearchableDropdown);
console.log('SearchableDropdownUtils:', typeof SearchableDropdownUtils);
const dropdown = document.querySelector('.searchable-dropdown');
console.log('Dropdown element:', dropdown);
2. Busca AJAX não funciona
def api_search_view(request):
try:
return JsonResponse({'results': results})
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
3. Estilos não aplicados
.searchable-dropdown {
display: block !important;
position: relative !important;
z-index: 1000 !important;
}
Debug Avançado
SearchableDropdownUtils.setDebugMode(true);
const dropdowns = SearchableDropdownUtils.getInitializedDropdowns();
console.log('Dropdowns inicializados:', dropdowns);
const configs = SearchableDropdownUtils.getConfigs();
console.log('Configurações:', configs);
Testes e Qualidade
Executando Testes
cd recoveredperispirit/django/django_searchable_dropdown
python -m pytest tests/ -v
python -m pytest tests/ --cov=. --cov-report=html
cd test_app
python manage.py test
Status dos Testes
- 110 testes passando (100% de cobertura)
- Formulários: 25 testes
- Widgets: 25 testes
- Utilitários: 35 testes
- Integração: 6 testes
Demonstração Completa
Para ver todos os widgets em ação, execute o test_app:
cd test_app
python manage.py runserver
Acesse: http://localhost:8000
Páginas de Demonstração
- Página Inicial: Visão geral de todas as funcionalidades
- Produtos: Dropdowns básicos e com filtros
- Clientes: Dropdowns com AJAX
- Pedidos: Dropdowns múltiplos
- AJAX Demo: Demonstração completa de busca AJAX
- With Info Demo: Demonstração de dropdowns com informações
Licença
Esta biblioteca é distribuída sob a licença MIT.
Contribuição
Para contribuir:
- Fork o repositório
- Crie uma branch para sua feature
- Faça commit das suas mudanças
- Push para a branch
- Abra um Pull Request
Suporte
Para suporte e dúvidas:
- Teste primeiro: Use o test_app para verificar se o problema é específico do seu projeto
- Consulte a documentação: Esta documentação e os exemplos no test_app
- Abra uma issue: No GitHub com detalhes do problema e ambiente
- Verifique os logs: Use o modo debug para identificar problemas