Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/htmx.js
Original file line number Diff line number Diff line change
Expand Up @@ -5116,7 +5116,7 @@ var htmx = (function() {
"[hx-trigger='restored'],[data-hx-trigger='restored']"
)
body.addEventListener('htmx:abort', function(evt) {
const target = evt.target
const target = (/** @type {CustomEvent} */(evt)).detail.elt || evt.target
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we try evt.target first? This seems like it could break behavior people that were using detail.elt for other reasons as it is

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the issue is the evt.target is not reliable if used inside shadow dom as it tries to protect any shadowDOM element encapsulation by replacing it with the host wrapping elt.

If we look at the triggerEvent logic used by all proper htmx raised events you have this:

  function triggerEvent(elt, eventName, detail) {
    elt = resolveTarget(elt)
    if (detail == null) {
      detail = {}
    }
    detail.elt = elt
    const event = makeEvent(eventName, detail)

detail.elt is always overridden to be the triggered on elt which is the ideal elt we want to use for both shadow dom and normal use cases. It can never be user overridden to anything else in a htmx custom event.

It could be possible for someone to create htmx;abort events manually without using htmx and these would not have a valid detail.elt which is why i've left a || evt.target as a fallback just to be safe.

const internalData = getInternalData(target)
if (internalData && internalData.xhr) {
internalData.xhr.abort()
Expand Down
60 changes: 60 additions & 0 deletions test/core/shadowdom.js
Original file line number Diff line number Diff line change
Expand Up @@ -1341,4 +1341,64 @@ describe('Core htmx Shadow DOM Tests', function() {
getWorkArea().innerHTML.should.equal('Clicked!')
chai.expect(getWorkArea().shadowRoot).to.be.a('null')
})

it('hx-sync works properly in shadow DOM', function() {
var count = 0
this.server.respondWith('GET', '/test', function(xhr) {
xhr.respond(200, {}, 'Click ' + count++)
})
make('<div hx-sync="this:drop"><button id="b1" hx-get="/test">Initial</button>' +
' <button id="b2" hx-get="/test">Initial</button></div>')
var b1 = byId('b1')
var b2 = byId('b2')
b1.click()
b2.click()
this.server.respond()
this.server.respond()
b1.innerHTML.should.equal('Click 0')
b2.innerHTML.should.equal('Initial')
})

it('can abort a request programmatically in shadow DOM', function() {
var count = 0
var abortedCount = 0
this.server.respondWith('GET', '/test', function(xhr) {
xhr.respond(200, {}, 'Click ' + count++)
})
make('<div><button id="b1" hx-get="/test">Initial</button>' +
' <button id="b2" hx-get="/test">Initial</button></div>')
var b1 = byId('b1')
var b2 = byId('b2')

// Listen for abort events to verify they're working
htmx.on(b1, 'htmx:abort', function() { abortedCount++ })

b1.click()
b2.click()

htmx.trigger(b1, 'htmx:abort')

this.server.respond()
this.server.respond()
b1.innerHTML.should.equal('Initial')
b2.innerHTML.should.equal('Click 0')
abortedCount.should.equal(1)
})

it('hx-sync abort strategy works in shadow DOM', function() {
var count = 0
this.server.respondWith('GET', '/test', function(xhr) {
xhr.respond(200, {}, 'Click ' + count++)
})
make('<div hx-sync="this"><button hx-sync="closest div:abort" id="b1" hx-get="/test">Initial</button>' +
' <button id="b2" hx-get="/test">Initial</button></div>')
var b1 = byId('b1')
var b2 = byId('b2')
b1.click()
b2.click()
this.server.respond()
this.server.respond()
b1.innerHTML.should.equal('Initial')
b2.innerHTML.should.equal('Click 0')
})
})