bigskysoftware / htmx

</> htmx - high power tools for HTML
https://htmx.org
Other
38.19k stars 1.3k forks source link

hx-swap-oob seems to break in the presence of `<tbody>` #1900

Open geoffbeier opened 1 year ago

geoffbeier commented 1 year ago

I'm following along with the Hypermedia Systems book. I'm trying to add a search status display that reports how many results there are for the live search field.

My search results are a tbody:

<tbody>
    {% for contact in page_obj %}
        <tr>
            <td>{{contact.first_name}}</td>
            <td>{{contact.last_name}}</td>
            <td>{{contact.phone_number}}</td>
            <td>{{contact.email}}</td>
            <td>
                <a href="{% url 'contacts:edit_contact' contact.id %}">Edit</a>
                <a href="{% url 'contacts:contact' contact.id %}">View</a>
            </td>
        </tr>
    {% endfor %}
    {% if page_obj.has_next %}
        <tr>
            <td colspan="5" style="text-align: center">
                <span hx-target="closest tr"
                      hx-swap="outerHTML"
                      hx-select="tbody > tr"
                      hx-trigger="revealed"
                      hx-get="?query={{ query }}&page={{ page_obj.next_page_number }}">
                    Loading more contacts...
                </span>
            </td>
        </tr>
    {% endif %}
</tbody>

My first attempt at the status display had it coming down in that same response with hx-swap-oob=true:

<div id="search_status" hx-get="count-contacts?query={{query}}" hx-trigger="load" hx-swap-oob="true"></div>

I expected that to update my search results table and replace the search status div with one that would send a GET to the count-contacts view with the correct query.

If I put the hx-swap-oob element after the tbody element, it simply failed to swap. If I put it before the tbody element, it went into the table and all of my tr elements did too, except that the tr and td tags were stripped out and replaced by whitespace.

I posted a question on discord and was advised that I should open an issue here. I'm happy to try to narrow this down either on discord or in response to this issue.

My source code for my version of the contacts app is here: https://git.sr.ht/~tuxpup/contacts-hypermedia-systems

The blog posts where I explain that source code in detail are linked from here: https://geoff.tuxpup.com/posts/hypermedia-systems-django-intro/

andryyy commented 1 year ago

Hey,

I remember there were some problems with tables (or its components) and that HTMX 2 may use the idiomorph approach for a better merge mechanism.

I cannot remember the exact problems discussed or if it was resolved, so I'm not really of help to fix that exact issue.

A HTMX-like workaround could be the following:

You can add the count to the context here and add a hidden input field to the reponse that you can use in an event handler on the search input field like this:

  hx-on::after-request="event.detail.successful&&(document.getElementById('search_status').textContent=someField.value)"

someField would be the input carrying the value.

Or, instead of an input field, an attribute on the tbody can to the same.

I'm excited to see other comments. :)

Edit: Found the issue here. Did you place your piggybacked div before or after the tbody content?

Edit 2: Sending an HX-Trigger header with an event carrying the value as payload may also work. The event is fired on the targeted element in your HTML. So it would be the input field.

geoffbeier commented 1 year ago

Thanks for taking a look!

Or, instead of an input field, an attribute on the tbody can to the same.

I did try that, and went with the event instead because I needed the status field to get its update using a query parameter that changes based on what came down with the table.

Thanks for linking the other issue. Looking at that and reading the htmx source code, they feel strongly related.

I tried changing the order of the <div> and the <tbody> and it did alter the behavior. When the div came first, the table got all the text from the search results but none of the <tr> tags. When the <tbody> came first, at least the first fetch of the table worked, but the <div> was never swapped. I think subsequent fetches of the <tbody> also failed to swap.

andryyy commented 1 year ago

Ah, I see. That's interesting.

Another idea:

<div id="search_status"
     {% if query %}
         hx-get="{% url 'contacts:count' %}?query={{ query }}"
         hx-trigger="load"
     {% endif %}
></div>

to

<div id="search_status"
     {% if query %}
         hx-get="{% url 'contacts:count' %}?query={{ query }}"
         hx-include="#{{ search_form.query.id_for_label }}"
         hx-trigger="load, htmx:afterSwap from:tbody"
     {% endif %}
></div>

That would always trigger on a swap from tbody.

geoffbeier commented 1 year ago

That does work as a trigger. The issue here is that I need to update the hx-get URL to include the new query that reflects the rows I just returned for the table. {{query}} is rendered on the server and fixed on the client.

That was the original motivation for using hx-swap-oob

xhaggi commented 1 year ago

This should be fixed by #1794.

There is a workaround via htmx.config.useTemplateFragments but you need to place the oob element after the table partial in the response.

geoffbeier commented 1 year ago

There is a workaround via htmx.config.useTemplateFragments but you need to place the oob element after the table partial in the response.

Thank you! I read the config documentation several times, and I never would have imagined useTemplateFragments would influence this. Losing IE11 is not a problem at all for me, and neither is placing the oob element after tbody.

For anyone who is searching before the fix to #1794 goes in, here's what worked in the end:

  1. Place this meta tag in the base template header:
<meta name="htmx-config" content='{"useTemplateFragments": true}'>
  1. Add this to the template that returns my updated table body in response to the query:
<div
        hx-swap-oob="true"
        id="search_status"
        hx-get="{% url 'contacts:count' %}?query={{ query }}"
        hx-trigger="load"
>
</div>
  1. Remove the old event handler.

Thank you all again for your patient help.