mirror of
https://github.com/bigskysoftware/htmx.git
synced 2025-09-26 20:40:41 +00:00
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.
This commit is contained in:
parent
8c6582679b
commit
99285cd5c3
18
src/htmx.js
18
src/htmx.js
@ -1405,9 +1405,11 @@ var htmx = (function() {
|
||||
* @param {string} oobValue
|
||||
* @param {Element} oobElement
|
||||
* @param {HtmxSettleInfo} settleInfo
|
||||
* @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'
|
||||
@ -1422,7 +1424,7 @@ var htmx = (function() {
|
||||
oobElement.removeAttribute('hx-swap-oob')
|
||||
oobElement.removeAttribute('data-hx-swap-oob')
|
||||
|
||||
const targets = getDocument().querySelectorAll(selector)
|
||||
const targets = querySelectorAllExt(rootNode, selector, false)
|
||||
if (targets) {
|
||||
forEach(
|
||||
targets,
|
||||
@ -1807,14 +1809,15 @@ var htmx = (function() {
|
||||
/**
|
||||
* @param {DocumentFragment} fragment
|
||||
* @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')
|
||||
@ -1838,6 +1841,7 @@ var htmx = (function() {
|
||||
}
|
||||
|
||||
target = resolveTarget(target)
|
||||
const rootNode = swapOptions.contextElement ? getRootNode(swapOptions.contextElement, false) : getDocument()
|
||||
|
||||
// preserve focus and selection
|
||||
const activeElt = document.activeElement
|
||||
@ -1876,14 +1880,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()
|
||||
}
|
||||
|
@ -260,4 +260,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')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user