diff --git a/src/htmx.d.ts b/src/htmx.d.ts index cac9916a4..0e26fadf5 100644 --- a/src/htmx.d.ts +++ b/src/htmx.d.ts @@ -131,13 +131,10 @@ export interface HtmxConfig { metaCharacter?: string; /** * Whether an empty response body performs the main swap. - * - `true` — swap (clears target) - * - `false` — skip swap - * - `undefined` — swap unless response contained only `` elements * Overridable per element via the `swapEmpty` modifier on `hx-swap`. - * @default undefined + * @default true */ - defaultSwapEmpty?: boolean; + defaultSwapEmpty: boolean; /** Requires hx-live. */ live?: HtmxLiveConfig; } diff --git a/src/htmx.js b/src/htmx.js index fe9ff3106..52501a053 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -206,6 +206,7 @@ var htmx = (() => { history: true, mode: 'same-origin', defaultSwap: "innerHTML", + defaultSwapEmpty: true, defaultFocusScroll: false, indicatorClass: "htmx-indicator", requestClass: "htmx-request", @@ -1268,7 +1269,7 @@ var htmx = (() => { tasks.push(...oobTasks, ...partialTasks); // Process main swap first - let mainSwap = this.__processMainSwap(ctx, fragment, partialTasks); + let mainSwap = this.__processMainSwap(ctx, fragment); if (mainSwap) { tasks.unshift(mainSwap); } @@ -1311,15 +1312,14 @@ var htmx = (() => { } } - __processMainSwap(ctx, fragment, partialTasks) { + __processMainSwap(ctx, fragment) { // 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 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 ?? this.config.defaultSwapEmpty) ) { if (ctx.select) { let selected = fragment.querySelectorAll(ctx.select); diff --git a/test/tests/ext/hx-upsert.js b/test/tests/ext/hx-upsert.js index c1ea8c2c2..e9a831da1 100644 --- a/test/tests/ext/hx-upsert.js +++ b/test/tests/ext/hx-upsert.js @@ -162,7 +162,7 @@ describe('hx-upsert extension', function() { it('works with hx-partial', async function () { mockResponse('GET', '/test', '
Two
B
') - let container = createProcessedHTML('
One
A
'); + let container = createProcessedHTML('
One
A
'); container.click() await htmx.timeout(20) let list1 = container.querySelector('#list1') @@ -189,7 +189,7 @@ describe('hx-upsert extension', function() { it('hx-upsert tag with basic upsert', async function () { mockResponse('GET', '/test', '
Two
') - let container = createProcessedHTML('
One
'); + let container = createProcessedHTML('
One
'); container.click() await htmx.timeout(20) let list = container.querySelector('#list') @@ -200,7 +200,7 @@ describe('hx-upsert extension', function() { it('hx-upsert tag with sort attribute', async function () { mockResponse('GET', '/test', '
Two
') - let container = createProcessedHTML('
One
Three
'); + let container = createProcessedHTML('
One
Three
'); container.click() await htmx.timeout(20) let list = container.querySelector('#list') @@ -211,7 +211,7 @@ describe('hx-upsert extension', function() { it('hx-upsert tag with sort="desc"', async function () { mockResponse('GET', '/test', '
Two
') - let container = createProcessedHTML('
Three
One
'); + let container = createProcessedHTML('
Three
One
'); container.click() await htmx.timeout(20) let list = container.querySelector('#list') @@ -222,7 +222,7 @@ describe('hx-upsert extension', function() { it('hx-upsert tag with key attribute', async function () { mockResponse('GET', '/test', '
Medium
') - let container = createProcessedHTML('
High
Low
'); + let container = createProcessedHTML('
High
Low
'); container.click() await htmx.timeout(20) let list = container.querySelector('#list') @@ -233,7 +233,7 @@ describe('hx-upsert extension', function() { it('hx-upsert tag with prepend attribute', async function () { mockResponse('GET', '/test', '
No Key
') - let container = createProcessedHTML('
One
'); + let container = createProcessedHTML('
One
'); container.click() await htmx.timeout(20) let list = container.querySelector('#list') diff --git a/test/tests/ext/hx-ws.js b/test/tests/ext/hx-ws.js index 0821f7670..4edd08337 100644 --- a/test/tests/ext/hx-ws.js +++ b/test/tests/ext/hx-ws.js @@ -553,7 +553,7 @@ describe('hx-ws WebSocket extension', function() { it('respects hx-swap attribute on partial', async function() { let container = createProcessedHTML(` -
+

Item 1

`); @@ -1603,7 +1603,7 @@ describe('hx-ws WebSocket extension', function() { it('handles live notifications pattern', async function() { let container = createProcessedHTML(` -
+
`); diff --git a/test/tests/unit/swap.js b/test/tests/unit/swap.js index 8c5551ff7..66bc30267 100644 --- a/test/tests/unit/swap.js +++ b/test/tests/unit/swap.js @@ -209,7 +209,7 @@ describe('swap() unit tests', function() { it('swaps partial with custom swap style', async function () { createProcessedHTML("
Existing
") - await htmx.swap({"target":"#test-playground", "text":"Partial"}) + await htmx.swap({"target":"#test-playground", "swap":"innerHTML swapEmpty:false", "text":"Partial"}) find('#d1').innerText.should.equal("ExistingPartial"); }) @@ -458,23 +458,23 @@ describe('swap() unit tests', function() { find('#target_oob').textContent.should.equal("OOB swap!"); }) - it('swaps only partial target when response contains only partial', async function () { + it('swaps empty main target when response contains only partial', async function () { createProcessedHTML("
Original
OOB Original
") await htmx.swap({ "target":"#target", "text":"
OOB Updated
" }) - find('#target').textContent.should.equal("Original"); + find('#target').textContent.should.equal(""); find('#target_oob').textContent.should.equal("OOB Updated"); }) - it('does not swap main target when only whitespace and partial present', async function () { + it('swaps empty main target when only whitespace and partial present', async function () { createProcessedHTML("
Original
OOB
") await htmx.swap({ "target":"#target", "text":"\n
OOB swap!
\n" }) - find('#target').textContent.should.equal("Original"); + find('#target').textContent.trim().should.equal(""); find('#target_oob').textContent.should.equal("OOB swap!"); }) @@ -685,7 +685,7 @@ describe('swap() unit tests', function() { it('swaps partial to all elements matching a class selector', async function () { createProcessedHTML("
A
B
") - await htmx.swap({"target":"#test-playground", "text":"Updated"}) + await htmx.swap({"target":"#test-playground", "swap":"innerHTML swapEmpty:false", "text":"Updated"}) playground().querySelectorAll('.target').forEach(el => el.innerText.should.equal('Updated')) }) diff --git a/www/src/content/docs.mdx b/www/src/content/docs.mdx index 6cd375d21..3ca37bf23 100644 --- a/www/src/content/docs.mdx +++ b/www/src/content/docs.mdx @@ -1845,13 +1845,10 @@ You can use the equivalent <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, htmx performs the main swap with an empty fragment. This matches [`hx-swap-oob`](/reference/attributes/hx-swap-oob). ```html - + 5 @@ -1860,10 +1857,10 @@ content to swap. ``` -If you also want the main target cleared, add `swapEmpty:true` to `hx-swap` on the triggering element: +To leave the main target unchanged, add `swapEmpty:false` to `hx-swap` on the triggering element: ```html - + ``` Or set the global default via [`htmx.config.defaultSwapEmpty`](/reference/config/htmx-config-defaultSwapEmpty). 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..3eb71dac7 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 @@ -162,9 +162,7 @@ If you want to prevent the empty main swap, use the [`swapEmpty`](/reference/att Or set the global default via [`htmx.config.defaultSwapEmpty`](/reference/config/htmx-config-defaultSwapEmpty). -[``](/reference/tags/hx-partial) uses the opposite default. Partial-only responses skip the empty main swap. - -A partial-only response explicitly routes targeted updates, so htmx assumes no main swap is needed. Set `swapEmpty:true` to run it. +[``](/reference/tags/hx-partial) follows the same `swapEmpty` behavior. ## See Also diff --git a/www/src/content/reference/04-config/01-htmx-config.md b/www/src/content/reference/04-config/01-htmx-config.md index 9d10d1015..6f681896a 100644 --- a/www/src/content/reference/04-config/01-htmx-config.md +++ b/www/src/content/reference/04-config/01-htmx-config.md @@ -41,7 +41,7 @@ htmx.config.defaultTimeout = 5000; | [`history`](/reference/config/htmx-config-history) | `true` | Enable history support | | [`mode`](/reference/config/htmx-config-mode) | `"same-origin"` | Request mode for `fetch()` | | [`defaultSwap`](/reference/config/htmx-config-defaultSwap) | `"innerHTML"` | Default swap style | -| [`defaultSwapEmpty`](/reference/config/htmx-config-defaultSwapEmpty) | `undefined` | Swap empty main content unless an `` was extracted | +| [`defaultSwapEmpty`](/reference/config/htmx-config-defaultSwapEmpty) | `true` | Swap empty main content | | [`defaultFocusScroll`](/reference/config/htmx-config-defaultFocusScroll) | `false` | Scroll to a focused element after swapping | | [`defaultSettleDelay`](/reference/config/htmx-config-defaultSettleDelay) | `1` | Delay before settling in milliseconds | | [`indicatorClass`](/reference/config/htmx-config-indicatorClass) | `"htmx-indicator"` | CSS class for indicators | diff --git a/www/src/content/reference/04-config/26-htmx-config-defaultSwapEmpty.md b/www/src/content/reference/04-config/26-htmx-config-defaultSwapEmpty.md index 88b2cafaf..c3475556d 100644 --- a/www/src/content/reference/04-config/26-htmx-config-defaultSwapEmpty.md +++ b/www/src/content/reference/04-config/26-htmx-config-defaultSwapEmpty.md @@ -7,7 +7,7 @@ The `htmx.config.defaultSwapEmpty` option controls the main swap when the respon Override it per element with [`swapEmpty`](/reference/attributes/hx-swap#swapempty). -**Default:** unset. When unset, htmx performs the main swap on an empty response except when the response contained only `` elements. +**Default:** `true` ## Values diff --git a/www/src/content/reference/06-tags/01-hx-partial.md b/www/src/content/reference/06-tags/01-hx-partial.md index cb086e53a..22dcabfaf 100644 --- a/www/src/content/reference/06-tags/01-hx-partial.md +++ b/www/src/content/reference/06-tags/01-hx-partial.md @@ -28,7 +28,7 @@ Either `hx-target` or `id` is required. If both are present, `hx-target` takes p ## 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. +When a response contains only `` tags, htmx performs the normal swap with an empty fragment. Set [`swapEmpty:false`](/reference/attributes/hx-swap#swapempty) to leave the normal target unchanged. ## Alternative Syntax