htmx icon indicating copy to clipboard operation
htmx copied to clipboard

Preserve elements with hx-swap-oob that are not at the root of a response

Open infogulch opened this issue 3 years ago • 6 comments

Currently when an element with the hx-swap-oob attribute is contained within an outer element in an hx response, the element with this attribute is deleted and not rendered to the document anywhere. I propose changing this behavior to instead ignore the hx-swap-oob attribute and include it in the final document at the original location within its outer element.

initially discussed on discord

What is hx-swap-oob

When an hx request is made and its response is interpreted, the standard behavior is to swap in the contents of the response according to the hx-swap and hx-target attributes on the requesting element. In addition, if the response contains extra root elements with the hx-swap-oob attribute, they are instead swapped into the document at arbitrary locations selected by the value of the attribute. This is a good way to update some node in a remote / outer scope when a node in a smaller scope changes. For example: an input is used to add a new item to a list, the main response is just the inserted element, and an extra hx-swap-oob element is returned at the same time to update the count that is located outside the scope of the list container. If not for this feature, an operation like adding an element to a list could require downloading and re-rendering a much larger portion of the document just to enclose the outer scope of all elements that need to be modified after some operation.

Intended use and edge cases

htmx expects elements with the hx-swap-oob attribute to be returned in an hx response as a root element of the response document. Like this: <li><label>added list item content</label</li><span hx-swap-oob="true" hx-target="count">7</span>. For the purpose of affecting remote elements, this restriction is reasonable to make its behavior understandable and predictable; if there were no such restriction an -oob tagged element could be nested anywhere in the response -- no matter how obscure -- which is too much unnecessary flexibility.

So the rule is "if you want to affect elements outside the scope of the response target, add a new element at the root of the response with the hx-swap-oob attribute". This is great, but it doesn't say what htmx should do if an element with the hx-swap-oob` attribute does appear in an inner element in an hx response.

There are a few reasonable behaviors to choose from:

  1. Treat the whole response as an error and abort the response handling. (This would be valid but doesn't hold with the usual hypermedia approach to ignore things you don't understand and apply your best effort to render something useful.)
  2. Assume that the presence of the inner element with hx-swap-oob is a programming error and omit the entire element from the rendered document. This is the current behavior.
  3. Ignore that the hx-swap-oob attribute is present and render the inner element as if this attribute wasn't there. This is my proposal.

Why prefer behavior 3?

  • This is more in keeping with the 'best effort' rendering strategy mentioned before.
  • This unifies the fresh-page-load and hx response loading behavior. If the user refreshes or loads a whole new page these elements are rendered normally, as if the attribute is omitted, but if they are present in an hx response they are currently omitted.
  • This would simplify the renderer code by allowing it to always include the hx-swap-oob attribute when rendering a specific element, which could be reused in different contexts such as rendering the whole page fresh and in response to an hx request.

Example

Here's a completely static example of what I wish would work in 3 endpoints, hi, list, and add-to-list:

Full page load at /hi:

<script src="https://unpkg.com/[email protected]/dist/htmx.min.js"></script>

<title>Hi</title>

<div class="tabs">
    <a href="hi" hx-get="hi" hx-push-url="hi" hx-target="body">Hi</a>
    <a href="list" hx-get="list" hx-push-url="list" hx-target="body">The List</a>
</div>
<p>Hello!</p>

Full page load at /list:

<script src="https://unpkg.com/[email protected]/dist/htmx.min.js"></script>

<title>The List</title>

<div class="tabs">
    <a href="hi" hx-get="hi" hx-push-url="hi" hx-target="body">Hi</a>
    <a href="list" hx-get="list" hx-push-url="list" hx-target="body">The List</a>
</div>
<button hx-get="add-to-list" hx-target="#mylist" hx-swap="beforeend">Add Item</button>
<ul id="mylist">
    <li>item1</li>
    <li>item2</li>
</ul>
<p>There are <span id="count" hx-swap-oob="true">2 items</span> in total.</p>

HX response at /add-to-list:

<li>item3</li>
<span id="count" hx-swap-oob="true">3 items</span>

Demo steps:

  1. Start at /hi
  2. Click on The List link to use htmx to navigate to the The List page
  3. Note how the span with "2 items" is missing
  4. Click on the Add Item button
  5. Note how the list is updated to add item3, but the span with the item count is still missing
  6. Refresh the page at /list
  7. Note how the span with "2 items" is visible
  8. Click on the Add Item button
  9. Note how the list is updated to add item3 like before, but also the span with the item count is updated successfully

I think that the steps from 2-5 should behave the same as the steps from 6-9.


I'm sorry this issue description is so long, if I had more time I would have made it shorter. :)

Thoughts?

infogulch avatar Nov 15 '22 02:11 infogulch

We've run into this issue as well. The docs were very misleading, imo. "Out of band elements must be in the top level of the response, and not children of the top level elements." To me, that implies that any hx-swap-oob elements further down in the tree will be ignored. I would never expect HTMX to modify those elements instead.

dkniffin avatar Jan 04 '23 15:01 dkniffin

I suspect the behavior could be adjusted by changing the selector here:

https://github.com/bigskysoftware/htmx/blob/bd812856103a67cbe8da9227a1423a223b2f6af9/dist/htmx.js#L768

.. to only select direct children of the fragment with the required attributes instead of all ancestors. Maybe this?

:root > [hx-swap-oob], :root > [data-hx-swap-oob]

infogulch avatar Jan 18 '23 03:01 infogulch

@dkniffin fyi, the PR #1235 contains a one-line solution that solves this for me. If you're hosting htmx.js yourself its pretty easy to apply manually.

infogulch avatar Feb 25 '23 00:02 infogulch

I was hoping this would allow me to create inline form errors, but I ended up using https://htmx.org/extensions/multi-swap/ instead (the key to getting this to work being Set hx-ext="multi-swap" attribute on <body>).

If you want to test/host your own modified version like @infogulch mentioned though, something like this should work:

  • git clone [email protected]:bigskysoftware/htmx.git && cd htmx
  • git fetch origin pull/1235/head:patch-1
  • git checkout patch-1
  • npm run dist
  • cp dist/htmx.min.js ~/path/to/your/project/.

jimafisk avatar Feb 21 '24 20:02 jimafisk

The current behavior just bit me and I wanted to share the circumstance if it helps the PR be accepted. My particular situation was that a simple refactoring made all of the sub elements disappear when I moved HTML from the first render of a page to a subcall using hx-get. The fragment had children elements with hx-swap-oob attributes set. They all disappeared with this seemingly simple refactor. A very jarring experience that led me here.

jeremylowery avatar Apr 11 '24 22:04 jeremylowery

Yes it's a common problem especially when using fragments.

You'll be happy to hear that #1235 was successfully merged last month which adds a config option to change the behavior. It was merged into the 2.0 branch, which is not yet released but I hope it will happen soon (tm).

I think it's reasonable to keep this issue open until the new version is released to make it easier for people like yourself to find. :)

infogulch avatar Apr 11 '24 22:04 infogulch

With the release of 2.0, this feature request is resolved!

https://htmx.org/attributes/hx-swap-oob/#nested-oob-swaps

Set the htmx.config.allowNestedOobSwaps config option to false.

infogulch avatar Jun 19 '24 19:06 infogulch