Resetting a form in Django using HTMX

Hello all, not sure if I can ask a HTMX related q here, but since it is so connected to my project in Django and the fact that I’m stuck, I am asking for help…

I am working on a simple form in Django and I would like to achieve reset functionality using HTMX… In instances where Django gives validation errors, they would go away when user clicks on “reset” button… I dont know if there are any easier ways to do this (which i would love to listen to), but I prefer the HTMX solution as it would be of use to me later on as well…

In the below,

index.html contains the form (portfolio form)… please note, index carries a base.html which contains htmx unpkg…
portfolio-reset.html is the partials/ code that I would like HTMX to do AJAX on…
portfolio_reset is the view to fetch the PortfolioForm…

The problem I’m facing is when I run the code, the HTMX is doing a full page reload of index.html and I am not able to fetch just the portfolio-reset code to just reset the form… Not sure where I’m going wrong… Appreciate your help…

index.html

`

{% csrf_token %}
{{ portfolio_form|crispy }}






                <div id="portfolio-reset" ></div>

`

portfolio-reset.html

`
{% load crispy_forms_filters %}
{% load crispy_forms_tags %}

<div hx-target="this" hx-swap="outerHTML">
<form>
    {% csrf_token %}
    {{ portfolio_form|crispy }}
</form>
</div>

`

portfolio_reset view

def portfolio_reset(request): portfolio_form = PortfolioForm() context = { 'portfolio_form': portfolio_form } return render(request, "portfolio/partials/portfolio-reset.html", context)

urls

`
app_name = ‘portfolio’
urlpatterns = [
# Portfolio app views
path(“index/”, portfolio_list, name=“index”),
path(“index/”, portfolio_reset, name=“portfolio-reset”)

`

Side note: When posting code or templates here, surround the code/template between lines of three backtick - ` characters. This means you’ll have a line of ```, then your code (or template), then another line of ```. This forces the forum software to keep your code properly formatted.

In general, when you’re using htmx, you want to create views and templates that will only render the html element that you wish to replace. You don’t want to re-render the entire page.

Unfortunately, because of the formatting, I can’t quite figure out what you have in order to be more specific than this. However, you may wish to review the docs for hx-target.

Hello Ken

Many thanks for your message… My apologies, I knew the formatting wasn’t best but was unsure how to format it… will take note of the backticks next time…

Right now, I am able to solve the current issue, hence we can consider this closed…

Best Regards

Hey! I’m facing a similar issue, i.e., trying to reset a form with the click of a button.

Mind sharing a snippet of how you resolved the issue, for me and other users facing similar challenges?

Are you talking about doing this with HTMX or in general?

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?

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..."

Sorry, I’m still having a bit of trouble understanding what you’re trying to say here.

Rephrasing to see if I “get it” -

You’re issuing some request.

This request contains query parameters. (http://url?something...)

This request responds with a form.

So I’m reading this as saying that you want a reset button to resend the same url as sent above? (e.g., http://url?something...)

Is that the gist of this, or is there something I’m missing?

If this is it, then I would approach this by constructing the htmx get for the reset button to supply the same data as was passed originally.

This leaves you with handling the issue of only retrieving the form and not the entire page.

You could either create a separate view to only render the form, and have htmx call that view.

Or, you could use one of the template “fragment” libraries to go ahead and render the entire page, but extract only the part you want. (Maybe with the addition of an extra parameter to the view to indicate whether it’s a full page or partial page render.)

We take a slightly different approach to this. Our htmx-enabled forms exist in a template of their own - and yes, in some cases the template is simply <div ...>{{ form }}</div>. These templates can be included in a full page, or rendered alone in an HTMX-specific view.

Hi @KenWhitesell - sorry for the very late response and thank you for your detailed line of questioning. It’s through you asking these questions that I resolved my issue.

The issue was this: I have a filter that includes multiple parameters; whenever a user selects one parameter, an HTMX request is sent, and the response updates a table in the DOM based on the parameters in that query.

I wanted a button that resets the filter (i.e., removes all user input/selections from the filter), and sends an HTMX request with a blank query (i.e., requests to get everything). The response should update only the table mentioned earlier, rather than reloading the entire page.

So, the filter is essentially a form, but it wasn’t that I wanted the entire form to be resent; rather, I wanted the form to be emptied and for an HTMX request with an empty form to be sent.

I didn’t want to use any JS, but I couldn’t figure out any other way. So, I made a button that has an HX-on trigger. Code below; hope it helps anyone else who’s struggling with this.

<button class="btn-dark button mb-2"
    hx-on="click: resetFilters()"
    hx-swap="none"
    >
    Reset Filters
</button>

<script>
    function resetFilters() {
        const container = document.getElementById('filtersContainer');
        const resetDict = [
                {type: 'text', attribute: 'value', value: ''},
                {type: 'number', attribute: 'value', value: ''},
                {type: 'checkbox', attribute: 'checked', value: false}
            ];
        if (container) {
            // Iterate over the dictionary to reset inputs
            resetDict.forEach(resetItem => {
                container.querySelectorAll(`input[type="${resetItem.type}"]`).forEach(
                    input => input[resetItem.attribute] = resetItem.value
                );
            });
            // Trigger HTMX to refresh the content
           htmx.trigger(htmx.find('div.table-container'), 'refresh');
        }
    }

    // reload table-container as required
    htmx.on('refresh', function(evt) {
        htmx.ajax('GET', '/', { target: 'div.table-container', swap: 'innerHTML' });
  });
</script>

Again, apologies for the delay and many thanks for your questions seeking clarification and overall guidance!