Filtering and Sorting Guide¶
This guide covers using URL-based filtering and sorting with HtmxListView.
Basic Filtering¶
Filters are expressed as query parameters using the format field_name.filter_type=value:
/articles/?status.eq=published
Building a Filter Form¶
Create a simple filter form to make filtering discoverable:
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<form method="get" class="mb-4">
<div class="row">
<div class="col-md-4">
<select name="status.eq" class="form-select">
<option value="">-- Select Status --</option>
<option value="draft" {% if 'draft' in filter_query %}selected{% endif %}>Draft</option>
<option value="published" {% if 'published' in filter_query %}selected{% endif %}>Published</option>
</select>
</div>
<div class="col-md-4">
<input type="text" name="title.ilike" placeholder="Search title..." class="form-control"
value="{% if 'title.ilike' in filter_query %}{{ request.GET.title__ilike }}{% endif %}">
</div>
<div class="col-md-4">
<button type="submit" class="btn btn-primary w-100">Filter</button>
</div>
</div>
</form>
<!-- Table with results -->
<div id="article-table" hx-trigger="articleCreated from:body">
<c-tables.htmx_table class="table table-striped" />
</div>
</div>
{% endblock %}
Common Filter Patterns¶
Exact Match¶
Find articles with exactly “published” status:
/articles/?status.eq=published
Partial Text Match¶
Find articles containing “django” in title:
/articles/?title.ilike=django
Numeric Comparison¶
Find articles with at least 100 views:
/articles/?views.gte=100
Date Range¶
Find articles from 2024:
/articles/?created_at.rng=['2024-01-01','2024-12-31']
Multiple Conditions¶
Combine filters with &:
/articles/?status.eq=published&views.gte=100&author.eq=alice
All conditions are AND’ed together.
Filter Reference¶
Text Filters¶
Parameter |
Operator |
Example |
|---|---|---|
|
Exact match (case-sensitive) |
|
|
Exact match (case-insensitive) |
|
|
Contains (case-sensitive) |
|
|
Contains (case-insensitive) |
|
|
Starts with (case-sensitive) |
|
|
Starts with (case-insensitive) |
|
|
Ends with (case-sensitive) |
|
|
Ends with (case-insensitive) |
|
Numeric and Date Filters¶
Parameter |
Operator |
Example |
|---|---|---|
|
Greater than |
|
|
Greater than or equal |
|
|
Less than |
|
|
Less than or equal |
|
|
Range (start, end) |
|
Special Filters¶
Parameter |
Operator |
Example |
|---|---|---|
|
In list |
|
|
Is null |
|
|
Full-text search |
|
Sorting¶
Sort with the order_by parameter. Use - prefix for descending:
# Ascending sort
/articles/?order_by=created_at
# Descending sort
/articles/?order_by=-created_at
# Combined with filters
/articles/?status.eq=published&order_by=-views
Combining Filters and Sorting¶
Use both together for flexible querying:
/articles/?status.eq=published&author.eq=alice&views.gte=100&order_by=-created_at&page=2
This query:
Filters to published articles
By author “alice”
With at least 100 views
Sorted newest first
Shows page 2
Advanced: Building Dynamic Filter URLs¶
In templates, use the context variables to build filter URLs:
<!-- Add filter while preserving existing filters -->
<a href="?{{ query_params }}&status.eq=published">Published Only</a>
<!-- Sort by title -->
<a href="?{{ filter_query }}&order_by=title">Sort by Title</a>
<!-- Toggle sort direction -->
{% if order_by == 'created_at' %}
<a href="?{{ filter_query }}&order_by=-created_at">Oldest First</a>
{% else %}
<a href="?{{ filter_query }}&order_by=created_at">Newest First</a>
{% endif %}
The context variables help build URLs that preserve or modify filters correctly.
Advanced: Custom Filter Widgets¶
Warning
AI generated content, I have not yet tested these samples
Create reusable filter components:
{# Filter widget #}
{% with param_name=param_name value=value options=options %}
<div class="filter-widget">
<label>{{ label }}</label>
<select name="{{ param_name }}" class="form-select" hx-get="." hx-target="#results">
<option value="">-- Any --</option>
{% for opt_value, opt_label in options %}
<option value="{{ opt_value }}" {% if opt_value == value %}selected{% endif %}>
{{ opt_label }}
</option>
{% endfor %}
</select>
</div>
{% endwith %}
Use in your filter form:
{% include "filters/select.html" with param_name="status.eq" value=status label="Status" options=status_choices %}
Advanced: Range Picker¶
Warning
AI generated content, I have not yet tested these samples
For date ranges, use a JavaScript date picker:
<div class="mb-3">
<label>Created Date Range</label>
<input type="date" name="created_at_start" class="form-control" id="start-date">
<input type="date" name="created_at_end" class="form-control" id="end-date">
</div>
<script>
document.querySelector('form').addEventListener('submit', function(e) {
const start = document.querySelector('#start-date').value;
const end = document.querySelector('#end-date').value;
if (start && end) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'created_at.rng';
input.value = `['${start}','${end}']`;
this.appendChild(input);
}
});
</script>
Best Practices¶
Only expose necessary fields – Use the
fieldsattribute to whitelist filterable columnsProvide default filters – Show relevant data without manual filtering
Test edge cases – Empty results, invalid dates, etc.
Make filters discoverable – Use forms instead of requiring users to know URL syntax
Use meaningful parameter names – Match field names when possible
Preserve filters in pagination – Use context variables to build URLs
Consider performance – Complex filters on large datasets may be slow