Advanced Features¶
As your project grows, you may need more than just basic autocomplete functionality. django_tomselect includes several advanced features that let you dynamically update dropdowns based on user selections, apply exclusion rules, handle large datasets efficiently, integrate with HTMX for seamless server-driven interactions, and customize the search logic to fit your needs. Additionally, you can override core view and widget methods to fine-tune how data is fetched and presented.
Visualizing Request Flow and Data Processing¶
sequenceDiagram
participant Client
participant Widget
participant View
participant Cache
participant Database
Client->>Widget: User interacts with select
Widget->>View: Send autocomplete request
alt Cache enabled
View->>Cache: Check permissions
Cache-->>View: Return cached permissions
else Cache disabled
View->>Database: Check permissions
Database-->>View: Return permissions
end
View->>Database: Query filtered data
Database-->>View: Return results
View->>View: Process results
View-->>Widget: Return JSON response
Widget-->>Client: Update dropdown options
note over Widget,View: Permissions are cached<br/>if caching is enabled
note over View,Database: Results are paginated<br/>and filtered based on search
Dependent/Chained Fields¶
One common pattern is to make the options in one dropdown field depend on the selected value of another field. For example, after a user chooses a Category, you might need to restrict available Subcategories to those related to that Category.
To achieve this, django_tomselect supports dependent (chained) fields. When setting up your widget configuration, you can specify a filter_by attribute, instructing the field to refresh its options whenever the parent field changes.
from django import forms
from django_tomselect import TomSelectConfig, TomSelectModelChoiceField
class CategoryForm(forms.Form):
category = TomSelectModelChoiceField(
config=TomSelectConfig(
url="category-autocomplete",
value_field="id",
label_field="name",
),
)
subcategory = TomSelectModelChoiceField(
config=TomSelectConfig(
url="subcategory-autocomplete",
value_field="id",
label_field="name",
# Instructs the subcategory field to dynamically filter by the selected category
filter_by=("category", "category_id"),
),
)
Multiple Field Filters¶
You can filter by multiple fields using a list of tuples. All conditions are combined (aka: AND):
from django import forms
from django_tomselect import (
TomSelectConfig,
TomSelectChoiceField,
TomSelectModelChoiceField,
TomSelectModelMultipleChoiceField,
)
class ArticleFilterForm(forms.Form):
magazine = TomSelectModelChoiceField(
config=TomSelectConfig(url="autocomplete-magazine"),
)
status = TomSelectChoiceField(
config=TomSelectConfig(url="autocomplete-article-status"),
)
# Filter articles by BOTH magazine AND status
articles = TomSelectModelMultipleChoiceField(
config=TomSelectConfig(
url="autocomplete-article",
filter_by=[
("magazine", "magazine_id"), # Filter by selected magazine
("status", "status"), # AND by selected status
],
),
)
See the Multiple Filter-By example for complete demonstration.
Formset Support¶
Both filter_by and exclude_by work within Django formsets. Each formset row operates independently - selecting a value in row 1 only affects dependent fields in row 1, not other rows. The widget automatically handles form prefixes (e.g., myformset-0-magazine, myformset-1-magazine).
from django.forms import formset_factory
class MagazineEditionForm(forms.Form):
magazine = TomSelectModelChoiceField(
config=TomSelectConfig(url="autocomplete-magazine"),
)
edition = TomSelectModelChoiceField(
config=TomSelectConfig(
url="autocomplete-edition",
filter_by=("magazine", "magazine_id"), # Works per-row in formsets
),
)
MagazineEditionFormset = formset_factory(MagazineEditionForm, extra=2)
See the Formset with filter_by example for complete demonstration including dynamic row addition.
For nested formsets, where an inner row needs to filter by a value on the outer parent row, use FilterSpec with levels_up:
from django_tomselect import TomSelectConfig, FilterSpec, TomSelectModelChoiceField
class LineItemForm(forms.Form):
product = TomSelectModelChoiceField(
config=TomSelectConfig(
url="autocomplete-product",
# Inner row, but pull "customer" from the parent Order row
filter_by=FilterSpec(
source="customer", lookup="id",
source_type="field", levels_up=1,
),
),
)
levels_up=0 (the default) keeps the same-row behavior. See FilterSpec for full details.
Constant Value Filters¶
Use the Const helper to filter by a constant value that doesn’t come from a form field. This is useful for enforcing business rules in the UI:
from django import forms
from django_tomselect import (
TomSelectConfig,
Const,
TomSelectModelChoiceField,
TomSelectModelMultipleChoiceField,
)
class PublishedArticleForm(forms.Form):
magazine = TomSelectModelChoiceField(
config=TomSelectConfig(url="autocomplete-magazine"),
required=False,
)
# Articles always filtered to published status, optionally by magazine
published_articles = TomSelectModelMultipleChoiceField(
config=TomSelectConfig(
url="autocomplete-article",
filter_by=[
("magazine", "magazine_id"), # Optional magazine filter
Const("published", "status"), # Always filter to published
],
),
)
Common use cases for constant filters:
Only show published content:
Const("published", "status")Only show active items:
Const("true", "is_active")Filter by current year:
Const("2024", "year")Restrict to specific primary keys via
__in:Const([11, 13], "id__in")Restrict to a numeric range via
__range:Const([2020, 2024], "year__range")
For the full Const signature, list/tuple handling for __in/__range lookups, and the rules around comma-joined transport, see the Const Helper reference.
See the Constant Filter-By example for complete demonstration.
Field Exclusions¶
Beyond filtering, you might need to exclude certain options dynamically. For example, if you have a form with a primary_author field and a contributing_authors field, you may want to prevent the primary author from appearing in the contributing authors list.
Using exclude_by¶
django_tomselect lets you specify exclude_by logic similar to how filter_by works. In your widget configuration, you can set exclude_by to exclude options based on the value of another field in the same form.
class ArticleForm(forms.Form):
primary_author = TomSelectModelChoiceField(
config=TomSelectConfig(
url="author-autocomplete",
value_field="id",
label_field="name",
)
)
contributing_authors = TomSelectModelMultipleChoiceField(
config=TomSelectConfig(
url="author-autocomplete",
value_field="id",
label_field="name",
exclude_by=("primary_author", "id"), # Excludes whoever is chosen as the primary author
)
)
This ensures that once a primary author is selected, they disappear from the options in the contributing authors field.
filter_by / exclude_by with Iterables Fields¶
filter_by and exclude_by also work for iterables-backed fields (TomSelectChoiceField /
TomSelectMultipleChoiceField served by an AutocompleteIterablesView), so choice/enum
dropdowns can be dependent or exclude already-chosen values.
The difference: an iterable item is just {"value", "label"} with no model behind it, so the
lookup must target the value or label key rather than a model field.
from django import forms
from django_tomselect import (
TomSelectConfig,
TomSelectChoiceField,
TomSelectModelChoiceField,
)
class ArticleForm(forms.Form):
# Parent field (its value drives the dependent dropdown below)
category = TomSelectModelChoiceField(
config=TomSelectConfig(url="category-autocomplete", value_field="id", label_field="name"),
)
# Iterables field whose options are narrowed by the selected category's value
status = TomSelectChoiceField(
config=TomSelectConfig(
url="status-autocomplete", # an AutocompleteIterablesView
filter_by=("category", "value"), # keep items where item value == category's value
)
)
Supported lookups for iterables: exact (default), iexact, in, contains, icontains,
startswith, istartswith, endswith, iendswith (e.g. ("category", "value__in") or
("other_field", "label__icontains")). Multiple filter_by entries are AND-ed and multiple
exclude_by entries drop the union, just like model views. Invalid configuration (a key other
than value/label, an unsupported lookup, or an empty/whitespace value) fails closed and
returns no results.
Note
A model-style lookup such as ("category", "category_id") targets category_id, which does not
exist on an iterable item, so it returns an empty list. For iterables, always target value or
label. See the AutocompleteIterablesView docs for details.
Pagination Handling¶
For large datasets, loading all results at once can be inefficient. django_tomselect implements pagination and supports virtual scrolling to load results incrementally as the user scrolls the dropdown.
In your AutocompleteModelView, set page_size to determine how many items load per request:
from django_tomselect.autocompletes import AutocompleteModelView
from .models import Author
class AuthorAutocompleteView(AutocompleteModelView):
model = Author
search_lookups = ["name__icontains"]
page_size = 20
When the user scrolls through the dropdown, django_tomselect will automatically fetch the next page of results. The JSON response should include has_more and next_page parameters to indicate whether more results are available. The widget will keep loading pages until no more results remain.
Custom Search Implementation¶
If your filtering and searching needs extend beyond basic lookups, you can override the search() method in your AutocompleteModelView to implement custom logic.
from django.db.models import Q
from django_tomselect.autocompletes import AutocompleteModelView
from .models import Category
class CategoryAutocompleteView(AutocompleteModelView):
model = Category
search_lookups = ["name__icontains", "description__icontains"]
def search(self, queryset, query):
if query:
# Combine multiple lookups with Q objects
return queryset.filter(
Q(name__icontains=query) | Q(description__icontains=query)
)
return queryset
By extending search(), you gain fine-grained control over how queries are processed, letting you implement complex logic such as multi-field search, fuzzy matching, or multi-level relationship lookups.
Overriding Methods in Autocomplete Views¶
AutocompleteModelView provides several hook methods that you can override to customize behavior at every stage of processing a request:
get_queryset(self): Define the base queryset. Addprefetch_relatedorannotatecalls for efficiency.apply_filters(self, queryset): Customize how filters and exclusions are applied based onfilter_byandexclude_by.search(self, queryset, query): Implement complex search logic.order_queryset(self, queryset): Change the ordering of results.prepare_results(self, results): Modify the output dictionary before sending JSON to the frontend.hook_queryset(self, queryset): Manipulate the queryset before any filters or searches are applied.hook_prepare_results(self, results): Adjust results after all other processing is complete.
For example, to annotate a queryset and then filter by a calculated field:
from django.db.models import F
class CustomAutocompleteView(AutocompleteModelView):
model = Category
def hook_queryset(self, queryset):
return queryset.annotate(is_special=(F('some_field') > 5))
def apply_filters(self, queryset):
# Only return "special" categories
return queryset.filter(is_special=True)