Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update hx-swap-oob documentation/examples #2560

Closed

Conversation

Keeper-of-the-Keys
Copy link
Contributor

@Keeper-of-the-Keys Keeper-of-the-Keys commented May 23, 2024

Add around returned in example 2 (hx-swap-oob) since this is required by strict browser engines as described - https://stackoverflow.com/questions/76612680/htmx-hx-swap-oob-adding-a-new-table-row-only-adds-the-tds-and-not-the-wrappin

Description

This brings the example in the documentation in line with what works in Firefox (126) and Chrome (124), for my enabling/disabling htmx.config.useTemplateFragments = true; has no effect but enclosing the hx-swap-oob in the appropriate tags did work.

Corresponding issue:
#1900
#1198

Testing

This is documentation but I used the following PHP/HTML to test my theories and based on it there is I think even more that needs to be changed in the documentation but I am unsure of the way to formulate things:

<?php

if (isset($_GET['test'])) {
	switch($_GET['test']) {
	case 1:?>
<tr hx-swap-oob="beforeend:#table tbody">
	<td>1 tr</td>
	<td>2 tr</td>
	<td>3 tr</td>
</tr>
<tr hx-swap-oob="beforeend:#table2">
	<td>1 tr</td>
	<td>2 tr</td>
	<td>3 tr</td>
</tr>
<?php
	break;
	
	case 2:?>
<div hx-swap-oob="beforeend:#table tbody">
<tr>
	<td>1 div tr</td>
	<td>2 div tr</td>
	<td>3 div tr</td>
</tr>
</div>
<div hx-swap-oob="beforeend:#table2">
<tr>
	<td>1 div tr</td>
	<td>2 div tr</td>
	<td>3 div tr</td>
</tr>
</div>
<?php
	break;
	case 3:?>
<tbody hx-swap-oob="beforeend:#table tbody">
<tr>
	<td>1 tbody tr</td>
	<td>2 tbody tr</td>
	<td>3 tbody tr</td>
</tr>
</tbody>
<tbody hx-swap-oob="beforeend:#table2">
<tr>
	<td>1 tbody tr</td>
	<td>2 tbody tr</td>
	<td>3 tbody tr</td>
</tr>
</tbody>
<?php
	break;
	case 4:?>
<li hx-swap-oob="beforeend:#list1">new item li</li>
<?php
	break;
	case 5:?>
<ul hx-swap-oob="beforeend:#list1"><li>new item ul li</li></ul>
<?php
	break;
	case 6:?>
<div hx-swap-oob="beforeend:#list1"><li>new item div li</li></div>
<?php
	break;
	case 7:?>
<li hx-swap-oob="beforeend:#list1"><li>new item li li</li></li>
<?php
	break;
	case 8:?>
<p hx-swap-oob="beforeend:#text">new paragraph p</p>
<?php
	break;
	case 9:?>
<div hx-swap-oob="beforeend:#text"><p>new paragraph div p</p></div>
<?php
	break;
	case 10:?>
<p hx-swap-oob="beforeend:#text"><p>new paragraph p p</p></p>
<?php
	break;
	case 11:?>
<span hx-swap-oob="beforeend:#text"><p>new paragraph span p</p></span>
<?php
	break;
	}
} else {?>

<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<script src="https://unpkg.com/[email protected]"></script>

<table id="table" class="table">
	<caption>With thead/tbody</caption>
	<thead>
		<tr>
			<td>Col 1</td>
			<td>Col 2</td>
			<td>Col 3</td>
		</tr>
	</thead>
	<tbody>
		<tr>
			<td>row 1-1</td>
			<td>row 1-2</td>
			<td>row 1-3</td>
		</tr>
	</tbody>
</table>
<table id="table2" class="table">
	<caption>No thead/tbody</caption>
	<tr>
		<th>Col 1</th>
		<th>Col 2</th>
		<th>Col 3</th>
	</tr>
	<tr>
		<td>row 1-1</td>
		<td>row 1-2</td>
		<td>row 1-3</td>
	</tr>
</table>
<button hx-get="?test=1" hx-swap="none">test1 tr</button>
<button hx-get="?test=2" hx-swap="none">test2 div</button>
<button hx-get="?test=3" hx-swap="none">test3 tbody</button>

<ul id="list1">
	<li>item 1</li>
	<li>item 2</li>
	<li>item 3</li>
</ul>
<button hx-get="?test=4" hx-swap="none">test4 li</button>
<button hx-get="?test=5" hx-swap="none">test5 ul li</button>
<button hx-get="?test=6" hx-swap="none">test6 div li</button>
<button hx-get="?test=7" hx-swap="none">test7 li li</button>

<div id="text">
	<p>paragraph1</p>
	<p>paragraph2</p>
</div>
<button hx-get="?test=8" hx-swap="none">test8 p</button>
<button hx-get="?test=9" hx-swap="none">test9 div p</button>
<button hx-get="?test=10" hx-swap="none">test10 p p</button>
<button hx-get="?test=11" hx-swap="none">test11 span p</button>


<?php
}
?>

