diff --git a/src/htmx.js b/src/htmx.js index e53ee80ba..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 document.querySelectorAll(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), @@ -1888,7 +1889,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..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({ @@ -683,6 +692,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.