Skip to content

Conversation

@fd-oncodna
Copy link

@fd-oncodna fd-oncodna commented Dec 2, 2025

Overview

This PR introduces customizable filter templates by adding a template attribute to filters. All built-in filters now define a default template to ensure full backward compatibility.

Filters are rendered through their declared template, with predictable fallbacks, making template overrides a first-class extension point.

Goals

  • Enable fully custom filter UIs (dropdowns, chips, branded widgets) without copying or patching core templates.
  • Provide a stable extension mechanism while preserving current behavior via default templates.

Example

from sqladmin.filters import StaticValuesFilter

class StatusDropdownFilter(StaticValuesFilter):
    template = "filters/status_dropdown_filter.html"

    def __init__(self):
        super().__init__(
            column=Article.status,
            values=[("draft", "Draft"), ("published", "Published")],
            title="Status",
        )

filters/status_dropdown_filter.html

  {% extends "sqladmin/filters/base_filter.html" %}
  {% block filter_body %}
    {% set current_value = request.query_params.get(filter.parameter_name, '') %}
    <form method="get" class="d-flex flex-column" style="gap: 8px;">
      {% for key, value in request.query_params.items() %}
        {% if key != filter.parameter_name %}
        <input type="hidden" name="{{ key }}" value="{{ value }}">
        {% endif %}
      {% endfor %}

      <select name="{{ filter.parameter_name }}" class="form-select form-select-sm">
        {% for value, label in filter.lookups(request, model_view.model, model_view._run_arbitrary_query) %}
          <option value="{{ value }}" {% if current_value == value %}selected{% endif %}>{{ label }}</option>
        {% endfor %}
      </select>

      <div class="d-flex align-items-center" style="gap: 8px;">
        <button type="submit" class="btn btn-sm btn-outline-primary">Apply</button>
        {% if current_value %}
        <a href="{{ request.url.remove_query_params(filter.parameter_name) }}" class="text-decoration-none small">Clear</a>
        {% endif %}
      </div>
    </form>
  {% endblock %}

By subclassing an existing filter and overriding only its template, developers can package and reuse bespoke filter widgets with minimal effort.

@fd-oncodna fd-oncodna changed the title Enable custom column filter templates and document bespoke filter widgets Allow Column Filters to Declare Custom Templates and Override Built-ins Dec 2, 2025
@fd-oncodna fd-oncodna changed the title Allow Column Filters to Declare Custom Templates and Override Built-ins Add template hooks to all filters for customizable UIs (dropdowns, sliders, etc.) Dec 5, 2025
Copy link
Contributor

@mmzeynalli mmzeynalli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! Do please update the mentioned part. Also, would be nice if you provided screenshots of vanilla vs custom design filter design, even if minimal.

filter.template
if filter.template is defined and filter.template
else (
"sqladmin/filters/operation_filter.html"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can skip this part. If user creates new filter, they will inherit the existing one, if they create from scratch, it should be their responsibility. No need for extra has_operator check.

Copy link
Contributor

@mmzeynalli mmzeynalli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM @aminalaee

@fd-oncodna
Copy link
Author

Sorry for the delay, here are the screenshots!

Before:

image

After:
image

Code used:

class StatusDropdownFilter(StaticValuesFilter):
    template = "filters/status_dropdown_filter.html"

Template:

{% extends "sqladmin/filters/base_filter.html" %}

{% block filter_body %}
  {% set current_value = request.query_params.get(filter.parameter_name, '') %}
  <form method="get" class="d-flex flex-column" style="gap: 8px;">
    {% for key, value in request.query_params.items() %}
      {% if key != filter.parameter_name %}
        <input type="hidden" name="{{ key }}" value="{{ value }}">
      {% endif %}
    {% endfor %}

    <select name="{{ filter.parameter_name }}" class="form-select form-select-sm">
      {% for value, label in filter.lookups(request, model_view.model, model_view._run_arbitrary_query) %}
        <option value="{{ value }}" {% if current_value == value %}selected{% endif %}>
          {{ label }}
        </option>
      {% endfor %}
    </select>

    <div class="d-flex align-items-center" style="gap: 8px;">
      <button type="submit" class="btn btn-sm btn-outline-primary">Apply</button>
      {% if current_value %}
        <a href="{{ request.url.remove_query_params(filter.parameter_name) }}" class="text-decoration-none small">Clear</a>
      {% endif %}
    </div>
  </form>
{% endblock %}

@mmzeynalli
Copy link
Contributor

@aminalaee ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants