# 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
```{mermaid}
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
if caching is enabled
note over View,Database: Results are paginated
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.
```python
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):
```python
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_app/multiple_filter_by.md) 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`).
```python
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_app/formset_filter_by.md) 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`:
```python
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](../api/config.md#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:
```python
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](../api/config.md#const-helper).
See the [Constant Filter-By](../example_app/constant_filter_by.md) 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.
```python
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.
```python
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](../api/autocomplete_views.md) 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:
```python
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.
```python
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. Add `prefetch_related` or `annotate` calls for efficiency.
- `apply_filters(self, queryset)`: Customize how filters and exclusions are applied based on `filter_by` and `exclude_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:
```python
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)
```