bigskysoftware/htmx

`hx-swap-oob` strips away the outer element when used with `beforeend`

EmrysMyrddin opened this issue · 13 comments

Hi, I'm facing an issue with Out Of Band swapping feature. Perhaps I'm miss understanding how it works, in such case the documentation is not very clear about it.

My use case is the following:

<ul id="list-1">
  <li hx-post="/move/item-1/list-2" hx-swap="outerHTML">
    <div class="item">Item 1</div>
  </li>
</ul>
<ul id="list-2">
</ul>

I have 2 lists, and want to be able to move items from one list to the other.

The response of the /move/item-1/list-2 is the following:

<li hx-swap-oob="beforeend:#list-2"  hx-post="/move/item-1/list-2"  hx-swap="outerHTML">
  <div class="item">Item 1</div>
</li>

My expectation is to have this resulting DOM:

<ul id="list-1">
</ul>
<ul id="list-2">
  <li hx-post="/move/item-1/list-2"  hx-swap="outerHTML">
    <div class="item">Item 1</div>
  </li>
</ul>

It kind of work, since there is not other element than the OOB one, the first <li> is swapped away, but the new expected <li> is broken:

<ul id="list-1">
</ul>
<ul id="list-2">
  <div class="item">Item 1</div>
</ul>

For some reason, the <li> element which has the hx-swap-oob attribute is stripped away.

Is this an expected behavior ? It seems to be specific to before* and after* strategies. If it is, it should be documented.

I've worked around it for now by just wrapping the <li> into a <div>, moving the hx-swap-oob attribute to the <div>:

<div hx-swap-oob="beforeend:#list-2" >
  <li hx-post="/move/item-1/list-2"  hx-swap="outerHTML">
    <div class="item">Item 1</div>
  </li>
</div>

This gives the expected result:

<ul id="list-1">
</ul>
<ul id="list-2">
  <li hx-post="/move/item-1/list-2"  hx-swap="outerHTML">
    <div class="item">Item 1</div>
  </li>
</ul>

From my quick testing it seems that for normal swaps the html fragment is processed by makeFragment() and wrapped in a body tag as it is turned into html document fragment. This is then passed to all the different swapping functions and handled as expected.

But with oob swaps the fragment is not wrapped by makeFragment() with a body tag and this puts the data being swapped one layer higher which then impacts how the swaps work and seems to require that for most swap styles (which are all innerHTML style) you need to wrap your oob to be swapped in data inside a dummy div tag with the hx-swap-oob tag set. I don't know if this is really a big issue as it is much nicer to not be oob swapping in the hx-swap-oob attribute root anyway as this allows you to oob swap just the content you want without being forced to put hx-swap-oob tags on everything.

If you use outerHTML swap style for both oob or normal swaps then both methods basically work the same and it will replace the target with the root oob swap element you supplied.

So it seems there are some inconsistencies between the two swap methods but this may be by design? Changing all the innerHTML style ones to force you to put the oob root element in by default would make oob harder to use I think so maybe all we need is better documentation explaining why it works the way it does.

I'm not sure if it's a bug too.

But I'm not sure to agree that the current behavior makes it easier to use. It's actually the other way around in my case. I'm using Go, so this means I actually have to wrap my component (which is not that easy) instead of just using it add a the hx-oob-swap prop when rendering it.

And it's a clearly inconsistent with the way classic swap works. The current documentation pretty much clearly state that the behavior of OOB swap works exactly the same than classic swap (it even link to the swap documentation instead of having an explanation of each options in place).

For the "it is much nicer to not be oob swapping in the hx-swap-oob attribute" part, the documentation clearly states that those attributes are stripped away at swap time, so you should already not see those attributes in the DOM. It can be verified when using outerHTML strategy.

To circle back to an actual solution to this: Its probably way more easy to make a clear statement about it in the documentation than changing the behavior, which is a huge breaking change for anyone already using OOB with any other strategy other than the default one.

actually the hx-swap-oob attributes are not stripped away when using the deafult true/outerHTML inline swap style but I don't think they do any harm when they make it to the DOM this way.

It only strips these attributes if it finds them in your response and doesn't want to do the oob swap because you set htmx.config.allowNestedOobSwaps false to block deep nested oob swapping.

can you give me some feedback or suggestions on the quick PR I just put up to see if this makes things easier to understand or not.

Just ran into this myself! I was testing websockets and was surprised with the behavior. My simplified example of the issue (without websockets) now is:

