Skip to content

Commit 99285cd

Browse files
authored
fix for hx-swab-oob within web components (#2846)
* Failing test for oob-swap within web components * hx-swap-oob respects shadow roots * Lint and type fixes * fix jsdoc types for rootNode parameter * Fix for linter issue I was confused about before * oob swaps handle global correctly * swap uses contextElement if available, document if not Previous a commit made swapOptions.contextElement a required field. This could have harmful ramifications for extensions and users, so instead, the old behavior of assuming document as a root will be used if the contextElement is not provided. * rootNode parameter is optional in oobSwap If not provided, it will fall back to using document as rootNode. jsdocs have been updated for oobSwap and findAndSwapElements accordingly.
1 parent 8c65826 commit 99285cd

File tree

2 files changed

+98
-7
lines changed

2 files changed

+98
-7
lines changed

src/htmx.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1405,9 +1405,11 @@ var htmx = (function() {
14051405
* @param {string} oobValue
14061406
* @param {Element} oobElement
14071407
* @param {HtmxSettleInfo} settleInfo
1408+
* @param {Node|Document} [rootNode]
14081409
* @returns
14091410
*/
1410-
function oobSwap(oobValue, oobElement, settleInfo) {
1411+
function oobSwap(oobValue, oobElement, settleInfo, rootNode) {
1412+
rootNode = rootNode || getDocument()
14111413
let selector = '#' + getRawAttribute(oobElement, 'id')
14121414
/** @type HtmxSwapStyle */
14131415
let swapStyle = 'outerHTML'
@@ -1422,7 +1424,7 @@ var htmx = (function() {
14221424
oobElement.removeAttribute('hx-swap-oob')
14231425
oobElement.removeAttribute('data-hx-swap-oob')
14241426

1425-
const targets = getDocument().querySelectorAll(selector)
1427+
const targets = querySelectorAllExt(rootNode, selector, false)
14261428
if (targets) {
14271429
forEach(
14281430
targets,
@@ -1807,14 +1809,15 @@ var htmx = (function() {
18071809
/**
18081810
* @param {DocumentFragment} fragment
18091811
* @param {HtmxSettleInfo} settleInfo
1812+
* @param {Node|Document} [rootNode]
18101813
*/
1811-
function findAndSwapOobElements(fragment, settleInfo) {
1814+
function findAndSwapOobElements(fragment, settleInfo, rootNode) {
18121815
var oobElts = findAll(fragment, '[hx-swap-oob], [data-hx-swap-oob]')
18131816
forEach(oobElts, function(oobElement) {
18141817
if (htmx.config.allowNestedOobSwaps || oobElement.parentElement === null) {
18151818
const oobValue = getAttributeValue(oobElement, 'hx-swap-oob')
18161819
if (oobValue != null) {
1817-
oobSwap(oobValue, oobElement, settleInfo)
1820+
oobSwap(oobValue, oobElement, settleInfo, rootNode)
18181821
}
18191822
} else {
18201823
oobElement.removeAttribute('hx-swap-oob')
@@ -1838,6 +1841,7 @@ var htmx = (function() {
18381841
}
18391842

18401843
target = resolveTarget(target)
1844+
const rootNode = swapOptions.contextElement ? getRootNode(swapOptions.contextElement, false) : getDocument()
18411845

18421846
// preserve focus and selection
18431847
const activeElt = document.activeElement
@@ -1876,14 +1880,14 @@ var htmx = (function() {
18761880
const oobValue = oobSelectValue[1] || 'true'
18771881
const oobElement = fragment.querySelector('#' + id)
18781882
if (oobElement) {
1879-
oobSwap(oobValue, oobElement, settleInfo)
1883+
oobSwap(oobValue, oobElement, settleInfo, rootNode)
18801884
}
18811885
}
18821886
}
18831887
// oob swaps
1884-
findAndSwapOobElements(fragment, settleInfo)
1888+
findAndSwapOobElements(fragment, settleInfo, rootNode)
18851889
forEach(findAll(fragment, 'template'), /** @param {HTMLTemplateElement} template */function(template) {
1886-
if (findAndSwapOobElements(template.content, settleInfo)) {
1890+
if (findAndSwapOobElements(template.content, settleInfo, rootNode)) {
18871891
// Avoid polluting the DOM with empty templates that were only used to encapsulate oob swap
18881892
template.remove()
18891893
}

test/attributes/hx-swap-oob.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,4 +260,91 @@ describe('hx-swap-oob attribute', function() {
260260
byId('td1').innerHTML.should.equal('hey')
261261
})
262262
}
263+
for (const config of [{ allowNestedOobSwaps: true }, { allowNestedOobSwaps: false }]) {
264+
it('handles oob target in web components with both inside shadow root and config ' + JSON.stringify(config), function() {
265+
this.server.respondWith('GET', '/test', '<div hx-swap-oob="innerHTML:#oob-swap-target">new contents</div>Clicked')
266+
class TestElement extends HTMLElement {
267+
connectedCallback() {
268+
const root = this.attachShadow({ mode: 'open' })
269+
root.innerHTML = `
270+
<button hx-get="/test" hx-target="next div">Click me!</button>
271+
<div id="main-target"></div>
272+
<div id="oob-swap-target">this should get swapped</div>
273+
`
274+
htmx.process(root) // Tell HTMX about this component's shadow DOM
275+
}
276+
}
277+
var elementName = 'test-oobswap-inside-' + config.allowNestedOobSwaps
278+
customElements.define(elementName, TestElement)
279+
var div = make(`<div><div id="oob-swap-target">this should not get swapped</div><${elementName}/></div>`)
280+
var badTarget = div.querySelector('#oob-swap-target')
281+
var webComponent = div.querySelector(elementName)
282+
var btn = webComponent.shadowRoot.querySelector('button')
283+
var goodTarget = webComponent.shadowRoot.querySelector('#oob-swap-target')
284+
var mainTarget = webComponent.shadowRoot.querySelector('#main-target')
285+
btn.click()
286+
this.server.respond()
287+
should.equal(mainTarget.textContent, 'Clicked')
288+
should.equal(goodTarget.textContent, 'new contents')
289+
should.equal(badTarget.textContent, 'this should not get swapped')
290+
})
291+
}
292+
for (const config of [{ allowNestedOobSwaps: true }, { allowNestedOobSwaps: false }]) {
293+
it('handles oob target in web components with main target outside web component config ' + JSON.stringify(config), function() {
294+
this.server.respondWith('GET', '/test', '<div hx-swap-oob="innerHTML:#oob-swap-target">new contents</div>Clicked')
295+
class TestElement extends HTMLElement {
296+
connectedCallback() {
297+
const root = this.attachShadow({ mode: 'open' })
298+
root.innerHTML = `
299+
<button hx-get="/test" hx-target="global #main-target">Click me!</button>
300+
<div id="main-target"></div>
301+
<div id="oob-swap-target">this should get swapped</div>
302+
`
303+
htmx.process(root) // Tell HTMX about this component's shadow DOM
304+
}
305+
}
306+
var elementName = 'test-oobswap-global-main-' + config.allowNestedOobSwaps
307+
customElements.define(elementName, TestElement)
308+
var div = make(`<div><div id="main-target"></div><div id="oob-swap-target">this should not get swapped</div><${elementName}/></div>`)
309+
var badTarget = div.querySelector('#oob-swap-target')
310+
var webComponent = div.querySelector(elementName)
311+
var btn = webComponent.shadowRoot.querySelector('button')
312+
var goodTarget = webComponent.shadowRoot.querySelector('#oob-swap-target')
313+
var mainTarget = div.querySelector('#main-target')
314+
btn.click()
315+
this.server.respond()
316+
should.equal(mainTarget.textContent, 'Clicked')
317+
should.equal(goodTarget.textContent, 'new contents')
318+
should.equal(badTarget.textContent, 'this should not get swapped')
319+
})
320+
}
321+
for (const config of [{ allowNestedOobSwaps: true }, { allowNestedOobSwaps: false }]) {
322+
it('handles global oob target in web components with main target inside web component config ' + JSON.stringify(config), function() {
323+
this.server.respondWith('GET', '/test', '<div hx-swap-oob="innerHTML:global #oob-swap-target">new contents</div>Clicked')
324+
class TestElement extends HTMLElement {
325+
connectedCallback() {
326+
const root = this.attachShadow({ mode: 'open' })
327+
root.innerHTML = `
328+
<button hx-get="/test" hx-target="next div">Click me!</button>
329+
<div id="main-target"></div>
330+
<div id="oob-swap-target">this should not get swapped</div>
331+
`
332+
htmx.process(root) // Tell HTMX about this component's shadow DOM
333+
}
334+
}
335+
var elementName = 'test-oobswap-global-oob-' + config.allowNestedOobSwaps
336+
customElements.define(elementName, TestElement)
337+
var div = make(`<div><div id="main-target"></div><div id="oob-swap-target">this should get swapped</div><${elementName}/></div>`)
338+
var webComponent = div.querySelector(elementName)
339+
var badTarget = webComponent.shadowRoot.querySelector('#oob-swap-target')
340+
var btn = webComponent.shadowRoot.querySelector('button')
341+
var goodTarget = div.querySelector('#oob-swap-target')
342+
var mainTarget = webComponent.shadowRoot.querySelector('#main-target')
343+
btn.click()
344+
this.server.respond()
345+
should.equal(mainTarget.textContent, 'Clicked')
346+
should.equal(goodTarget.textContent, 'new contents')
347+
should.equal(badTarget.textContent, 'this should not get swapped')
348+
})
349+
}
263350
})

0 commit comments

Comments
 (0)