diff --git a/src/ext/hx-sse.js b/src/ext/hx-sse.js index a1c8e148a..2874babc8 100644 --- a/src/ext/hx-sse.js +++ b/src/ext/hx-sse.js @@ -279,6 +279,8 @@ // Swap content using the ctx from core (target/swap already resolved) ctx.text = detail.message.data; + // Always prevent empty swap for SSE - protects against empty data and + // ensures OOB-only messages don't clear target (regardless of allowEmptySwapAfterOOB) if (!ctx.swap.includes('swapEmpty')) ctx.swap += ' swapEmpty:false'; await htmx.swap(ctx); delete detail.message.cancelled; diff --git a/src/htmx.js b/src/htmx.js index 024baa612..738faba34 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -218,7 +218,8 @@ var htmx = (() => { morphScanLimit: 10, noSwap: [204, 304], implicitInheritance: false, - defaultSettleDelay: 1 + defaultSettleDelay: 1, + allowEmptySwapAfterOOB: false } let metaConfig = document.querySelector('meta[name="htmx-config"]'); if (metaConfig) { @@ -1268,8 +1269,12 @@ var htmx = (() => { let partialTasks = this.__processPartials(fragment, ctx); tasks.push(...oobTasks, ...partialTasks); - // Process main swap first - let mainSwap = this.__processMainSwap(ctx, fragment, partialTasks); + // Determine if empty swap should be prevented + // partials always prevent; oob prevents by default unless config.allowEmptySwapAfterOOB is true + let hasPartials = partialTasks.length || (oobTasks.length && !this.config.allowEmptySwapAfterOOB); + + // Process main swap + let mainSwap = this.__processMainSwap(ctx, fragment, hasPartials); if (mainSwap) { tasks.unshift(mainSwap); } @@ -1312,15 +1317,16 @@ var htmx = (() => { } } - __processMainSwap(ctx, fragment, partialTasks) { + __processMainSwap(ctx, fragment, hasPartials) { // Create main task if needed let swapSpec = this.__parseSwapSpec(ctx.swap || this.config.defaultSwap); - // skip main swap if fragment is empty after hx-partial removal but respect empty modifier + // skip main swap if fragment is empty after partial/oob removal + // swapEmpty modifier can override; default: skip if hasPartials if ( swapSpec.style === 'delete' || // delete always runs regardless of content fragment.childElementCount > 0 || // or fragment has elements fragment.textContent.trim() || // or fragment has text - (swapSpec.swapEmpty ?? this.config.defaultSwapEmpty ?? !partialTasks.length) // swapEmpty:true/false overrides, default: allow if no partials + (swapSpec.swapEmpty ?? !hasPartials) ) { if (ctx.select) { let selected = fragment.querySelectorAll(ctx.select); diff --git a/src/skills/htmx-upgrade-from-htmx2.md b/src/skills/htmx-upgrade-from-htmx2.md index 21327c20c..8ca430575 100644 --- a/src/skills/htmx-upgrade-from-htmx2.md +++ b/src/skills/htmx-upgrade-from-htmx2.md @@ -301,12 +301,16 @@ delete buttons relied on form data: ``` -## Step 12: Handle OOB Swap Order Change +## Step 12: Handle OOB Swap Changes In htmx 2, OOB swaps happened before the main content swap. In htmx 4, main content swaps first, then OOB/partial elements swap after. If you have code that depends on OOB elements being present when the main content is swapped, you may need to restructure. +Additionally, responses containing only OOB elements no longer perform an empty main swap by default. +If your code relied on OOB-only responses clearing the main target, set `htmx.config.allowEmptySwapAfterOOB = true` +or add `swapEmpty:true` to `hx-swap` on specific elements. + ## Step 13: Handle Non-200 Response Swapping In htmx 2, 4xx and 5xx responses did not swap by default. In htmx 4, all responses swap except diff --git a/test/tests/unit/swap.js b/test/tests/unit/swap.js index c509e9930..fee06d969 100644 --- a/test/tests/unit/swap.js +++ b/test/tests/unit/swap.js @@ -736,6 +736,58 @@ describe('swap() unit tests', function() { find('#oob').innerText.should.equal('Updated'); }) + it('by default (allowEmptySwapAfterOOB:false), oob-only response prevents main swap', async function () { + let original = htmx.config.allowEmptySwapAfterOOB; + htmx.config.allowEmptySwapAfterOOB = false; + try { + createProcessedHTML("
<template> form: <templat
#### Empty Response Behaviour
-When a response contains only `` elements and no main content, htmx will **not** perform the main swap.
-This is the opposite default to [`hx-swap-oob`](/reference/attributes/hx-swap-oob): with partials, an
-all-partial response signals intent — the server is explicitly routing multiple targeted updates and there is no main
-content to swap.
+When a response contains only `` elements and no main content, htmx will **not** perform the main swap. Partials are designed as true response separators — each partial is a self-contained section, giving the server explicit control over multi-target updates.
```html
@@ -1860,13 +1857,13 @@ content to swap.
```
-If you also want the main target cleared, add `swapEmpty:true` to `hx-swap` on the triggering element:
+If you want the main target cleared, add `swapEmpty:true` to `hx-swap` on the triggering element:
```html
```
-Or set the global default via [`htmx.config.defaultSwapEmpty`](/reference/config/htmx-config-defaultSwapEmpty).
+[`hx-swap-oob`](/reference/attributes/hx-swap-oob) also prevents empty main swaps by default, but this can be changed globally via [`htmx.config.allowEmptySwapAfterOOB`](/reference/config/htmx-config-allowEmptySwapAfterOOB). Partials always prevent empty swaps regardless of that setting.
#### When to Use Partials
diff --git a/www/src/content/reference/01-attributes/07-hx-swap.md b/www/src/content/reference/01-attributes/07-hx-swap.md
index fd4f80153..b51f91a70 100644
--- a/www/src/content/reference/01-attributes/07-hx-swap.md
+++ b/www/src/content/reference/01-attributes/07-hx-swap.md
@@ -351,7 +351,7 @@ Use `swapEmpty` to keep the target or clear it:
Original
```
-Default: [`htmx.config.defaultSwapEmpty`](/reference/config/htmx-config-defaultSwapEmpty)
+Default behavior: skip if partials or OOB swaps were extracted (unless [`htmx.config.allowEmptySwapAfterOOB`](/reference/config/htmx-config-allowEmptySwapAfterOOB) is `true`)
## Caveats
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 25e7e4a44..fe18984c8 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
@@ -142,29 +142,15 @@ Nested OOB attributes are stripped without swapping.
## Empty Response Behaviour
-A response containing only OOB elements still performs an empty main swap.
+By default, a response containing only OOB elements will **not** perform an empty main swap.
-Use this to remove the main target while updating other elements.
+To allow the empty main swap after OOB extraction, set [`htmx.config.allowEmptySwapAfterOOB`](/reference/config/htmx-config-allowEmptySwapAfterOOB) to `true`, or use the [`swapEmpty`](/reference/attributes/hx-swap#swapempty) modifier per-element:
```html
-
-
- New item
-
-
+