Checklist

  • I have read the contribution guidelines
  • I have targeted this PR against the correct branch (master for website changes, dev for
    source changes)
  • This is either a bugfix, a documentation update, or a new feature that has been explicitly
    approved via an issue
    * [ ] I ran the test suite locally (npm run test) and verified that it succeeded Not applicable for a documentation change

Add <tbody> around returned <tr> in example 2 (hx-swap-oob) since this is required by strict browser engines as described - 
https://stackoverflow.com/questions/76612680/htmx-hx-swap-oob-adding-a-new-table-row-only-adds-the-tds-and-not-the-wrappin
bigskysoftware#1900
bigskysoftware#1198
@Keeper-of-the-Keys
Copy link
Contributor Author

From my basic tests what I saw was that the element with the hx-swap-oob property was stripped and not inserted in the target element, thus if like in the current example you would return

<tr hx-swap-oob="beforeend:#contacts-table">
    <td>Joe Smith</td>
    <td>[email protected]</td>
</tr>

Only the two <td> elements would be inserted, or if you return just an <li> only it's enclosed content would be inserted and thus you are required to enclose the element, for a <tr> it would seem a <tbody> is required whereas for other elements <div> or <span> seems to also work.

Based on all these observations IMHO this is not a bug in htmx as others suspected but just a bug in the documentation on how to use hx-swap-oob since it makes no mention or suggestion of the containing element not being inserted.

@Telroshan Telroshan added the documentation Improvements or additions to documentation label May 23, 2024
@Keeper-of-the-Keys Keeper-of-the-Keys changed the title Update update-other-content.md Update hx-swap-oob documentation/examples May 23, 2024
@Keeper-of-the-Keys
Copy link
Contributor Author

Keeper-of-the-Keys commented May 23, 2024

I hope that my previous two messages weren't too rambling they were done very late at night/early in the morning for me, I actually thought out a more generalized way of saying what I was trying to say -

If I return the following

<elementX hx-swap-oob="beforeend:#someid">Content</elementX>

Then based on the documentation and all the given examples and other behavior of htmx I would expect the result of this action to be:

<elementA id="someid">
   <elementPreexistingChild></elementPreexistingChild>
   <elementX>Content</elementX>
</elementA>

Based on my tests what actually happens is

<elementA id="someid">
   <elementPreexistingChild></elementPreexistingChild>
   Content
</elementA>

Thus elementX needs to be enclosed in a container, in addition to this there seems to be a dependency on what elementX is as to what containing elements will be acceptable (specifically table elements need a valid table container like tbody).

Based on some further testing the only hx-swap option that would preserve elementX is outerHTML.

As stated previously I do not think this is a bug in htmx, rather it is an omission of a behavior in the documentation.

@alexpetros
Copy link
Collaborator

Yeah, table stuff is a nightmare, I appreciate you doing this. Would mind adding a note in the documentation about why the tbody is necessary?

@Keeper-of-the-Keys
Copy link
Contributor Author

Hey it's been a while since I ran all these tests and htmx 2.0 was also released in the mean time, I'll try to make some time to test again and expand the documentation because I may be wrong but I seem to recall that some time after filing this I ran into an opposite case with oob-swap.

@alexpetros
Copy link
Collaborator

alexpetros commented Aug 21, 2024

Sure thing, I'm gonna close this for now, and feel free to re-open if/when you get around to it :)

@alexpetros alexpetros closed this Aug 21, 2024
@Keeper-of-the-Keys
Copy link
Contributor Author

Hey just swapped [email protected] into my test code and the issue still exists.

