A question that often comes up when people are first working with htmx is:
“I need to update other content on the screen. How do I do this?”
There are multiple ways to do so, and in this example will walk you through some of them.
We’ll use the following basic UI to discuss this concept: a simple table of contacts, and a form below it to add new contacts to the table using hx-post.
<h2>Contacts</h2>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th></th>
</tr>
</thead>
<tbody id="contacts-table">
...
</tbody>
</table>
<h2>Add A Contact</h2>
<form hx-post="/contacts">
<label>
Name
<input name="name" type="text">
</label>
<label>
Email
<input name="email" type="email">
</label>
</form>
The problem here is that when you submit a new contact in the form, you want the contact table above to refresh and include the contact that was just added by the form.
What solutions to we have?
The easiest solution here is to “expand the target” of the form to enclose both the table and the form. For example,
you could wrap the whole thing in a div
and then target that div
in the form:
<div id="table-and-form">
<h2>Contacts</h2>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th></th>
</tr>
</thead>
<tbody id="contacts-table">
...
</tbody>
</table>
<h2>Add A Contact</h2>
<form hx-post="/contacts" hx-target="#table-and-form">
<label>
Name
<input name="name" type="text">
</label>
<label>
Email
<input name="email" type="email">
</label>
</form>
</div>
Note that we are targeting the enclosing div using the hx-target attribute. You would need
to render both the table and the form in the response to the POST
to /contacts
.
This is a simple and reliable approach, although it might not feel particularly elegant.
A more sophisticated approach to this problem would use out of band swaps to swap in updated content to the DOM.
Using this approach, the HTML doesn’t need to change from the original setup at all:
<h2>Contacts</h2>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th></th>
</tr>
</thead>
<tbody id="contacts-table">
...
</tbody>
</table>
<h2>Add A Contact</h2>
<form hx-post="/contacts">
<label>
Name
<input name="name" type="text">
</label>
<label>
Email
<input name="email" type="email">
</label>
</form>
Instead of modifying something on the front end, in your response to the POST
to /contacts
you would include some additional content:
<tr hx-swap-oob="beforeend:#contacts-table">
<td>Joe Smith</td>
<td>joe@smith.com</td>
</tr>
<form hx-post="/contacts">
<label>
Name
<input name="name" type="text">
</label>
<label>
Email
<input name="email" type="email">
</label>
</form>
This content uses the hx-swap-oob attribute to append itself to the #contacts-table
, updating
the table after a contact is added successfully.
Note that because we are using table rows here, we must enable template fragment parsing (thus sacrificing IE11 compatibility)
htmx.config.useTemplateFragments = true;
An even more sophisticated approach would be to trigger a client side event when a successful contact is created and then listen for that event on the table, causing the table to refresh.
<h2>Contacts</h2>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th></th>
</tr>
</thead>
<tbody id="contacts-table" hx-get="/contacts/table" hx-trigger="newContact from:body">
...
</tbody>
</table>
<h2>Add A Contact</h2>
<form hx-post="/contacts">
<label>
Name
<input name="name" type="text">
</label>
<label>
Email
<input name="email" type="email">
</label>
</form>
We have added a new end-point /contacts/table
that re-renders the contacts table. Our trigger for this request
is a custom event, newContact
. We listen for this event on the body
because when it
is triggered by the response to the form, it will end up hitting the body due to event bubbling.
When a successful contact creation occurs during a POST to /contacts
, the response includes
an HX-Trigger response header that looks like this:
HX-Trigger:newContact
This will trigger the table to issue a GET
to /contacts/table
and this will render the newly added contact row
(in addition to the rest of the table.)
Very clean, event driven programming!
A final approach is to use REST-ful path dependencies to refresh the table. Intercooler.js, the predecessor to htmx, had path-based dependencies integrated into the library.
htmx dropped this as a core feature, but supports an extension, path deps, that gives you similar functionality.
Updating our example to use the extension would involve loading the extension javascript and then annotating our HTML like so:
<h2>Contacts</h2>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th></th>
</tr>
</thead>
<tbody id="contacts-table" hx-get="/contacts/table" hx-ext="path-deps" hx-trigger="path-deps" path-deps="/contacts">
...
</tbody>
</table>
<h2>Add A Contact</h2>
<form hx-post="/contacts">
<label>
Name
<input name="name" type="text">
</label>
<label>
Email
<input name="email" type="email">
</label>
</form>
Now, when the form posts to the /contacts
URL, the path-deps
extension will detect that and trigger an path-deps
event on the contacts table, therefore triggering a request.
The advantage here is that you don’t need to do anything fancy with response headers. The downside is that a request
will be issued on every POST
, even if a contact was not successfully created.
Generally I would recommend the first approach, expanding your target, especially if the elements that need to be updated are reasonably close to one another in the DOM. It is simple and reliable.
After that, I would say it is a tossup between the custom event and an OOB swap approaches. I would lean towards the custom event approach because I like event-oriented systems, but that’s a personal preference. Which one you choose should be dictated by your own software engineering tastes and which of the two matches up better with your server side technology of choice.
Finally, the path-deps approach is interesting, and if it fits well with your mental model and overall system architecture, it can be a fun way to avoid explicit refreshing. I would look at it last, however, unless the concept really grabs you.