Using HTMX and django - mostly great, but how to send along GET params with htmx?

hi folks.

I’ve been using django-htmx of late, and it’s a really, realy lovely package to add to working project, but I’ve bumped up against a problem I can’t seem to google the fix to, or solve through trial and error.

How do you send along GET params in a htmx request with django ?

I have an app I’m working on, which is designed to work like a sort of shared address book fo events and communities (you might use it at a conference, to help with networking for example), and a key part of the app is a filtered search view, that currently uses PostgresSQL’s full text search and tags to provide a nice way to filter a group of people by shared interests, or matching text in their profile.

I’m using django filter for this, and I’ve figured out how to get it to search through text, as well as a bunch of tags on a profile too. It looks a bit like this - any change to search text, or updating the chosen tags triggers a ‘submit’ event on the main filter form, sending along the GET params, and then storing them in the HTML history.

The hx-target=".sidebar" bit tells htmx to update my sidebar with an updated list of profiles, a bit like how search works in an native address book on a mac, for example. The tags are visually hidden, but on a profile, the profiles tags for someone’s interests and skills are listed like clickable buttons, which updates the filtering on the site.

        <form id="filter-form"
              method="get"
              hx-get="/"
              hx-trigger="submit, htmx:confirm from:#id_tags, htmx:confirm from:#id_bio, toggle-tag from:body"
              hx-target=".sidebar"
              hx-push-url="true">
          <p>
            <label class="inline-block w-100 mr-8 text-xl"
                   for="{{ profile_filter.form.bio.id_for_label }}">Showing profile results matching:</label>
          </p>
          <p>
            {% render_field profile_filter.form.bio hx-get="/" hx-trigger="keyup changed delay:0.2s," hx-target=".sidebar" class="text-xl min-w-[50%]" hx-sync="closest form:abort" %}
          </p>
          <p class="hidden">{% render_field profile_filter.form.tags %}</p>
          <button class="btn mt-4 ml-4" type="submit">Search</button>
        </form>

So, when the django filter is updated, you end up with a path like so (this shows a search filtered to full text for the word something, and add two tags

/?bio=something?tags=3&tags=6

You can see the home page showing this code at this link in the repo

In the sidebar

In the sidebar, I have a list of profiles, listing photos and names, and each profile is made to act like a button with some code like below (I’ve edited it for brevity):

<div class="hover:cursor-pointer"
         hx-get="{% url 'profile-detail' profile.short_id %}"
         hx-target="#profile-slot"
         hx-push-url="true">
<!-- snip -->
</div>

You can also see the paginated list of profiles too if the context helps.

This request fetches profile information and loads it into the page (or more preciselty, loads the HTMX response into #profile-slot div on the page.

You can see the full view code on github too, where we either serve a full page, or a smaller htmx response.

HTTP as the engine of state (I think)

I’m trying to use HTML as the engine of state, hence the reliance on GET params to represent the state of the search. So when a user clicks on a profile in the sidebar, I’d like to store the state of filtered search in the url. this would make it possible to easily share urls with a filtered view of a search and a detail view of a specific profile .

To make this concrete, I want to be able send a URL like this to the browse history, when a user clicks on a profile:

/profile/SOME_ID/?bio=something?tags=3&tags=6

This would return the info, but leave me with a nice, sharable URL.

With this in mind, I’m using hx-push-url here too, which is adding /profile/SOME_ID/ to the URL history.

I can’t seem to add the GET params tto send along the state of the filtered search view though.

How can I dynamically add these to the URLs being sent as GET requests with htmx?

I’ve tried:

  • hx-vals (which appears to be designed to let you send along other values in the URL), but nothing is sent.
  • hx-include, which is also designed to add addiitonal values in a Ajax request, and finally
  • hx-params, which is designed to filter the params send along (I looked at this in case the values were not being sent).

None of them seem to send along the GET params I want to send, so I’m now scratching my head on this one.

Elsewhere in the app, I’ve found a workaround from HTMX’s default to let me alter HTMX behaviour before, by using events, and having HTMX respond to them. This seems to be one recommended approach in many cases

The link below shows me dropping the use of HTMX to trigger HTMX events using vanilla javascript, by emitting events that HTMX listens for elsehwere, but what i’m doing doesn’t seem to be particularly exotic, so before I go down that route, I figured I should ask here.

Am I missing something really obvious here?

Thanks in advance.

1 Like

Success!

Here’s what I needed to add:

hx-include="filter-form [name='bio'], filter-form [name='tags']"

hx-include takes one or more CSS selectors to identify the values you want to send along, so adding these two, filters to the htmx attributes did the trick:

In my case, I had this form here:

<form id="filter-form"
              method="get"
              hx-get="/"
              hx-trigger="submit, htmx:confirm from:#id_tags, htmx:confirm from:#id_bio, toggle-tag from:body"
              hx-target=".sidebar"
              hx-push-url="true">
          <p>
            {% render_field profile_filter.form.bio hx-get="/" hx-trigger="keyup changed delay:0.2s," hx-target=".sidebar" class="text-xl min-w-[50%]" hx-sync="closest form:abort" %}
          </p>
          <p class="hidden">{% render_field profile_filter.form.tags %}</p>
          <button class="btn mt-4 ml-4" type="submit">Search</button>
        </form>

So I had to select the corresponding inputs in the hx-include:

<div class="card-body flex-row hover:bg-yellow-100 hover:shadow-lg hover:cursor-pointer"
         hx-get="{% url 'profile-detail' profile.short_id %}"
         hx-target="#profile-slot"
         hx-include="filter-form [name='bio'], filter-form [name='tags']"
         hx-push-url="true">

BTW, I’m using the render_field template tag from the very helpful widget_tweaks django package - it’s v useful for working with htmx as it lets you pass along aribitrary attributes like you see here.

Update

It turns out you can simplify the code even more. The HTMX docs say this:

Note that if you include a non-input element, all input elements enclosed in that element will be included.

This means that if you have a form that sends GET requests, and you want to store all the params, if you pass in the CSS selector to the form, it’ll send them all along for you.

So all I needed to add was this

hx-include="#filter-form"

Here’s the final HTMX element:

<div class="card-body flex-row hover:bg-yellow-100 hover:shadow-lg hover:cursor-pointer"
         hx-get="{% url 'profile-detail' profile.short_id %}"
         hx-target="#profile-slot"
         hx-include="#filter-form"
         hx-push-url="true">
<!-- snip -->
</div>
3 Likes

That’s a great example. Would make a lovely blog post! :stuck_out_tongue_winking_eye:

Thanks @carltongibson - I spend a bunch of time messing with this project this weekend, and I reckon I have about 5 blog posts worth of stuff I learned now :joy:

I’m hoping to get a version of this app tidied up so it’s a project for other people because I’ve been working on some variation of this for the last few years, and rebuilding it in Vue, Firebase ad nauseum.

I think Htmx and Django will let me end up with a small, more maintainable project that I don’t dread firing up.

2 Likes

I’m refactoring my code and trying to reduce JavaScript and include only HTMX when possible.

I just stumbled upon this while looking for a solution to basically the same exact problem (GET request via HTMX with django-filter and multiple filters).

Your elegant hx-include='#id-of-filter-container' solution helped me reduce about 200 lines of JS code (no more making event listeners, building the entire query, etc)!

Many thanks for sharing your solution!!!

1 Like