Based on my finding the containing tags is are stripped, where the context of what the containing tags are and what the target is also matters examples:

  • li needs to be contained in div, ul or ol but not in li, p or nothing
  • table elements are even more finicky and depend on the way the table is built:
    • when returning a tr to a table that has no thead/tbody it needs to be enclosed in a table
    • when returning a tr to a table with thead/tbody it needs to be in the relevant thead or tbody (and presumably tfoot but I did not test that).
  • p needs to be enclosed in a div or span

@Keeper-of-the-Keys
Copy link
Contributor Author

So adding tbody is the right answer for the context of the example in the documentation but the actual answer is much more complicated and I'm not really sure how that should be documented.

@alexpetros please consider re-opening and merging, if you have suggestions on how to document it better I am more than happy to do so, the PR as is makes sure that the example in the documentation works.

@Telroshan Telroshan reopened this Sep 2, 2024
@alexpetros
Copy link
Collaborator

Unfortunately, the table handling is just wonky all around. Probably some of it is our fault, some of it is because tables do not play nice with partial page replacement at all. We have random caveats all around the documentation about it. But, we're happy to have the specific example fixed! If you rebase I'll merge it.

@Keeper-of-the-Keys
Copy link
Contributor Author

But should the other examples also be added?

@alexpetros
Copy link
Collaborator

alexpetros commented Sep 2, 2024

Taking a look at the recommendations in the oob-swap docs—would most of these be solved by just using a <template>?

@Keeper-of-the-Keys
Copy link
Contributor Author

Just tested it on firefox 129 with htmx 2.0.2 and no, it would seem that that documentation is also inaccurate:

<template>
    <TAG hx-swap-oob="#target">
           content
    </TAG>
</template>

where TAG is any of tr, li, p will result in both template and TAG being stripped.

The following (not according to the docs):

<template hx-swap-oob="#target">
    <TAG>
         content
    </TAG>
</template>

results in the whole reply being ignored for all TAGs I tested (tr, li, p)

@Keeper-of-the-Keys
Copy link
Contributor Author

Keeper-of-the-Keys commented Sep 2, 2024

I'm rebasing and will try to see when I have time to read both pages and maybe update them more.

Either way the issue is not just tables and not just elements that

can’t stand on their own in the DOM

since it would seem all containers are stripped.

@Keeper-of-the-Keys
Copy link
Contributor Author

Keeper-of-the-Keys commented Sep 2, 2024

💡
Most of the documentation about hx-swap-oob assumes a outerHTML strategy in which case the container is maintained, but all the other strategies result in the container being stripped.

@alexpetros
Copy link
Collaborator

Most of the documentation about hx-swap-oob assumes a outerHTML strategy in which case the container is maintained, but all the other strategies result in the container being stripped.

Yes, and it's definitely a little bit of trail an error to get a sense of what gets stripped and what doesn't. It's not perfect.

My main feeling about this is that some of these details are important, and some of them you don't want to overwhelm the documentation with. Like the oob section is literally called "troublesome tables", which very effectively warns you: "tables will be annoying".

So we'd love more precise documentation on this, if you think you can do it without sacrificing the general approachability.

@Keeper-of-the-Keys
Copy link
Contributor Author

I see that someone else already got this change merged - a191588

I'll try to have a look at the OOB page.

@MichaelWest22
Copy link
Contributor

someone reported the same issue in #2790 and we had a discussion there. I put up a small documentation update in #2806 that addresses the same core issue. Your documentation is similar but has more detail on the tables situations. So i'm not too fussed which PR wins the day. I also have #2823 addresses some of the confusion around the fact it places the attribute into the final page which is not ideal. I also just added some SVG examples in recently.

There is much confusion around this behavior because it is not quite the same as full page load behavior.

I notice one issue when you were testing you had :

<template>
    <TAG hx-swap-oob="#target">
           content
    </TAG>
</template>

And this is missing the format : which i think may default to innerHTML which explains why it was doing inner swaps.

There are separate issues at play here. First is that there is a inline swap called outerHTML that replaces an existing element on the page and then the rest of the swap strategies have to follow a different pattern. This non inline swap pattern is based on innerHTML where you replace the inside of a tagged element and not the outer tag itself. For this swap to work with oob swaps it has to use the OOB wrapping tag's inner contents as the source of the swap because otherwise your swap options would be very limited and you would only be able to swap in full tags and you couldn't update the innerHTML text of an element freely. We can't really go back and change this behavior either without breaking half the existing users of oob swaps. Then we have the other similar inner html replacement options like beforebegin and afterend etc which also replace inner contents of a tag by appending inside the tag or place some inner content just before or after the selected tag. You could be placing some text or a tag or anything so we have to wrap these in a wrapper to support all cases. We can also use the wrapping tag that is thrown away for really usefull things like making the element valid during fragment text to HTML document conversion.

