Skip to content

fix(swap): find oob targets in the document when the context element is detached - #3998

Open
yfwmaniish wants to merge 1 commit into
bigskysoftware:devfrom
yfwmaniish:fix/oob-swap-detached-context-element
Open

fix(swap): find oob targets in the document when the context element is detached#3998
yfwmaniish wants to merge 1 commit into
bigskysoftware:devfrom
yfwmaniish:fix/oob-swap-detached-context-element

Conversation

@yfwmaniish

Copy link
Copy Markdown

Fixes #3959.

What

swap() picks the rootNode used for hx-swap-oob target lookup by calling getRootNode() on the request's contextElement (the element that issued it):

const rootNode = swapOptions.contextElement ? getRootNode(swapOptions.contextElement, false) : getDocument()

That's correct while contextElement is still in the document - it's exactly what #2846 needed to make OOB swaps work inside shadow roots. But if contextElement has since been detached (its own region re-rendered while the request was in flight - e.g. a row in a list whose own hx-get is pending when the list itself gets replaced), elt.getRootNode() on a detached node returns its orphaned subtree's own root, not the document. oobSwap()'s querySelectorAllExt(rootNode, selector, false) then searches that orphaned root and finds nothing, so every OOB fragment in the response gets silently dropped (htmx:oobErrorNoTarget in the console is the only sign) - even though its target is still live in the document, and even though the response's main swap applies normally.

Fix

const rootNode = (swapOptions.contextElement && swapOptions.contextElement.isConnected)
  ? getRootNode(swapOptions.contextElement, false)
  : getDocument()

Falls back to the document when contextElement is disconnected, leaving the connected-trigger / shadow-root behavior #2846 added completely untouched.

Testing

  • Added a test to test/attributes/hx-swap-oob.js: click a trigger, detach it (.remove()) before the response lands, and assert a separate, still-in-document OOB target still receives its content.
  • Negative control: reverted just src/htmx.js, kept the test - it fails with the target's innerHTML staying empty (the drop), all other 33 tests in the file still pass. Restored.
  • Full suite: 848/848 passing (3 pre-existing skips, unrelated), including both existing shadow-DOM OOB tests from fix for hx-swab-oob within web components #2846 - confirming the connected/shadow-root case is untouched.
  • eslint clean on both changed files.

PR targets dev per CONTRIBUTING.md.

Copilot AI lite review requested due to automatic review settings September 1, 2026 09:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…is detached

swap() picks the rootNode for hx-swap-oob target lookup by calling
getRootNode() on the request's contextElement (the element that
issued it). That was fine when contextElement is still in the
document - it's how bigskysoftware#2846 made OOB swaps work inside shadow roots -
but if contextElement has since been detached (its own region
re-rendered while the request was in flight), getRootNode() returns
its orphaned subtree's own root, not the document. oobSwap()'s
selector lookup against that orphaned root finds nothing, so every
OOB fragment in the response is silently dropped (htmx:oobErrorNoTarget)
even though its target is still live in the document, and even though
the response's main swap still applies normally.

Falling back to the document when contextElement.isConnected is false
fixes the drop while leaving the connected-trigger / shadow-root case
bigskysoftware#2846 added completely untouched.

Fixes bigskysoftware#3959.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants