Add config option to ignore nested oob-swaps instead of processing them
This adds a new configuration option, allowNestedOobSwaps, which preserves existing behavior when set to the default true, but when set to false it changes handleOutOfBandSwaps to only process elements with hx-swap-oob and data-hx-swap-oob that appear at the root of the response fragment, i.e. elements whose parent element is body. Elements deeper in the dom tree fragment are ignored and the oob-swap attribute is stripped instead.
The current system means you can just randomly intersperse oob fragments anywhere within the main response and they are swapped in from wherever to wherever. It seems much more conceptually sound to require all oob-swaps to originate adjacent to the response's main element.
Enabling this configuration option is helpful when you want to reuse a small html fragment to render both as as an element within a larger fragment (say a page) as well as more fine grained interactions where the small fragment is updated out of band. By setting allowNestedOobSwaps: false, you can place hx-swap-oob="true" on the fragment and leave it: when a full page renders then the attribute ignored since the fragment is nested inside the main response, but if you do want to update the fragment out of band then render it adjacent to the to the root response element and htmx performs the oob swap as expected. However in this scenario if allowNestedOobSwaps: true (default) then the small fragment is removed from the root element first and then the root element is swapped leaving you confused about why the small fragment vanished.
Here's an example of how this simplifies htmx usage in Go templates. In particular, note how this eliminates the conditional logic that controls whether hx-swap-oob="true" is included in the response or not: https://github.com/infogulch/go-htmx/commit/ea227b68fe8d7e6df9996f7d13eb951ed046f1ee
This PR implements the feature request in #1133
This looks good—could you add tests that validate the new functionality?
I added a test that should show the new behavior, but I'm not sure it's correct.
I updated the test so it should pass if it were to run again.
That said I'm not sure if the behavior is exactly what we want. Specifically, should the final innerHTML contain the hx-swap-oob attribute or should it be stripped? The point of this PR is to preserve the <span> in this scenario, but my question now is should it preserve the hx attribute?
btn.innerHTML.should.equal('<button>Clicked <span id="when" hx-swap-oob="true">now!</span></button>');
Specifically, should the final innerHTML contain the hx-swap-oob attribute or should it be stripped?
What happens for a successful OOB swap? Is the attribute there? It should match that behavior.
I updated the test so it should pass if it were to run again.
Unfortunately it does not pass, but you can run it locally with npm run test
Also, if you could update the swap-oob docs to explicitly say that elements with swap-oob not in the root are ignored that would be great.
Ok I looked into this further and this PR seems to be in tension with the 'oob swaps can be nested in content' test.
The question is: What to do when an HX response body contains a nested element with the hx-swap-oob attribute? (The case of subsequent root elements with the hx-swap-oob attr is already clear.)
- The status quo's answer is to remove them from their parent element and swap them into the document in parallel with the parent element.
- The downside is that if you want the child element to stay with its parent then you have to strip off the
hx-swap-oobbefore sending depending on if you send the parent and child or just the child element, and it's a bit of a pain to plumb conditionals everywhere.
- The downside is that if you want the child element to stay with its parent then you have to strip off the
- My conception, and this PR, would change that behavior to assume that the intention is to keep the nested element together with its parent and ignore the attribute. This lets you reuse nested fragments depending on the need and not have to worry about whether the inner element contains a
hx-swap-oobattr.- The downside is that this changes current behavior.
IMO, 'oob swaps can be nested in content' seems kinda silly; if you want to do an oob swap then return it at the root, adjacent to the main response. But the current system means you can just randomly intersperse parts of the page anywhere within the main response and its just swapped in from wherever to wherever. It seems much more conceptually sound to require all oob-swaps to originate adjacent to the response's root element, and consider oob-swaps elsewhere to be superfluous and ignore them.
I wanted to surface this difference and get feedback before I proceed due to the behavior change. Thoughts?
It seems much more conceptually sound to require all oob-swaps to originate adjacent to the response's root element, and consider oob-swaps elsewhere to be superfluous and ignore them.
Very hard to know if anyone is relying on this behavior, though I agree with you that, conceptually, it's a little absurd.
I will note that the documentation currently warns: “Out of band elements must be in the top level of the response, and not children of the top level elements.”
Perhaps adding another property like hx-swap-oob=“true with-depth” or a server response header HX-Swap-OOB-Depth: true? Just some way to either reverse new functionality (or to set new functionality to keep backwards compatibility)
This has been paused for a while, but I'm currently making this change as a configuration option to switch behaviors. A 2.0 release might consider changing the default for this configuration option.
Yeah I apologize about this being stalled, it's a tough one for the reasons identified. We're closing in on having many fewer PRs, which will make it easier to focus on the hard choices. I think a config is a good call.
With this change, the current behavior is preserved by default, and can be changed by adjusting the config option. Perhaps a later version could change the default and eventually remove the config option.
The description has been updated to add my motivation for opening this PR.
Just off the top of my head here are a couple things that I think would improve it:
- Update config listing
- Update docs to reflect how oob swaps work when the config is set to both settings
Should I continue working on this? What are htmx collaborators' disposition to this change?
I like the config option and I'm going to push for it. I'll get you back a final up or down soon (< a week). Would probably be against the htmx 2 branch, if that's alright?
A good reason to support this, which I think you're calling out, is that it composes really well with template fragments. You can send the fragment as an OOB swap, or just send the template normally.
A good reason to support this is that it composes really well with template fragments. You can send the fragment as an OOB swap, or just send the template normally.
Go ahead and just summarize my giant meandering paragraphs in two pithy sentences. It's fine. I'm not mad. Why would I be mad? I'm a great writer I'll have you know. 😡😆😭
If you weren't a good writer I wouldn't have know what you were talking about ☺️ I have the advantage of not having to also explain all the technical details.
Anyway, got the go-ahead! Can you rebase again the 2.0 branch and add the docs?
Great! I'll get it done.
- [x] Rebase changes onto
v2.0v2.0branch, assuming that's the right one - [x] Update the configuration listings (both of them, thank you @infogulch for that)
- [x] Add a short explanation of the config option on the
hx-swap-oobattribute page - [x] Fix lint errors
- [x] Fix issues with
parentNode === null - [x] Add tests for
htmx.config.allowNestedOobSwaps+ template node - [x] Add system to save/restore config between tests to test specific configurations
- [x] Duplicate various
hx-swap-oobtests with different settings forhtmx.config.allowNestedOobSwapsto validate that they work - [x] Add tests for when the response is wrapped in html and/or body elements
@infogulch ready for review?
@alexpetros yessir