Which leads to the second issue at play here which is that many elements have rules when converting them from text into a HTML document that htmx can work with to do DOM replacements with. You have to live by the browsers html parsers rules which is where template, table, span and div wrappers have to come in. The rules are not that complicated but it is still confusing and listing every combination and trying to explain everything to the users of the documentation is probably going to be too much. But maybe your PR may be a good balance of this. Users knowing they can wrap the sensitive content in the proper wrapper is ideal.

Finally there is some quirks in the way it has to process things of different content types. often with OOB swaps you have the main response with one type of component and then you have some oob swap content with a different tag type and they do not always mix well which I think is why template tags often magically solve things and its not always obvious why. I think what is happening is htmx has logic for full swaps that it will detect the first tag type it sees and it has some conditional logic to adjust and wrap the content if required for that tag type. So when you place your oob tag type first in the response it wraps it one way possibly breaking the content below by stripping the incompatible tags (turning them to just text). Or you have your oob tags at the bottom after the main body content and the tag types are not compatible to sit below or next to the main body tags and so your oob tags get ruined. This may be where template tag wrapping is magically solving things but getting it right is confusing. Using tags and wrappers to make all the content a single valid chunk of html before you split it up into oob and main content is probably the key.

@Keeper-of-the-Keys
Copy link
Contributor Author

Hey @MichaelWest22 this PR was superseded by #2889 which IMHO is a much better addition to the documentation (though lacking in the department of nifty titles).

As for the issue you raised about the missing swap-strategy: that is a mistake on my part when I typed up the abstracted example, I was under the impression that the default swap strategy for OOB swaps was outerHTML but based on further tests it actually just doesn't work if no strategy is provided and seems to behave like innerHTML when set to true instead of a specific swap strategy.

@Keeper-of-the-Keys
Copy link
Contributor Author

I'm very confused now, I just realized that the documentation does say that outerHTML and true are equivalent which would mean that it is the default behavior, but the documentation also says at the end that the default is innerHTML and actually the default is none since it just doesn't work when no swap-strategy is provided.

I don't mind doing a further clarification on that in #2889 but I don't know what I would need to write now with all these contradictions.

@MichaelWest22
Copy link
Contributor

MichaelWest22 commented Sep 13, 2024

yeah the true does cause some confusion but I think it is fine as it is. html often has attributes like hidden="true" so htmx copies this and picks the most obvious oob strategy which is outerHTML and has made "true" implement this swap style making it read very naturaly for this useful case. But true is NOT a swap strategy itself and so you can't use it in the <swap>:<css selector> format and it only matches on the exact string "true". hx-swap-oob="true:#id" doesn't read that well either. So you have to use explicit swap strategies for all but this obvious case. Also there is a config item for default swap strategy that defaults to innerHTML but you can change it per app if required. If you do hx-swap-oob="banana:#id" or any other invalid swap mode it will pass this string to the extensions to see if you have a banana swap plugin installed and then if none are found fall back to the default probably innerHTML swap style. It doesn't warn that its ignored the value and picked the default.

@Keeper-of-the-Keys
Copy link
Contributor Author

That is very strange behavior IMHO, because it makes absolutely no sense to use hx-swap-oob without a target css selector, so if true does not parse like that and falls back to the default innerHTML but the "more logical" default that it would use if no css selector is provided is outerHTML shouldn't the default just be outerHTML or alternatively the whole business about true should not be mentioned?

@MichaelWest22
Copy link
Contributor

I would go re-read the first couple of paragraphs in the oob-swap-oob documentation which you may have missed while going down the very complex rabbit hole. The most common default use case for oob-swap-oob is to just use ="true" where you place this on a element with a id. like:

<div id="alerts" hx-swap-oob="true">
    Saved!
</div>

when there is no css selector falls back to using the id of the tag so this tag magically gets swapped into the page and overwrites the alerts element with just this one simple attribute added. The other alternative swap and css selector options are the other few cases where a more complex solution is required where you want to insert inner html somewhere instead of inline replacing an existing element.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants