Hi Ken,
Thanks for your questions and clear desire to help other developers grow! Always a pleasure getting responses from you!
Are you talking about doing this with HTMX or in general?
With HTMX in particular.
When you’re talking about resettings a form, are you talking about clearing the values or resetting to what was there when the form was originally rendered?
Resetting to what was there when the form was originally rendered; in this instance, it’s essentially the same as a page reload, but I’d rather not force a full page reload because of UX concerns.
In more detail:
I’m talking about resetting a form (really, it’s a filter that enables a user to restrict objects in a table based on certain properties of those objects) by pressing a button. When a user presses the ‘Reset Filter’ button, all elements in the form/filter should be reset and an hx-get request should be sent to the server, with the response handled identically other elements in the filter handle the response (i.e., hx-swap innerHTML w/ the response, etc.).
I’ve tried <input type="reset">
which appropriately resets the form/filters, but this element does not seem possible of holding the hx-types required for the GET request and repopulating the page as expected. I’ve also tried a number of different <button>
types but none of them reset the form or handled the response as expected.
Form works 100% with HTMX, but this reset button is finicky.
Relevant code below:
filter template
{% load crispy_forms_filters %}
{% load crispy_forms_tags %}
<!-- Review Required Block -->
<div class="d-none d-lg-block">
{% if request.user.is_procurement_section_head or request.user.is_superuser %}
{% if suppliers_pending_review and request.resolver_match.url_name == 'home'%}
<div class="col-2"></div>
<div class="row justify-content-between mb-3">
<div class="col-md-2"></div>
<div class="col-md-8 d-grid">
<button class="btn btn-danger"
onclick="location.href='{% url 'suppliers_review_required' %}'"
>Review Pending Suppliers</button>
</div>
<div class="col-2"></div>
{% endif %}
{% endif %}
</div>
<!-- End Review Required Block -->
<!-- TODO - Export Block -->
{#<div class="d-none d-lg-block">#}
{# <div class="col-2"></div>#}
{# <div class="row justify-content-between mb-3">#}
{# <div class="col-md-2"></div>#}
{# <div class="col-md-8 d-grid">#}
{# <button class="btn btn-success"#}
{# onclick="location.href='{% url 'suppliers_export' %}'"#}
{# >Export Suppliers Data</button>#}
{# </div>#}
{# <div class="col-2"></div>#}
{#</div>#}
<!-- End Export Block -->
<div class="row mb-2">
<!-- Supplier Score Min and Max Filters -->
<div class="col text-center">
<label for="id_score_min" class="fw-bold mb-2">Supplier Score</label>
<div class="d-flex">
{{ form.score_min|as_crispy_field }} - {{ form.score_max|as_crispy_field }}
</div>
</div>
<!-- Turnover Min and Max Filters -->
<div class="col text-center">
<label for="id_turnover_min" class="fw-bold mb-2">Turnover</label>
<div class="d-flex justify-content-center">
{{ form.turnover_min|as_crispy_field }}<span> - </span>{{ form.turnover_max|as_crispy_field }}
</div>
</div>
</div>
<!-- New Filter Row -->
<!-- Business Type Filter -->
<div class="row mb-3">
<div class="col-6 ">
<label class="fw-bold">Business Type</label>
<div class="scrollable-checkbox-list">
{{ form.business_type|as_crispy_field }}
</div>
</div>
<!-- Classifications Filter -->
<div class="col-6 ">
<label class="fw-bold">Classifications</label>
<div class="scrollable-checkbox-list">
{{ form.classifications|as_crispy_field }}
</div>
</div>
</div>
<!-- New Filter Row -->
<!-- Disciplines Filter -->
<div class="row">
<div class="col-6 ">
<label class="fw-bold mb-2">Disciplines</label>
<div class="scrollable-checkbox-list">
{{ form.disciplines|as_crispy_field }}
</div>
</div>
<!-- Classifications Filter -->
<div class="col-6 ">
<label class="fw-bold mb-2">Statuses</label>
<div class="scrollable-checkbox-list">
{{ form.status|as_crispy_field }}
</div>
</div>
</div>
<!-- End of Filter/Form -->
<!-- Reset Button -->
<div class="row mt-4">
<div class="col-11">
<div class="d-flex justify-content-end">
<button class="btn-dark button mb-2"
type="reset"
hx-get="{% url request.resolver_match.url_name %}"
hx-select="#filtersContainer"
hx-swap="innerHTML"
>Reset Filter</button>
</div>
</div>
</div>
<!-- End Reset Button -->
</div>
</div>
base template
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="{% static 'css/bootstrap.css' %}" rel="stylesheet">
<!-- Site-Specific CSS-->
<link href="{% static "css/base.css" %}" rel="stylesheet" type="text/css">
<link href="{% static "css/main.css" %}" rel="stylesheet" type="text/css">
<!-- HTMX JavaScript File -->
<script src="{% static "js/htmx.min.js" %}"></script>
<!-- FavIcon File -->
<link rel="shortcut icon" type="image/png" href="{% static 'images/favicon.png' %}" >
{% if supplier %}
<title>{{ supplier.name }}</title>
{% else %}
<title>{{ title }}</title>
{% endif %}
<!-- HEAD BLOCK -->
{% block head %}
{% endblock %}
</head>
<form hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
<!-- Nav Header Bar Block -->
{% include "navigation_header.html" %}
<!-- Main Content Block -->
<div class="container" id="filtersContainer">
<div class="row"> <!-- Ensure both columns are within the same row -->
<div class="col-md-8" id="contentBlock">
<!-- Message Alert Block -->
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
<!-- End Message Alert Block -->
<!-- Main Content Block -->
<div id="content-main">
<!-- Nav Headers Block -->
<div id="header-tabs">
{% include 'nav_header_tabs/general_header_tabs.html' %}
</div>
<!-- End Nav Headers Block -->
{% block content %}
{% endblock %}
</div>
<!-- End Main Content Block -->
</div>
<!-- End Content Block -->
<!-- Sidebar -->
<div class="col-md-4 navigation-sidebar" id="navigation-sidebar">
{% include "sidebar_right.html" %}
</div>
<!-- End Sidebar -->
</div>
</div>
{% if request.resolver_match.url_name == 'home' %}
</form>
{% elif request.resolver_match.url_name == '' %}
{% endif %}
<script src="{% static 'js/bootstrap.bundle.js' %}" type="text/javascript"></script>
{% if request.resolver_match.url_name == 'home' or request.resolver_match.url_name == 'suppliers_review_required' %}
{# This JS file is now only required for the RESET button; TODO: refactor and remove need for JS here #}
{# <script src="{% static 'js/dynamic_filtering.js' %}"></script>#}
{% elif request.resolver_match.url_name == 'supplier_create' or 'supplier_update' in request.resolver_match.url_name %}
<script src="{% static 'js/form_wizard_post_prepends.js' %}"></script>
<script src="{% static 'js/dynamic_formset.js' %}"></script>
{% elif request.resolver_match.url_name == 'supplier_update_documents' %}
<script src="{% static 'js/dynamic_formset.js' %}"></script>
{% endif %}
{% if request.resolver_match.url_name == 'contract_award_from_bid' %}
<script src="{% static 'js/dynamic_formset.js' %}"></script>
{% endif %}
{% if request.resolver_match.url_name == 'suppliers_report' %}
<script src="{% static 'js/bokeh-3.4.1.min.js' %}"></script>
<script src="{% static 'js/bokeh-api-3.4.1.min.js' %}"></script>
<script src="{% static 'js/bokeh-gl-3.4.1.min.js' %}"></script>
<script src="{% static 'js/bokeh-mathjax-3.4.1.min.js' %}"></script>
<script src="{% static 'js/bokeh-tables-3.4.1.min.js' %}"></script>
<script src="{% static 'js/bokeh-widgets-3.4.1.min.js' %}"></script>
{% endif %}
</body>
</html>
filter.py
import django_filters
from .models import *
from django import forms
from django.urls import reverse_lazy
class SupplierFilter(django_filters.FilterSet):
name = django_filters.CharFilter(
lookup_expr="icontains",
label="",
widget=forms.TextInput(
attrs={
"id": "supplier_filter_name",
"class": "form-inline form-control mt-3",
"hx-trigger": "keyup",
}
),
)
score_min = django_filters.NumberFilter(
field_name="riskset__score",
lookup_expr="gte",
label="",
widget=forms.NumberInput(
attrs={
"placeholder": "Min",
"class": "form-control",
"style": "font-size: 12px;",
"hx-trigger": "keyup",
}
),
)
score_max = django_filters.NumberFilter(
field_name="riskset__score",
lookup_expr="lte",
label="",
widget=forms.NumberInput(
attrs={
"placeholder": "Max",
"class": "form-control",
"style": "font-size: 12px;",
"hx-trigger": "keyup",
}
),
)
turnover_min = django_filters.NumberFilter(
field_name="riskset__turnover",
lookup_expr="gte",
label="",
widget=forms.NumberInput(
attrs={
"placeholder": "Min",
"class": "form-control",
"style": "font-size: 12px;",
"hx-trigger": "keyup",
}
),
)
turnover_max = django_filters.NumberFilter(
field_name="riskset__turnover",
lookup_expr="lte",
label="",
widget=forms.NumberInput(
attrs={
"placeholder": "Max",
"class": "form-control",
"style": "font-size: 12px;",
"hx-trigger": "keyup",
}
),
)
business_type = django_filters.ModelMultipleChoiceFilter(
queryset=BusinessType.objects.all().order_by("name"),
field_name="business_type__name",
lookup_expr="exact",
to_field_name="name",
label="",
widget=forms.CheckboxSelectMultiple(
attrs={
"class": "form-inline form-control mb-2",
"hx-trigger": "change",
"name": "business_type",
}
),
)
classifications = django_filters.ModelMultipleChoiceFilter(
queryset=Classification.objects.all().order_by("name"),
field_name="classifications__name",
to_field_name="name",
label="",
widget=forms.CheckboxSelectMultiple(
attrs={
"class": "form-inline form-control mb-2",
"hx-trigger": "change",
}
),
)
disciplines = django_filters.ModelMultipleChoiceFilter(
queryset=Discipline.objects.all().order_by("name"),
field_name="offered_services__discipline",
label="",
widget=forms.CheckboxSelectMultiple(
attrs={
"class": "form-inline form-control mb-2",
"hx-trigger": "change",
}
),
)
status = django_filters.ModelMultipleChoiceFilter(
queryset=SupplierStatus.objects.all().order_by('name').distinct('name'),
field_name="status__name",
label="",
widget=forms.CheckboxSelectMultiple(
attrs={
"class": "form-inline form-control mb-2",
"hx-trigger": "change",
}
),
)
class Meta:
model = Supplier
fields = [
"name",
]
def __init__(self, *args, **kwargs):
super(SupplierFilter, self).__init__(*args, **kwargs)
fields = [
"name",
"score_min",
"score_max",
"turnover_min",
"turnover_max",
"business_type",
"classifications",
"disciplines",
"status",
]
for field in fields:
if field == 'name':
self.filters["name"
].field.widget.attrs["placeholder"] = self.get_placeholder()
self.filters[field].field.widget.attrs["hx-get"] = self.get_hx_get_url()
self.filters[field].field.widget.attrs["hx-target"] = "div.table-container"
self.filters[field].field.widget.attrs["hx-swap"] = "innerHTML"
self.filters[field].field.widget.attrs["hx-boost"] = "true"
self.filters[field].field.widget.attrs["hx-include"] = "#filtersContainer"
self.filters['status'].field.label_from_instance = lambda obj: obj.get_name_display()
def get_hx_get_url(self):
return reverse_lazy("home")
def get_placeholder(self):
return "Find a Supplier..."
def get_filter_fields(self):
fields = {}
for field_name, filter_instance in self.declared_filters.items():
field_type = type(filter_instance).__name__
form_field_class = filter_instance.field_class
form_widget = form_field_class.widget
fields[field_name] = [field_type, form_widget]
return fields
class SupplierReviewFilter(SupplierFilter): # Management permissions required for pages w/ this filter
def get_hx_get_url(self):
return reverse_lazy("suppliers_review_required")
def get_placeholder(self):
return "Find a Supplier to review..."