From ce6eb12d8057044bf826b5bd24e6fc54ab73c257 Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Fri, 28 Aug 2026 13:10:01 +1200 Subject: [PATCH 1/2] Fix: outerHTML swap with body fragment corrupts non-body targets --- src/htmx.js | 8 ++++--- test/tests/attributes/hx-swap.js | 36 ++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/htmx.js b/src/htmx.js index 7ee3b0ea2..38871d130 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -1347,10 +1347,12 @@ var htmx = (() => { } let swapStyle = swapSpec.style; if (swapStyle === 'none') return; - // full-page response: fragment has a wrapper, so upgrade outerHTML to outerSync, strip for everything else + if (swapSpec.reset) task.sourceElement?.closest?.('form')?.reset(); + // Body fragment: strip wrapper unless outer* swap on document.body if (fragment.firstElementChild?.tagName === 'BODY') { - if (swapStyle === 'outerHTML') swapStyle = 'outerSync'; - else if (!swapStyle.startsWith('outer')) swapSpec.strip = true; + const keepBody = target === document.body && swapStyle.startsWith('outer') + if (keepBody && swapStyle === 'outerHTML') swapStyle = 'outerSync' + swapSpec.strip ??= !keepBody } if (swapSpec.strip && fragment.firstElementChild) { fragment = document.createDocumentFragment(); diff --git a/test/tests/attributes/hx-swap.js b/test/tests/attributes/hx-swap.js index 97234d843..93b22b01c 100644 --- a/test/tests/attributes/hx-swap.js +++ b/test/tests/attributes/hx-swap.js @@ -95,4 +95,40 @@ describe('hx-swap modifiers', function() { assert.equal(find('#target').className, 'test') assert.equal(find('#target').textContent, 'New Text') }) + + it('outerHTML with body fragment on non-body target strips body wrapper', async function () { + mockResponse('GET', '/test', '

New content

') + createProcessedHTML('
Old
'); + find('#btn').click() + await forRequest() + // Target should be replaced by

, body wrapper stripped + assert.isUndefined(find('#target'), 'Target should be replaced') + let p = find('p') + assert.exists(p) + assert.equal(p.textContent, 'New content') + assert.equal(p.parentElement.id, 'test-playground') + }) + + it('outerHTML with full HTML document on non-body target strips body wrapper', async function () { + mockResponse('GET', '/test', 'Page

Content
') + createProcessedHTML('
Widget
'); + find('#btn').click() + await forRequest() + assert.isUndefined(find('#target'), 'Target should be replaced') + let content = find('.content') + assert.exists(content) + assert.equal(content.parentElement.id, 'test-playground') + }) + + it('innerHTML with body fragment strips body wrapper', async function () { + mockResponse('GET', '/test', 'AB') + createProcessedHTML('
Old
'); + find('#btn').click() + await forRequest() + let target = find('#target') + assert.exists(target) + assert.equal(target.className, 'keep-me', 'Target keeps its attributes') + assert.equal(target.children.length, 2) + assert.equal(target.children[0].tagName, 'SPAN') + }) }) From 425a218a124e06bf5cf6aced4687823b14c83bb2 Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Fri, 28 Aug 2026 13:59:33 +1200 Subject: [PATCH 2/2] fix after rebase --- src/htmx.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/htmx.js b/src/htmx.js index 38871d130..d9c9b4877 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -1347,7 +1347,6 @@ var htmx = (() => { } let swapStyle = swapSpec.style; if (swapStyle === 'none') return; - if (swapSpec.reset) task.sourceElement?.closest?.('form')?.reset(); // Body fragment: strip wrapper unless outer* swap on document.body if (fragment.firstElementChild?.tagName === 'BODY') { const keepBody = target === document.body && swapStyle.startsWith('outer')