From c6b198d6e446d6ceaa795ca1ad2630baec39b889 Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Wed, 29 Jul 2026 02:12:06 +1200 Subject: [PATCH 1/2] support extended css selectors in hx-partial --- src/htmx.js | 4 ++-- test/tests/unit/swap.js | 10 ++++++++++ .../reference/06-tags/01-hx-partial.md | 20 ++++++++++++++++++- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/htmx.js b/src/htmx.js index e53ee80ba..92eec4de4 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -1176,7 +1176,7 @@ var htmx = (() => { if (targetSelector) { this.__processScripts(templateElt.content); let swapSpec = this.__parseSwapSpec(this.__attr(templateElt, 'hx-swap') || this.config.defaultSwap); - for (let target of document.querySelectorAll(targetSelector)) { + for (let target of this.__findAllExt(ctx.sourceElement, targetSelector)) { tasks.push({ type: 'partial', fragment: templateElt.content.cloneNode(true), @@ -1888,7 +1888,7 @@ var htmx = (() => { __findAllExt(eltOrSelector, maybeSelector, thisAttr, global) { let selector = maybeSelector ?? eltOrSelector; - let elt = maybeSelector ? this.__normalizeElement(eltOrSelector) : document; + let elt = maybeSelector ? (this.__normalizeElement(eltOrSelector) || document.body) : document; if (selector.startsWith('global ')) { return this.__findAllExt(elt, selector.slice(7), thisAttr, true); } diff --git a/test/tests/unit/swap.js b/test/tests/unit/swap.js index 8c5551ff7..61df0f898 100644 --- a/test/tests/unit/swap.js +++ b/test/tests/unit/swap.js @@ -683,6 +683,16 @@ describe('swap() unit tests', function() { target.textContent.should.equal('response') }) + it('hx-partial hx-target resolves closest relative to sourceElement', async function () { + createProcessedHTML("") + await htmx.swap({ + target: find('#btn'), + sourceElement: find('#btn'), + text: "Updated" + }) + find('#item-1').innerText.should.equal('Updated') + }) + it('swaps partial to all elements matching a class selector', async function () { createProcessedHTML("
A
B
") await htmx.swap({"target":"#test-playground", "text":"Updated"}) 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..90dd6bff3 100644 --- a/www/src/content/reference/06-tags/01-hx-partial.md +++ b/www/src/content/reference/06-tags/01-hx-partial.md @@ -20,12 +20,30 @@ The `` tag lets you update multiple elements from a single response, ## Attributes -- [`hx-target`](/reference/attributes/hx-target) - CSS selector for where to place content +- [`hx-target`](/reference/attributes/hx-target) - Where to place content. Accepts any CSS selector or htmx extended selector, resolved relative to the element that triggered the request - `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. +## Relative Targeting + +`hx-target` supports the full htmx extended selector vocabulary — `closest`, `next`, `previous`, `find`, `findAll` — resolved relative to the element that triggered the request. This lets the server express structural intent without requiring stable IDs: + +```html + + +
  • Updated item
  • +
    + + + + Required + +``` + +Avoid targeting an ancestor that the main swap is also replacing — the partial would be swapping into a detached node. + ## 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. From 8c114131dca23ce4194bea789c9d886192b3d0ae Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Thu, 30 Jul 2026 02:46:02 +1200 Subject: [PATCH 2/2] fix empty hx-partial targeting cuasing empty swaps --- src/htmx.js | 3 ++- test/tests/unit/swap.js | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/htmx.js b/src/htmx.js index 92eec4de4..d014b2505 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -1176,7 +1176,8 @@ var htmx = (() => { if (targetSelector) { this.__processScripts(templateElt.content); let swapSpec = this.__parseSwapSpec(this.__attr(templateElt, 'hx-swap') || this.config.defaultSwap); - for (let target of this.__findAllExt(ctx.sourceElement, targetSelector)) { + let targets = this.__findAllExt(ctx.sourceElement, targetSelector); +                        for (let target of targets.length ? targets : [null]) { tasks.push({ type: 'partial', fragment: templateElt.content.cloneNode(true), diff --git a/test/tests/unit/swap.js b/test/tests/unit/swap.js index 61df0f898..c509e9930 100644 --- a/test/tests/unit/swap.js +++ b/test/tests/unit/swap.js @@ -468,6 +468,15 @@ describe('swap() unit tests', function() { find('#target_oob').textContent.should.equal("OOB Updated"); }) + it('does not swap main target when partial target is not found', async function () { + createProcessedHTML("
    Original
    ") + await htmx.swap({ + "target":"#target", + "text":"
    Should Not Appear
    " + }) + find('#target').textContent.should.equal("Original"); + }) + it('does not swap main target when only whitespace and partial present', async function () { createProcessedHTML("
    Original
    OOB
    ") await htmx.swap({