<div
    id="test"
    hx-trigger="click"
    hx-get="/message"
    hx-swap="none"
>
    <p>I will start printing messages when clicked...</p>
</div>

And when clicking I'm swapping in:

<p hx-swap-oob="beforeend:#test">Message</p>

I expected to see:

I will start printing messages when clicked...
Message
Message
Message
Message

But instead I see:

I will start printing messages when clicked...
MessageMessageMessageMessage

I can definitely work around this by adding an extra tag around my returned data but it is very counterintuitive to see this behave differently than the standard non-oob "beforeend" swaps. I'll leave it up to collective wisdom but wanted to throw in a vote to have this changed to be consistent.

Thanks everyone!!!

I think I've found similar (maybe the same) problem, but in slightly different example and I think it's clearly a bug.

I have a form which is supposed to add a row to a table (tbody) (#target_tbody) on submit:

<form hx-post="/add-row" hx-target="#target_tbody" hx-swap="beforeend">

The response returned by /add-row endpoint is a table row with some cells and one oob swap of a div:

<div id="field-overlay" hx-swap-oob="true"></div>
<tr>
  <td>Name</td>
  <td>Description</td>
  <td><input type="checkbox"></td>
  <td><button>Some_button</button></td>
</tr>

Expected result is the <tr> is added at the end of #target_tbody and the div #field-overlay is replaced by empty div.

The actual result is unfortunately broken table - htmx strips <tr> tags and even <td> tags! (but the div is swapped correctly)

<table>
    (...)
    <tbody id="target_tbody">
        <tr><td>Existing content</td></tr>
        <tr><td>Existing content</td></tr>
        <tr><td>Existing content</td></tr>
        Name Description <input type="checkbox"><button>Some_button</button>
    </tbody>
</table>

When I remove <div> from the response, the row is added correctly.

Expected result should be:

<table>
    (...)
    <tbody id="target_tbody">
        <tr><td>Existing content</td></tr>
        <tr><td>Existing content</td></tr>
        <tr><td>Existing content</td></tr>
        <tr>
            <td>Name</td>
            <td>Description</td>
            <td><input type="checkbox"></td>
            <td><button>Some_button</button></td>
        </tr>
    </tbody>
</table>

I thought it may be related to <template> tags, but it should be used only if I want to include <tr> to swap oob.

Just ran into this myself! I was testing websockets and was surprised with the behavior. My simplified example of the issue (without websockets) now is:

<div
    id="test"
    hx-trigger="click"
    hx-get="/message"
    hx-swap="none"
>
    <p>I will start printing messages when clicked...</p>
</div>

And when clicking I'm swapping in:

<p hx-swap-oob="beforeend:#test">Message</p>

Yeah the issue is that beforeend is a inner style swap just like innerHTML that replaces the inner content of a tag and you need the option to be able to swap in just text with no wrapping tags sometimes so you could have the option to insert text without the

tags if required. With normal swaps this is simple but with oob swaps this would not be possible if it always swapped the outerHTML content of the tag containing hx-swap-oob. So to give this flexibility i'm guessing it was coded to work this way to cover all use cases. But it does cause confusion as it does work a little differently to normal swaps so hopefully the documentation update i've proposed makes it easier to understand.

I think I've found similar (maybe the same) problem, but in slightly different example and I think it's clearly a bug.

@wojciech-mazur-mlg I think it is best to have your oob swap divs at the end after your main content to avoid such problems. From my quick testing moving the div to the bottom resolved the issue. I think there is an issue if the content is that htmx is trying to handle and process many complex different formats and try and handle partial content like tr's which can not stand on their own in html. It does this by detecting partial content and wrapping it in a template tag to allow such content to be processed by the browser as valid content. The first tag it finds with this method is handled well by the template tag (in your case the oob div) and then later tags seem to not be handled well. This is why it is recommended to wrap any oob swap content tags that you place at the bottom of your content in template tags if they have certain tags. Anyway I think if you follow the standard pattern you should be fine.

@MichaelWest22 Thanks for your response. It's interesting, but I tested moving div with hx-swap-oob to the end of the response, but then nothing is replaced. Neither a new row is not added nor the div is cleared.

@MichaelWest22 This makes perfect sense! Thanks for the explanation. I think your recommendation to update the documentation does seem to make the most sense. Excellent point we may want to update the inner tag text only. Appreciate it!