From 04812fffaca9a4ab630f93c1d650d7918f95510b Mon Sep 17 00:00:00 2001 From: Christian Tanul Date: Tue, 16 Jun 2026 13:05:44 +0300 Subject: [PATCH 1/2] Fix hx-swap-oob-only response wiping main target --- src/htmx.js | 52 ++++++++++++++++++++++++----------------- test/tests/unit/swap.js | 14 +++++++++++ 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/htmx.js b/src/htmx.js index 5fedc7d59..5a2be8cd0 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -1186,7 +1186,7 @@ var htmx = (() => { tasks.push(...oobTasks, ...partialTasks); // Process main swap first - let mainSwap = this.__processMainSwap(ctx, fragment, partialTasks); + let mainSwap = this.__processMainSwap(ctx, fragment, tasks); if (mainSwap) { tasks.unshift(mainSwap); } @@ -1225,29 +1225,37 @@ var htmx = (() => { } } - __processMainSwap(ctx, fragment, partialTasks) { - // Create main task if needed + /** + * @param {object} ctx - request context + * @param {DocumentFragment} fragment - response content (after extraction) + * @param {Array} extractedTasks - swap tasks from hx-swap-oob and + */ + __processMainSwap(ctx, fragment, extractedTasks) { let swapSpec = this.__parseSwapSpec(ctx.swap || this.config.defaultSwap); - // skip creating main swap if extracting partials resulted in empty response except for delete style - if (swapSpec.style === 'delete' || fragment.childElementCount > 0 || /\S/.test(fragment.textContent) || !partialTasks.length) { - if (ctx.select) { - let selected = fragment.querySelectorAll(ctx.select); - fragment = document.createDocumentFragment(); - fragment.append(...selected); - } - if (this.__isBoosted(ctx.sourceElement)) { - swapSpec.show ||= 'top'; - } - let mainSwap = { - type: 'main', - fragment, - target: this.__resolveTarget(ctx.sourceElement || document.body, swapSpec.target || ctx.target), - swapSpec, - sourceElement: ctx.sourceElement, - transition: ctx.transition && swapSpec.transition !== false - }; - return mainSwap; + + let hasElements = fragment.childElementCount > 0; + let hasText = /\S/.test(fragment.textContent); + let allContentWasExtracted = !hasElements && !hasText && extractedTasks.length > 0; + + // delete always proceeds (intentional target removal) + if (allContentWasExtracted && swapSpec.style !== 'delete') return; + + if (ctx.select) { // hx-select / HX-Reselect + let selected = fragment.querySelectorAll(ctx.select); + fragment = document.createDocumentFragment(); + fragment.append(...selected); } + if (this.__isBoosted(ctx.sourceElement)) { + swapSpec.show ||= 'top'; + } + return { + type: 'main', + fragment, + target: this.__resolveTarget(ctx.sourceElement || document.body, swapSpec.target || ctx.target), + swapSpec, + sourceElement: ctx.sourceElement, + transition: ctx.transition && swapSpec.transition !== false + }; } async __insertContent(task, cssTransition = true) { diff --git a/test/tests/unit/swap.js b/test/tests/unit/swap.js index c50427d96..e3fe984c6 100644 --- a/test/tests/unit/swap.js +++ b/test/tests/unit/swap.js @@ -189,6 +189,20 @@ describe('swap() unit tests', function() { find('#d2').innerText.should.equal("OOB"); }) + it('does not swap main target when response contains only hx-swap-oob elements', async function () { + createProcessedHTML("
Original
Old
") + await htmx.swap({"target":"#d1", "text":"
OOB Updated
"}) + find('#d1').innerText.should.equal("Original"); + find('#d2').innerText.should.equal("OOB Updated"); + }) + + it('does not swap main target when response contains only hx-swap-oob with whitespace', async function () { + createProcessedHTML("
Original
Old
") + await htmx.swap({"target":"#d1", "text":"\n
OOB Updated
\n"}) + find('#d1').innerText.should.equal("Original"); + find('#d2').innerText.should.equal("OOB Updated"); + }) + it('swaps oob with innerHTML', async function () { createProcessedHTML("
Old
") await htmx.swap({"target":"#d1", "text":"
Main
OOB
"}) From 463235df051cac6aa2fc79d288c385057c831896 Mon Sep 17 00:00:00 2001 From: Christian Tanul Date: Tue, 16 Jun 2026 13:23:53 +0300 Subject: [PATCH 2/2] Add docs for hx-swap-oob/hx-partial responses without main content --- www/src/content/docs.mdx | 15 +++++++ .../reference/01-attributes/13-hx-swap-oob.md | 7 ++- .../reference/06-tags/01-hx-partial.md | 45 +++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 www/src/content/reference/06-tags/01-hx-partial.md diff --git a/www/src/content/docs.mdx b/www/src/content/docs.mdx index 2ec5a8d14..9ddff74af 100644 --- a/www/src/content/docs.mdx +++ b/www/src/content/docs.mdx @@ -1870,6 +1870,21 @@ Both methods work together. Use them in the same response if needed. **Use partial tags** for everything else. +#### Responses Without Main Content + +When a response contains only [`hx-swap-oob`](/reference/attributes/hx-swap-oob) or [``](/reference/tags/hx-partial) elements (no main content), +the main target is left untouched. + +```html + +
+ 3 new messages +
+``` + +This is useful for SSE and WebSocket connections where every message targets specific +elements and there is no main swap. + ### Additional Features #### Select Specific Elements for OOB diff --git a/www/src/content/reference/01-attributes/13-hx-swap-oob.md b/www/src/content/reference/01-attributes/13-hx-swap-oob.md index f1bab3ef8..5ed6313e1 100644 --- a/www/src/content/reference/01-attributes/13-hx-swap-oob.md +++ b/www/src/content/reference/01-attributes/13-hx-swap-oob.md @@ -143,6 +143,11 @@ This behavior can be changed by setting the config `htmx.config.allowNestedOobSw is `false`, OOB swaps are only processed when the element is *adjacent to* the main response element, OOB swaps elsewhere will be ignored and oob-swap-related attributes stripped. +## Responses Without Main Content + +When a response contains only `hx-swap-oob` elements, the main target is not modified. +See [Multi-Target Updates](/docs#choosing-between-them) for details. + ## See Also -- [``](/docs#partials-hx-partial), an alternative for multi-target updates with explicit control over targeting and swap strategy +- [``](/reference/tags/hx-partial), an alternative for multi-target updates with explicit control over targeting and swap strategy diff --git a/www/src/content/reference/06-tags/01-hx-partial.md b/www/src/content/reference/06-tags/01-hx-partial.md new file mode 100644 index 000000000..43b8f7cbf --- /dev/null +++ b/www/src/content/reference/06-tags/01-hx-partial.md @@ -0,0 +1,45 @@ +y--- +title: "" +description: "Target multiple elements from a single response" +--- + +The `` tag lets you update multiple elements from a single response, with explicit control over targeting and swap strategy. + +## Syntax + +```html + +
New message
+
+ + + 5 + +``` + +## Attributes + +- [`hx-target`](/reference/attributes/hx-target) - CSS selector for where to place content +- `id` - Shorthand alternative to `hx-target`. Targets the element with that ID (e.g. `` targets `#messages`) +- [`hx-swap`](/reference/attributes/hx-swap) - Swap style (defaults to `innerHTML`) + +Either `hx-target` or `id` is required. If both are present, `hx-target` takes precedence. + +## Responses Without Main Content + +When a response contains only `` tags (no main content), the main target is left untouched. See [Multi-Target Updates](/docs#choosing-between-them) for details. + +## Alternative Syntax + +Template languages that strip unknown tags can use the equivalent `