htmx
htmx copied to clipboard
Add support for hx-swap-oob-source as "firstChild" (or "innerHTML")
Description
Added support for a new hx-swap-oob-source
attribute which presently can only take one value, firstChild
(in line with the DocumentFragment API.) If set to firstChild
, the swapped node is not the node with the hx-swap-oob
attribute but its first child.
At the moment this silently fails if no child node is passed, but it might be desirable to throw an error, which I'd be happy to implement.
Corresponding issue: https://github.com/bigskysoftware/htmx/issues/423
Testing
I added one test to the hx-swap-oob suite.
Checklist
- [x] I have read the contribution guidelines
- [x] 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 No, but the change is so minimal that I thought I'd offer an actual PR
- [x] I ran the test suite locally (
npm run test
) and verified that it succeeded
Actually, firstChild
might not be the right terminology, as several children inside would all be swapped. Perhaps innerHTML
then?
Why would you want to not swap inline? Can you provide more details about the use case you are trying to cover here?
The reason I am asking is that I think this functionality is not usable nor scalable. The persistance of OOB swap target across renders is what ensures that the same code can be run again. In your example, running the test twice will fail because the target will have been swallowed by the swap.
OOB swaps are very useful for things like toasts or alerts that you would want to display somewhere else as a side effect of a request. In most (all?) cases where an OOB swap is useful, you would want to keep the target element so that it can be reused for following swaps.
If you come up with a proper use case, I think it will help understanding what you are trying to do.
Hi @Atmos4
You make a good point regarding repeatability. My use case is particularly related to component-based templates (say with a JavaScript backend or even Laravel Blade components and such like.)
What I wanted to avoid was to have any kind of oob-related attributes as part of my components' definition. Say I have the following Toast component in JSX (with an Elysia backend for instance)
type ToastProps = {
title: string;
content: string;
type: 'info' | 'success' | 'alert';
}
function Toast({ title, content, type }: ToastProps) {
return (
<div class="toast">
<h3>${title}</h3>
<p>${content}</p>
</div>
);
}
It would be neat to be able to do something like:
<div hx-swap-oob="beforeend:#toasts" hx-swap-oob-source="firstChild">
<Toast .../>
</div>
Rather than having to add extra, purely oob-related complexity to the Toast component (assuming I have defined an extra MaybeOobProps
type:)
function Toast({ title, content, type, ...oobProps }: ToastProps & MaybeOobProps) {
return (
<div class="toast" {...oobProps}>
<h3>${title}</h3>
<p>${content}</p>
</div>
);
}
Admittedly this is not a huge increase in complexity, but I appreciated the fact that the Toast component - or any other that might be swapped out of band - could define their own logic without having to know whether they would be swapped OOB or not.
Am I missing something? Do you have any tips regarding component-based architectures for views?
I would suggest against reasoning with framework-specific logic. All you need to be concerned about is the HTML you render, because this is what will be swapped by HTMX.
You may want it to always swap your toasts at the same place, e.g. #toast-root
.
Take this example structure:
<body>
<main>
<div hx-get="/my-content" hx-trigger="load" hx-swap="innerHTML"></div>
</main>
<div id="toast-root">
<!-- your toasts -->
</div>
</body>
Let's say that you want to OOB swap a toast to #toast-root
when requesting GET /my-content
. Then you can return this HTML:
<!-- example content of the request -->
<p>Hello world</p>
<!-- oob swap -->
<div id="toast-root" hx-swap-oob="innerHTML">
<!-- your toast -->
</div>
If you want to learn more about out-of-band swaps and how it works, I suggest taking a look at the docs and experimenting by yourself as it is the best way to learn!
Okay, thank you for taking the time to write this. I understand the framework-agnostic approach, and wouldn't have made the PR if I thought this would somehow bloat the core in a significant way. However, based on the scalability and repeatability issues you mentioned, it makes sense to no want to include this in the core. I'll figure out a way to keep things clean in my templates.
I also understand that the extension API is evolving in 2.0 - we might have access to more internal methods? - so I might be able to do something with that.
Is my approach solving your problem though? Let me know if you need more help, but it sounds like what I described above is what you wanted to do!
It might also be that I understood the problem you are trying to solve completely wrong 😅
Here is what the above code results to. This is from a website I made with HTMX:
The logic is almost identical, but I use hx-swap-oob="afterbegin"
to prepend the items to #toast-root
, then I can have multiple toasts
Then I use some JS to make them disappear after some delay
Dear @Atmos4,
Although I am grateful you took the time to pitch in, I didn't learn anything new from your hints, in the sense that I was well aware of how oob works, of their purpose, and of the documentation.
My concerns were not why and how to "spit them out" in the final HTML, but rather how to not bloat up my components with purely oob-related logic. Admittedly this is somewhat framework-dependent, but many frameworks - not only JS ones - do use the component paradigm for their views (Laravel Blade templates, for one.)
In any case, I think I have found an approach I am satisfied with which wouldn't require adding support for a new oob attribute and wouldn't require my components to handle oob logic, which is basically the way "Click to Load" is implemented.
It looks like this:
<body>
<main>
<div hx-get="/my-content" hx-trigger="load" hx-swap="innerHTML"></div>
</main>
<div id="toast-root">
<!-- your toasts -->
<div id="toasts-next"></div>
</div>
</body>
And then on GET /my-content
I could return:
<!-- example content of the request -->
<p>Hello world</p>
<!-- oob swap with embedded toasts-next-->
<div hx-swap-oob="outerHTML:#toasts-next">
<Toast />
<div id="toasts-next"></div>
</div>
I didn't learn anything new from your hints
I think you did learn something new :) otherwise you would not have:
- closed your PR
- reused my code in your own example
I am glad I could help in some small way then!
It can be further simplified though, similar to how I have done in my example. Your GET /content
response can look like this:
<div id="toast-root" hx-swap-oob="innerHTML"> <!-- or afterbegin -->
<Toast />
</div>
I think I would still need to embed a new #toast-next
container in my response, wouldn't I? Otherwise it will break subsequent oobs (in the same way that the Click To Load implementation has a 'special' #replaceMe
final row, which is nested/reincluded on each further load more call)
click to load
This example isn't about OOB at all 🙈 instead you should check out Updating other content
I took some time to make a CodePen, hopefully it helps!
The example is not about oob per se, but the 'embedding trick' used (bringing along a nested container that acts as the recipient for subsequent swaps) is definitely helpful for what I was trying to accomplish.
I have basically come full circle on this one. My original implementation of simply using
<div hx-swap-oob="beforeend:#toasts">
<div class="toast toast-info">Toast content</div>
</div>
Works as expected and results in the following DOM:
<body>
<div id="toasts" class="fixed top flex etc">
<div class="toast toast-info">Toast content</div>
</div>
</body>
For whatever reason, I had earlier had the parent container, which holds the oob attribute, swapped in as well, resulting in the following DOM after the oob swap:
<body>
<div id="toasts" class="fixed top flex etc>
<div hx-swap-oob="beforeend:#toasts">
<div class="toast toast-info">Toast content</div>
</div>
</div>
</body>
Not exactly sure why this is now working as expected, and why it wasn't before, but I will post if I discover anything more.
Instead of beforeend:#toasts
, maybe you should just use id=toasts hx-swap-oob=beforeend
. I have never tried using a selector, but this is maybe what is wrong with your code.
Have you even tried some of the solutions I had? For example this CodePen.
You seem to be very resistant to learning 😅