support hx-on in shadowroot

This commit is contained in:
Carson Gross 2024-04-17 05:25:28 -06:00
parent 81afe922d7
commit 01cb5e0d8d
3 changed files with 34 additions and 201 deletions

View File

@ -2602,19 +2602,29 @@ var htmx = (function() {
* @param {Node} elt
* @returns {Element[]}
*/
const HX_ON_QUERY = new XPathEvaluator()
.createExpression('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or' +
' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]');
function processHXOnRoot(elt, elements) {
if (shouldProcessHxOn(elt)) {
elements.push(asElement(elt))
}
const iter = HX_ON_QUERY.evaluate(elt)
let node = null;
while (node = iter.iterateNext()) elements.push(asElement(node))
}
function findHxOnWildcardElements(elt) {
let node = null
/** @type {Element[]} */
const elements = []
if (!(elt instanceof ShadowRoot)) {
if (shouldProcessHxOn(elt)) {
elements.push(asElement(elt))
if (elt instanceof DocumentFragment) {
for (const child of elt.childNodes) {
processHXOnRoot(child, elements);
}
const iter = document.evaluate('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or' +
' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]', elt)
while (node = iter.iterateNext()) elements.push(asElement(node))
} else {
processHXOnRoot(elt, elements);
}
return elements
}

View File

@ -1,193 +0,0 @@
describe('hx-on attribute', function() {
beforeEach(function() {
this.server = makeServer()
clearWorkArea()
})
afterEach(function() {
this.server.restore()
clearWorkArea()
})
it('can handle basic events w/ no other attributes', function() {
var btn = make("<button hx-on='click: window.foo = true'>Foo</button>")
btn.click()
window.foo.should.equal(true)
delete window.foo
})
it('can modify a parameter via htmx:configRequest', function() {
this.server.respondWith('POST', '/test', function(xhr) {
var params = parseParams(xhr.requestBody)
xhr.respond(200, {}, params.foo)
})
var btn = make("<button hx-on='htmx:configRequest: event.detail.parameters.foo = \"bar\"' hx-post='/test'>Foo</button>")
btn.click()
this.server.respond()
btn.innerText.should.equal('bar')
})
it('can cancel an event via preventDefault for htmx:configRequest', function() {
this.server.respondWith('POST', '/test', function(xhr) {
xhr.respond(200, {}, '<button>Bar</button>')
})
var btn = make("<button hx-on='htmx:configRequest: event.preventDefault()' hx-post='/test' hx-swap='outerHTML'>Foo</button>")
btn.click()
this.server.respond()
btn.innerText.should.equal('Foo')
})
it('can respond to kebab-case events', function() {
this.server.respondWith('POST', '/test', function(xhr) {
var params = parseParams(xhr.requestBody)
xhr.respond(200, {}, params.foo)
})
var btn = make("<button hx-on='htmx:config-request: event.detail.parameters.foo = \"bar\"' hx-post='/test'>Foo</button>")
btn.click()
this.server.respond()
btn.innerText.should.equal('bar')
})
it('has the this symbol set to the element', function() {
this.server.respondWith('POST', '/test', function(xhr) {
xhr.respond(200, {}, 'foo')
})
var btn = make("<button hx-on='htmx:config-request: window.elt = this' hx-post='/test'>Foo</button>")
btn.click()
this.server.respond()
btn.innerText.should.equal('foo')
btn.should.equal(window.elt)
delete window.elt
})
it('can handle multi-line JSON', function() {
this.server.respondWith('POST', '/test', function(xhr) {
xhr.respond(200, {}, 'foo')
})
var btn = make("<button hx-on='htmx:config-request: window.elt = {foo: true,\n" +
" bar: false}' hx-post='/test'>Foo</button>")
btn.click()
this.server.respond()
btn.innerText.should.equal('foo')
var obj = { foo: true, bar: false }
obj.should.deep.equal(window.elt)
delete window.elt
})
it('can handle multiple event handlers in the presence of multi-line JSON', function() {
this.server.respondWith('POST', '/test', function(xhr) {
xhr.respond(200, {}, 'foo')
})
var btn = make("<button hx-on='htmx:config-request: window.elt = {foo: true,\n" +
' bar: false}\n' +
" htmx:afterRequest: window.foo = true'" +
" hx-post='/test'>Foo</button>")
btn.click()
this.server.respond()
btn.innerText.should.equal('foo')
var obj = { foo: true, bar: false }
obj.should.deep.equal(window.elt)
delete window.elt
window.foo.should.equal(true)
delete window.foo
})
it('de-initializes hx-on content properly', function() {
window.tempCount = 0
this.server.respondWith('POST', '/test', function(xhr) {
xhr.respond(200, {}, "<button id='foo' hx-on=\"click: window.tempCount++;\">increment</button>")
})
var div = make("<div hx-post='/test'>Foo</div>")
// get response
div.click()
this.server.respond()
// click button
byId('foo').click()
window.tempCount.should.equal(1)
// get second response
div.click()
this.server.respond()
// click button again
byId('foo').click()
window.tempCount.should.equal(2)
delete window.tempCount
})
it('is not evaluated when allowEval is false', function() {
var calledEvent = false
var handler = htmx.on('htmx:evalDisallowedError', function() {
calledEvent = true
})
htmx.config.allowEval = false
try {
var btn = make("<button hx-on='click: window.foo = true'>Foo</button>")
btn.click()
should.not.exist(window.foo)
} finally {
htmx.config.allowEval = true
htmx.off('htmx:evalDisallowedError', handler)
delete window.foo
}
calledEvent.should.equal(true)
})
it('can handle event types with dots', function() {
var btn = make("<button hx-on='my.custom.event: window.foo = true'>Foo</button>")
// IE11 doesn't support `new CustomEvent()` so call htmx' internal utility function
btn.dispatchEvent(htmx._('makeEvent')('my.custom.event'))
window.foo.should.equal(true)
delete window.foo
})
it('can handle being swapped using innerHTML', function() {
this.server.respondWith('GET', '/test', function(xhr) {
xhr.respond(200, {}, '<button id="bar" hx-on="click: window.bar = true">Bar</button>')
})
make(
'<div>' +
'<button id="swap" hx-get="/test" hx-target="#baz" hx-swap="innerHTML">Swap</button>' +
'<div id="baz"><button id="foo" hx-on="click: window.foo = true">Foo</button></div>' +
'</div>'
)
var fooBtn = byId('foo')
fooBtn.click()
window.foo.should.equal(true)
var swapBtn = byId('swap')
swapBtn.click()
this.server.respond()
var barBtn = byId('bar')
barBtn.click()
window.bar.should.equal(true)
delete window.foo
delete window.bar
})
it('cleans up all handlers when the DOM updates', function() {
// setup
window.foo = 0
window.bar = 0
var div = make("<div hx-on='increment-foo: window.foo++\nincrement-bar: window.bar++'>Foo</div>")
make('<div>Another Div</div>') // sole purpose is to update the DOM
// check there is just one handler against each event
htmx.trigger(div, 'increment-foo')
htmx.trigger(div, 'increment-bar')
window.foo.should.equal(1)
window.bar.should.equal(1)
// teardown
delete window.foo
delete window.bar
})
})

View File

@ -1298,4 +1298,20 @@ describe('Core htmx Shadow DOM Tests', function() {
this.server.respond()
values.should.deep.equal({ name: '', outside: '' })
})
it('can handle basic events w/ no other attributes', function() {
var btn = make("<button hx-on:click='window.foo = true'>Foo</button>")
btn.click()
window.foo.should.equal(true)
delete window.foo
})
it('can handle basic events w/ no other attributes in child', function() {
var div = make("<div><button id='b1' hx-on:click='window.foo = true'>Foo</button></div>")
var btn = div.querySelector("#b1")
btn.click()
window.foo.should.equal(true)
delete window.foo
})
})