htmx icon indicating copy to clipboard operation
htmx copied to clipboard

Add config option to ignore nested oob-swaps instead of processing them

Open infogulch opened this issue 2 years ago • 19 comments

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

infogulch avatar Feb 01 '23 22:02 infogulch

This looks good—could you add tests that validate the new functionality?

alexpetros avatar Jul 18 '23 17:07 alexpetros

I added a test that should show the new behavior, but I'm not sure it's correct.

infogulch avatar Jul 24 '23 17:07 infogulch

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>');

infogulch avatar Jul 26 '23 23:07 infogulch

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.

alexpetros avatar Jul 30 '23 17:07 alexpetros

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.)

  1. 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-oob before 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.
  2. 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-oob attr.
    • 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?

infogulch avatar Aug 22 '23 20:08 infogulch

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.

alexpetros avatar Aug 22 '23 22:08 alexpetros

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)

Itzdlg avatar Sep 30 '23 21:09 Itzdlg

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.

infogulch avatar Nov 08 '23 20:11 infogulch

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.

alexpetros avatar Nov 08 '23 21:11 alexpetros

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.

infogulch avatar Feb 12 '24 22:02 infogulch

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?

infogulch avatar Feb 25 '24 17:02 infogulch

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?

alexpetros avatar Feb 25 '24 23:02 alexpetros

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.

alexpetros avatar Feb 25 '24 23:02 alexpetros

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. 😡😆😭

infogulch avatar Feb 26 '24 20:02 infogulch

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?

alexpetros avatar Feb 26 '24 20:02 alexpetros

Great! I'll get it done.

infogulch avatar Feb 26 '24 20:02 infogulch

  • [x] Rebase changes onto v2.0v2.0 branch, 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-oob attribute 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-oob tests with different settings for htmx.config.allowNestedOobSwaps to validate that they work
  • [x] Add tests for when the response is wrapped in html and/or body elements

infogulch avatar Feb 27 '24 04:02 infogulch

@infogulch ready for review?

alexpetros avatar Feb 29 '24 16:02 alexpetros

@alexpetros yessir

infogulch avatar Feb 29 '24 19:02 infogulch