Skip to content
Merged
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
29 changes: 14 additions & 15 deletions src/htmx.js
Original file line number Diff line number Diff line change
Expand Up @@ -2431,23 +2431,22 @@ var htmx = (function() {
* @returns {boolean}
*/
function shouldCancel(evt, elt) {
if (evt.type === 'submit' || evt.type === 'click') {
// use elt from event that was submitted/clicked where possible to determining if default form/link behavior should be canceled
elt = asElement(evt.target) || elt
if (elt.tagName === 'FORM') {
return true
}
// find button wrapping the event elt
const btn = elt.closest('input[type="submit"], button')
// @ts-ignore Do not cancel on buttons that 1) don't have a related form or 2) have a type attribute of 'reset'/'button'.
// The properties will resolve to undefined for elements that don't define 'type' or 'form', which is fine
if (evt.type === 'submit' && elt.tagName === 'FORM') {
return true
} else if (evt.type === 'click') {
// find button wrapping the trigger element
const btn = /** @type {HTMLButtonElement|HTMLInputElement|null} */ (elt.closest('input[type="submit"], button'))
// Do not cancel on buttons that 1) don't have a related form or 2) have a type attribute of 'reset'/'button'.
if (btn && btn.form && btn.type === 'submit') {
return true
}
elt = elt.closest('a')
// @ts-ignore check for a link wrapping the event elt or if elt is a link. elt will be link so href check is fine
if (elt && elt.href &&
(elt.getAttribute('href') === '#' || elt.getAttribute('href').indexOf('#') !== 0)) {

// find link wrapping the trigger element
const link = elt.closest('a')
// Allow links with href="#fragment" (anchors with content after #) to perform normal fragment navigation.
// Cancel default action for links with href="#" (bare hash) to prevent scrolling to top and unwanted URL changes.
const samePageAnchor = /^#.+/
if (link && link.href && !samePageAnchor.test(link.getAttribute('href'))) {
return true
}
}
Expand Down Expand Up @@ -2524,7 +2523,7 @@ var htmx = (function() {
if (ignoreBoostedAnchorCtrlClick(elt, evt)) {
return
}
if (explicitCancel || shouldCancel(evt, elt)) {
if (explicitCancel || shouldCancel(evt, eltToListenOn)) {
evt.preventDefault()
}
if (maybeFilterEvent(triggerSpec, elt, evt)) {
Expand Down
10 changes: 5 additions & 5 deletions test/core/internals.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,20 +98,20 @@ describe('Core htmx internals Tests', function() {

var form = make('<form></form>')
htmx._('shouldCancel')({ type: 'submit', target: form }, form).should.equal(true)
htmx._('shouldCancel')({ type: 'click', target: form }, form).should.equal(true)

// falls back to check elt tag when target is not an element
htmx._('shouldCancel')({ type: 'click', target: null }, form).should.equal(true)

// check that events targeting elements that shouldn't cancel don't cancel
htmx._('shouldCancel')({ type: 'submit', target: anchorThatShouldNotCancel }, form).should.equal(false)
htmx._('shouldCancel')({ type: 'click', target: divThatShouldNotCancel }, form).should.equal(false)

// check elements inside links getting click events should cancel parent links
var anchorWithButton = make("<a href='/foo'><button></button></a>")
htmx._('shouldCancel')({ type: 'click', target: anchorWithButton.firstChild }, anchorWithButton).should.equal(true)
htmx._('shouldCancel')({ type: 'click', target: anchorWithButton.firstChild }, anchorWithButton.firstChild).should.equal(true)

// check that links inside htmx elements should not cancel
var divWithLink = make("<div hx-get='/data'><a href='/page'>Link</a></div>")
var link = divWithLink.querySelector('a')
htmx._('shouldCancel')({ type: 'click', target: link }, divWithLink).should.equal(false)

form = make('<form id="f1">' +
'<input id="insideInput" type="submit">' +
'<button id="insideFormBtn"></button>' +
Expand Down
93 changes: 90 additions & 3 deletions test/core/regressions.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,11 @@ describe('Core htmx Regression Tests', function() {
it('does not submit with a false condition on a form', function() {
this.server.respondWith('POST', '/test', 'Submitted')
var defaultPrevented = false
htmx.on('click', function(evt) {
htmx.on('submit', function(evt) {
defaultPrevented = evt.defaultPrevented
})
var form = make('<form hx-post="/test" hx-trigger="click[false]"></form>')
form.click()
var form = make('<form hx-post="/test" hx-trigger="submit[false]"><button id="b1">submit</button></form>')
byId('b1').click()
this.server.respond()
defaultPrevented.should.equal(true)
})
Expand Down Expand Up @@ -385,6 +385,93 @@ describe('Core htmx Regression Tests', function() {
span.click()
})

it('a htmx enabled element inside a form button will prevent the button submitting a form', function(done) {
var defaultPrevented = 'unset'
var form = make('<form><button><span hx-get="/foo">test</span></button></form>')
var button = form.firstChild
var span = button.firstChild

htmx.on(button, 'click', function(evt) {
// we need to wait so the state of the evt is finalized
setTimeout(() => {
defaultPrevented = evt.defaultPrevented
try {
defaultPrevented.should.equal(true)
done()
} catch (err) {
done(err)
}
}, 0)
})

span.click()
})

it('from: trigger on form prevents default form submission', function(done) {
var defaultPrevented = 'unset'
var form = make('<form id="test-form" action="/submit"><input type="submit" value="Submit"></form>')
var div = make('<div hx-post="/test" hx-trigger="submit from:#test-form"></div>')
var submitBtn = form.firstChild

htmx.on(form, 'submit', function(evt) {
defaultPrevented = evt.defaultPrevented // Capture state before our preventDefault
evt.preventDefault() // Prevent navigation in case test fails
setTimeout(() => {
try {
defaultPrevented.should.equal(true)
done()
} catch (err) {
done(err)
}
}, 0)
})

submitBtn.click()
})

it('from: trigger on button prevents default form submission', function(done) {
var defaultPrevented = 'unset'
var form = make('<form><button id="test-btn" type="submit">Submit</button></form>')
var div = make('<div hx-post="/test" hx-trigger="click from:#test-btn"></div>')
var button = byId('test-btn')

htmx.on(button, 'click', function(evt) {
defaultPrevented = evt.defaultPrevented // Capture state before our preventDefault
evt.preventDefault() // Prevent form submission in case test fails
setTimeout(() => {
try {
defaultPrevented.should.equal(true)
done()
} catch (err) {
done(err)
}
}, 0)
})

button.click()
})

it('from: trigger on link prevents default navigation', function(done) {
var defaultPrevented = 'unset'
var link = make('<a id="test-link" href="/page">Go to page</a>')
var div = make('<div hx-get="/test" hx-trigger="click from:#test-link"></div>')

htmx.on(link, 'click', function(evt) {
defaultPrevented = evt.defaultPrevented // Capture state before our preventDefault
evt.preventDefault() // Prevent navigation in case test fails
setTimeout(() => {
try {
defaultPrevented.should.equal(true)
done()
} catch (err) {
done(err)
}
}, 0)
})

link.click()
})

it('check deleting button during click does not trigger exception error in getRelatedFormData when button can no longer find form', function() {
var defaultPrevented = 'unset'
var form = make('<form><button>delete</button></form>')
Expand Down