Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix for hx-swab-oob within web components #2846

Merged
merged 9 commits into from
Oct 3, 2024
18 changes: 11 additions & 7 deletions src/htmx.js
Original file line number Diff line number Diff line change
Expand Up @@ -1417,9 +1417,11 @@ var htmx = (function() {
* @param {string} oobValue
* @param {Element} oobElement
* @param {HtmxSettleInfo} settleInfo
workjonathan marked this conversation as resolved.
Show resolved Hide resolved
* @param {Node|Document} [rootNode]
* @returns
*/
function oobSwap(oobValue, oobElement, settleInfo) {
function oobSwap(oobValue, oobElement, settleInfo, rootNode) {
rootNode = rootNode || getDocument()
let selector = '#' + getRawAttribute(oobElement, 'id')
/** @type HtmxSwapStyle */
let swapStyle = 'outerHTML'
Expand All @@ -1432,7 +1434,7 @@ var htmx = (function() {
swapStyle = oobValue
}

const targets = getDocument().querySelectorAll(selector)
const targets = querySelectorAllExt(rootNode, selector, false)
workjonathan marked this conversation as resolved.
Show resolved Hide resolved
if (targets) {
forEach(
targets,
Expand Down Expand Up @@ -1808,14 +1810,15 @@ var htmx = (function() {
/**
* @param {DocumentFragment} fragment
workjonathan marked this conversation as resolved.
Show resolved Hide resolved
* @param {HtmxSettleInfo} settleInfo
* @param {Node|Document} [rootNode]
*/
function findAndSwapOobElements(fragment, settleInfo) {
function findAndSwapOobElements(fragment, settleInfo, rootNode) {
var oobElts = findAll(fragment, '[hx-swap-oob], [data-hx-swap-oob]')
forEach(oobElts, function(oobElement) {
if (htmx.config.allowNestedOobSwaps || oobElement.parentElement === null) {
const oobValue = getAttributeValue(oobElement, 'hx-swap-oob')
if (oobValue != null) {
oobSwap(oobValue, oobElement, settleInfo)
oobSwap(oobValue, oobElement, settleInfo, rootNode)
}
} else {
oobElement.removeAttribute('hx-swap-oob')
Expand All @@ -1839,6 +1842,7 @@ var htmx = (function() {
}

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

// preserve focus and selection
const activeElt = document.activeElement
Expand Down Expand Up @@ -1877,14 +1881,14 @@ var htmx = (function() {
const oobValue = oobSelectValue[1] || 'true'
const oobElement = fragment.querySelector('#' + id)
if (oobElement) {
oobSwap(oobValue, oobElement, settleInfo)
oobSwap(oobValue, oobElement, settleInfo, rootNode)
}
}
}
// oob swaps
findAndSwapOobElements(fragment, settleInfo)
findAndSwapOobElements(fragment, settleInfo, rootNode)
forEach(findAll(fragment, 'template'), /** @param {HTMLTemplateElement} template */function(template) {
if (findAndSwapOobElements(template.content, settleInfo)) {
if (findAndSwapOobElements(template.content, settleInfo, rootNode)) {
// Avoid polluting the DOM with empty templates that were only used to encapsulate oob swap
template.remove()
}
Expand Down
87 changes: 87 additions & 0 deletions test/attributes/hx-swap-oob.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,91 @@ describe('hx-swap-oob attribute', function() {
byId('td1').innerHTML.should.equal('hey')
})
}
for (const config of [{ allowNestedOobSwaps: true }, { allowNestedOobSwaps: false }]) {
it('handles oob target in web components with both inside shadow root and config ' + JSON.stringify(config), function() {
this.server.respondWith('GET', '/test', '<div hx-swap-oob="innerHTML:#oob-swap-target">new contents</div>Clicked')
class TestElement extends HTMLElement {
connectedCallback() {
const root = this.attachShadow({ mode: 'open' })
root.innerHTML = `
<button hx-get="/test" hx-target="next div">Click me!</button>
<div id="main-target"></div>
<div id="oob-swap-target">this should get swapped</div>
`
htmx.process(root) // Tell HTMX about this component's shadow DOM
}
}
var elementName = 'test-oobswap-inside-' + config.allowNestedOobSwaps
customElements.define(elementName, TestElement)
var div = make(`<div><div id="oob-swap-target">this should not get swapped</div><${elementName}/></div>`)
var badTarget = div.querySelector('#oob-swap-target')
var webComponent = div.querySelector(elementName)
var btn = webComponent.shadowRoot.querySelector('button')
var goodTarget = webComponent.shadowRoot.querySelector('#oob-swap-target')
var mainTarget = webComponent.shadowRoot.querySelector('#main-target')
btn.click()
this.server.respond()
should.equal(mainTarget.textContent, 'Clicked')
should.equal(goodTarget.textContent, 'new contents')
should.equal(badTarget.textContent, 'this should not get swapped')
})
}
for (const config of [{ allowNestedOobSwaps: true }, { allowNestedOobSwaps: false }]) {
it('handles oob target in web components with main target outside web component config ' + JSON.stringify(config), function() {
this.server.respondWith('GET', '/test', '<div hx-swap-oob="innerHTML:#oob-swap-target">new contents</div>Clicked')
class TestElement extends HTMLElement {
connectedCallback() {
const root = this.attachShadow({ mode: 'open' })
root.innerHTML = `
<button hx-get="/test" hx-target="global #main-target">Click me!</button>
<div id="main-target"></div>
<div id="oob-swap-target">this should get swapped</div>
`
htmx.process(root) // Tell HTMX about this component's shadow DOM
}
}
var elementName = 'test-oobswap-global-main-' + config.allowNestedOobSwaps
customElements.define(elementName, TestElement)
var div = make(`<div><div id="main-target"></div><div id="oob-swap-target">this should not get swapped</div><${elementName}/></div>`)
var badTarget = div.querySelector('#oob-swap-target')
var webComponent = div.querySelector(elementName)
var btn = webComponent.shadowRoot.querySelector('button')
var goodTarget = webComponent.shadowRoot.querySelector('#oob-swap-target')
var mainTarget = div.querySelector('#main-target')
btn.click()
this.server.respond()
should.equal(mainTarget.textContent, 'Clicked')
should.equal(goodTarget.textContent, 'new contents')
should.equal(badTarget.textContent, 'this should not get swapped')
})
}
for (const config of [{ allowNestedOobSwaps: true }, { allowNestedOobSwaps: false }]) {
it('handles global oob target in web components with main target inside web component config ' + JSON.stringify(config), function() {
this.server.respondWith('GET', '/test', '<div hx-swap-oob="innerHTML:global #oob-swap-target">new contents</div>Clicked')
class TestElement extends HTMLElement {
connectedCallback() {
const root = this.attachShadow({ mode: 'open' })
root.innerHTML = `
<button hx-get="/test" hx-target="next div">Click me!</button>
<div id="main-target"></div>
<div id="oob-swap-target">this should not get swapped</div>
`
htmx.process(root) // Tell HTMX about this component's shadow DOM
}
}
var elementName = 'test-oobswap-global-oob-' + config.allowNestedOobSwaps
customElements.define(elementName, TestElement)
var div = make(`<div><div id="main-target"></div><div id="oob-swap-target">this should get swapped</div><${elementName}/></div>`)
var webComponent = div.querySelector(elementName)
var badTarget = webComponent.shadowRoot.querySelector('#oob-swap-target')
var btn = webComponent.shadowRoot.querySelector('button')
var goodTarget = div.querySelector('#oob-swap-target')
var mainTarget = webComponent.shadowRoot.querySelector('#main-target')
btn.click()
this.server.respond()
should.equal(mainTarget.textContent, 'Clicked')
should.equal(goodTarget.textContent, 'new contents')
should.equal(badTarget.textContent, 'this should not get swapped')
})
}
})