HTMX Patterns - Preserving Query Parameters
Feb. 3, 2024
I’ve been using HTMX quite a lot lately and I’m sold on the idea of the Hypertext Driven Application (HDA for short). For a thorough introduction into the topic I recommend going through the book.
When working with HTMX, I try to make use of the tools the Platform (aka the browser) provides. Part of that is making comprehensive use of URLs, which - when used correctly - make a web application sharable, discoverable and in general a good citizen in the web.
The problem
Let’s say you have a list of items that’s getting out of hand. To make it more manageable for the user we want to add pagination and search.
Let’s first see a naive solution that does not quite work, followed by different solutions, all with different pros and cons. As always: it’s a trade-off.
The naive solution
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
This should pretty self-explanatory, so I’ll just highlight the problem here: The search term is lost when we change pages, since it’s not persisted in the query parameters. This is actually quite easy to forget and of course gets amplified when more query parameters are involved. Even in the official book this problem exists, when the initial paginated design is enhanced with an active search.
Using forms instead of links
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | |
This might be a bit unconventional, but at least it does not suffer the same problem as the naive version. What we’re doing here, is using hidden form inputs to carry the search term information when using the pagination.
Of course a pretty serious drawback is that we’re now using buttons where links would be much better in terms of semantics, accessibility and UX. Having a button instead of a links removes some pretty handy browser features, like middle-clicking to open in a new tab or hovering over the link to show the destination of the link.
In terms of readability and composability I like this solution though, since there’s no manual string formatting needed. All the “logic” of which parameters are being preserved is part of the HTML. If in this particular instance that’s actually a good thing is questionable, though.
Using hx-include
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
As it often seems to be the case, with just a few annotations we can tweak an existing solution using HTMX attributes. As you might have noticed, in the previous examples we weren’t even using HTMX for our page.
It turns out, that it’s enough to add the hx-include attribute to our anchor elements alongside enabling HTMX for them using the hx-boost attribute. What this does is effectively telling HTMX to include the value(s) of the matched inputs in the request. Instead of just including specific inputs another viable approach might be to use a data attribute on the to-be-included inputs and then filter using that, i.e. hx-include="[data-preserve-query]" .
The only downsides I can see with this solution is that hovering over the links won’t reveal their true destination (because the query parameter q will be added by HTMX for the actual request) and that without JavaScript enabled, we’re back to the original buggy behavior.
Using a better paginator
For the previous solutions, I’ve been using the standard Django paginator like so:
1 2 3 4 5 6 7 8 9 10 11 12 | |
The “problem” with this paginator is that it is completely oblivious of the context it is running in. Just as an idea, here’s an enhanced version of the standard Paginator, which is bound to the request and knows how to generate the next and previous URLs we so desperately tried to construct ourselves previously:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | |
While this seems to be quite overkill for such a simple task, it simplifies our template quite a bit, leaving us almost where we started:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
Conclusion
So there you have it. A possibly much too thorough look at how to work with query parameters. To me, the hx-include approach probably will be just enough most of the time.
All the code for this blog is available in this repo for you to play around with. Have fun!