diff --git a/src/htmx.js b/src/htmx.js index 7b1d9b57..25e51245 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -1347,6 +1347,16 @@ var htmx = (function() { return [findThisElement(elt, attrName)] } else { const result = querySelectorAllExt(elt, attrTarget) + // find `inherit` whole word in value, make sure it's surrounded by commas or is at the start/end of string + const shouldInherit = /(^|,)(\s*)inherit(\s*)($|,)/.test(attrTarget) + if (shouldInherit) { + const eltToInheritFrom = asElement(getClosestMatch(elt, function(parent) { + return parent !== elt && hasAttribute(asElement(parent), attrName) + })) + if (eltToInheritFrom) { + result.push(...findAttributeTargets(eltToInheritFrom, attrName)) + } + } if (result.length === 0) { logError('The selector "' + attrTarget + '" on ' + attrName + ' returned no matches!') return [DUMMY_ELT] @@ -1850,6 +1860,30 @@ var htmx = (function() { return oobElts.length > 0 } + /** + * Apply swapping class and then execute the swap with optional delay + * @param {string|Element} target + * @param {string} content + * @param {HtmxSwapSpecification} swapSpec + * @param {SwapOptions} [swapOptions] + */ + function swap(target, content, swapSpec, swapOptions) { + if (!swapOptions) { + swapOptions = {} + } + + target = resolveTarget(target) + target.classList.add(htmx.config.swappingClass) + const localSwap = function() { + runSwap(target, content, swapSpec, swapOptions) + } + if (swapSpec?.swapDelay && swapSpec.swapDelay > 0) { + getWindow().setTimeout(localSwap, swapSpec.swapDelay) + } else { + localSwap() + } + } + /** * Implements complete swapping pipeline, including: focus and selection preservation, * title updates, scroll, OOB swapping, normal swapping and settling @@ -1858,7 +1892,7 @@ var htmx = (function() { * @param {HtmxSwapSpecification} swapSpec * @param {SwapOptions} [swapOptions] */ - function swap(target, content, swapSpec, swapOptions) { + function runSwap(target, content, swapSpec, swapOptions) { if (!swapOptions) { swapOptions = {} } @@ -4168,7 +4202,7 @@ var htmx = (function() { } const target = etc.targetOverride || asElement(getTarget(elt)) if (target == null || target == DUMMY_ELT) { - triggerErrorEvent(elt, 'htmx:targetError', { target: getAttributeValue(elt, 'hx-target') }) + triggerErrorEvent(elt, 'htmx:targetError', { target: getClosestAttributeValue(elt, 'hx-target') }) maybeCall(reject) return promise } @@ -4790,8 +4824,6 @@ var htmx = (function() { swapSpec.ignoreTitle = ignoreTitle } - target.classList.add(htmx.config.swappingClass) - // optional transition API promise callbacks let settleResolve = null let settleReject = null @@ -4822,7 +4854,7 @@ var htmx = (function() { } swap(target, serverResponse, swapSpec, { - select: selectOverride || select, + select: selectOverride === 'unset' ? null : selectOverride || select, selectOOB, eventInfo: responseInfo, anchor: responseInfo.pathInfo.anchor, @@ -4878,12 +4910,7 @@ var htmx = (function() { }) } } - - if (swapSpec.swapDelay > 0) { - getWindow().setTimeout(doSwap, swapSpec.swapDelay) - } else { - doSwap() - } + doSwap() } if (isError) { triggerErrorEvent(elt, 'htmx:responseError', mergeObjects({ error: 'Response Status Error Code ' + xhr.status + ' from ' + responseInfo.pathInfo.requestPath }, responseInfo)) diff --git a/test/attributes/hx-include.js b/test/attributes/hx-include.js index d3b5bd1d..3617c130 100644 --- a/test/attributes/hx-include.js +++ b/test/attributes/hx-include.js @@ -434,4 +434,88 @@ describe('hx-include attribute', function() { this.server.respond() btn.innerHTML.should.equal('Clicked!') }) + + it('`inherit` can be used to expand parent hx-include', function() { + this.server.respondWith('POST', '/include', function(xhr) { + var params = getParameters(xhr) + params.i1.should.equal('test1') + params.i2.should.equal('test2') + xhr.respond(200, {}, 'Clicked!') + }) + make('
' + + ' ' + + '
' + + '' + + '') + var btn = byId('btn') + btn.click() + this.server.respond() + btn.innerHTML.should.equal('Clicked!') + }) + + it('`inherit` can be used to expand multiple parents hx-include', function() { + this.server.respondWith('POST', '/include', function(xhr) { + var params = getParameters(xhr) + params.i1.should.equal('test1') + params.i2.should.equal('test2') + params.i3.should.equal('test3') + xhr.respond(200, {}, 'Clicked!') + }) + make('
' + + '
' + + ' ' + + '
' + + '
' + + '' + + '' + + '') + var btn = byId('btn') + btn.click() + this.server.respond() + btn.innerHTML.should.equal('Clicked!') + }) + + it('`inherit` chain breaks properly', function() { + this.server.respondWith('POST', '/include', function(xhr) { + var params = getParameters(xhr) + should.not.exist(params.i1) + params.i2.should.equal('test2') + params.i3.should.equal('test3') + xhr.respond(200, {}, 'Clicked!') + }) + make('
' + + '
' + + ' ' + + '
' + + '
' + + '' + + '' + + '') + var btn = byId('btn') + btn.click() + this.server.respond() + btn.innerHTML.should.equal('Clicked!') + }) + + it('`inherit` syntax regex properly catches keyword', function() { + this.server.respondWith('POST', '/include', function(xhr) { + var params = getParameters(xhr) + params.i1.should.equal('test1') + params.i2.should.equal('test2') + params.i3.should.equal('test3') + xhr.respond(200, {}, 'Clicked!') + }) + make('
' + + '
' + + ' ' + + '
' + + '
' + + '' + + '' + + '') + var btn = byId('btn') + btn.click() + this.server.respond() + btn.innerHTML.should.equal('Clicked!') + }) }) diff --git a/test/attributes/hx-indicator.js b/test/attributes/hx-indicator.js index 2d5afc12..5e4e3e5f 100644 --- a/test/attributes/hx-indicator.js +++ b/test/attributes/hx-indicator.js @@ -123,4 +123,68 @@ describe('hx-indicator attribute', function() { b2.classList.contains('htmx-request').should.equal(false) a1.classList.contains('htmx-request').should.equal(false) }) + + it('`inherit` can be used to expand parent hx-indicator', function() { + this.server.respondWith('GET', '/test', 'Clicked!') + make('
' + + ' ' + + '
') + var btn = byId('btn') + var a1 = make('') + var a2 = make('') + btn.click() + btn.classList.contains('htmx-request').should.equal(false) + a1.classList.contains('htmx-request').should.equal(true) + a2.classList.contains('htmx-request').should.equal(true) + this.server.respond() + btn.classList.contains('htmx-request').should.equal(false) + a1.classList.contains('htmx-request').should.equal(false) + a2.classList.contains('htmx-request').should.equal(false) + }) + + it('`inherit` can be used to expand multiple parents hx-indicator', function() { + this.server.respondWith('GET', '/test', 'Clicked!') + make('
' + + '
' + + ' ' + + '
' + + '
') + var btn = byId('btn') + var a1 = make('') + var a2 = make('') + var a3 = make('') + btn.click() + btn.classList.contains('htmx-request').should.equal(false) + a1.classList.contains('htmx-request').should.equal(true) + a2.classList.contains('htmx-request').should.equal(true) + a3.classList.contains('htmx-request').should.equal(true) + this.server.respond() + btn.classList.contains('htmx-request').should.equal(false) + a1.classList.contains('htmx-request').should.equal(false) + a2.classList.contains('htmx-request').should.equal(false) + a3.classList.contains('htmx-request').should.equal(false) + }) + + it('`inherit` chain breaks properly', function() { + this.server.respondWith('GET', '/test', 'Clicked!') + make('
' + + '
' + + ' ' + + '
' + + '
') + var btn = byId('btn') + var a1 = make('') + var a2 = make('') + var a3 = make('') + btn.click() + btn.classList.contains('htmx-request').should.equal(false) + a1.classList.contains('htmx-request').should.equal(false) + a2.classList.contains('htmx-request').should.equal(true) + a3.classList.contains('htmx-request').should.equal(true) + this.server.respond() + btn.classList.contains('htmx-request').should.equal(false) + a1.classList.contains('htmx-request').should.equal(false) + a2.classList.contains('htmx-request').should.equal(false) + a3.classList.contains('htmx-request').should.equal(false) + }) }) diff --git a/test/core/api.js b/test/core/api.js index 707f262d..f566b8b1 100644 --- a/test/core/api.js +++ b/test/core/api.js @@ -483,6 +483,17 @@ describe('Core htmx API test', function() { output.innerHTML.should.be.equal('
Swapped!
') }) + it('swap works with a swap delay', function(done) { + var div = make("
") + div.innerText.should.equal('') + htmx.swap(div, 'jsswapped', { swapDelay: 10 }) + div.innerText.should.equal('') + setTimeout(function() { + div.innerText.should.equal('jsswapped') + done() + }, 30) + }) + it('swaps content properly (with select)', function() { var output = make('') htmx.swap('#output', '

Swapped!

', { swapStyle: 'innerHTML' }, { select: '#select-me' }) diff --git a/test/core/events.js b/test/core/events.js index bd3eb7d7..c220fabd 100644 --- a/test/core/events.js +++ b/test/core/events.js @@ -738,4 +738,36 @@ describe('Core htmx Events', function() { htmx.off('htmx:afterSwap', afterSwapHandler) } }) + + it('htmx:targetError should include the hx-target value', function() { + var target = null + var handler = htmx.on('htmx:targetError', function(evt) { + target = evt.detail.target + }) + try { + this.server.respondWith('GET', '/test', '') + var div = make('
') + div.click() + this.server.respond() + target.should.equal('#non-existent') + } finally { + htmx.off('htmx:targetError', handler) + } + }) + + it('htmx:targetError can include an inherited hx-target value', function() { + var target = null + var handler = htmx.on('htmx:targetError', function(evt) { + target = evt.detail.target + }) + try { + this.server.respondWith('GET', '/test', '') + make('
') + byId('child').click() + this.server.respond() + target.should.equal('#parent-target') + } finally { + htmx.off('htmx:targetError', handler) + } + }) }) diff --git a/test/core/headers.js b/test/core/headers.js index dd500ae1..466f2f8f 100644 --- a/test/core/headers.js +++ b/test/core/headers.js @@ -299,6 +299,16 @@ describe('Core htmx AJAX headers', function() { div.innerHTML.should.equal('
bar
') }) + it('should handle HX-Reselect unset', function() { + this.server.respondWith('GET', '/test', [200, { 'HX-Reselect': 'unset' }, 'bar']) + + var div = make('
') + div.click() + this.server.respond() + + div.innerHTML.should.equal('bar') + }) + it('should handle simple string HX-Trigger-After-Swap response header properly w/ outerHTML swap', function() { this.server.respondWith('GET', '/test', [200, { 'HX-Trigger-After-Swap': 'foo' }, ''])