Skip to content

Commit 8ec48d9

Browse files
committed
Merge remote-tracking branch 'origin/dev' into dev
2 parents 70f41e0 + 82546db commit 8ec48d9

6 files changed

Lines changed: 239 additions & 11 deletions

File tree

src/htmx.js

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,6 +1347,16 @@ var htmx = (function() {
13471347
return [findThisElement(elt, attrName)]
13481348
} else {
13491349
const result = querySelectorAllExt(elt, attrTarget)
1350+
// find `inherit` whole word in value, make sure it's surrounded by commas or is at the start/end of string
1351+
const shouldInherit = /(^|,)(\s*)inherit(\s*)($|,)/.test(attrTarget)
1352+
if (shouldInherit) {
1353+
const eltToInheritFrom = asElement(getClosestMatch(elt, function(parent) {
1354+
return parent !== elt && hasAttribute(asElement(parent), attrName)
1355+
}))
1356+
if (eltToInheritFrom) {
1357+
result.push(...findAttributeTargets(eltToInheritFrom, attrName))
1358+
}
1359+
}
13501360
if (result.length === 0) {
13511361
logError('The selector "' + attrTarget + '" on ' + attrName + ' returned no matches!')
13521362
return [DUMMY_ELT]
@@ -1850,6 +1860,30 @@ var htmx = (function() {
18501860
return oobElts.length > 0
18511861
}
18521862

1863+
/**
1864+
* Apply swapping class and then execute the swap with optional delay
1865+
* @param {string|Element} target
1866+
* @param {string} content
1867+
* @param {HtmxSwapSpecification} swapSpec
1868+
* @param {SwapOptions} [swapOptions]
1869+
*/
1870+
function swap(target, content, swapSpec, swapOptions) {
1871+
if (!swapOptions) {
1872+
swapOptions = {}
1873+
}
1874+
1875+
target = resolveTarget(target)
1876+
target.classList.add(htmx.config.swappingClass)
1877+
const localSwap = function() {
1878+
runSwap(target, content, swapSpec, swapOptions)
1879+
}
1880+
if (swapSpec?.swapDelay && swapSpec.swapDelay > 0) {
1881+
getWindow().setTimeout(localSwap, swapSpec.swapDelay)
1882+
} else {
1883+
localSwap()
1884+
}
1885+
}
1886+
18531887
/**
18541888
* Implements complete swapping pipeline, including: focus and selection preservation,
18551889
* title updates, scroll, OOB swapping, normal swapping and settling
@@ -1858,7 +1892,7 @@ var htmx = (function() {
18581892
* @param {HtmxSwapSpecification} swapSpec
18591893
* @param {SwapOptions} [swapOptions]
18601894
*/
1861-
function swap(target, content, swapSpec, swapOptions) {
1895+
function runSwap(target, content, swapSpec, swapOptions) {
18621896
if (!swapOptions) {
18631897
swapOptions = {}
18641898
}
@@ -4168,7 +4202,7 @@ var htmx = (function() {
41684202
}
41694203
const target = etc.targetOverride || asElement(getTarget(elt))
41704204
if (target == null || target == DUMMY_ELT) {
4171-
triggerErrorEvent(elt, 'htmx:targetError', { target: getAttributeValue(elt, 'hx-target') })
4205+
triggerErrorEvent(elt, 'htmx:targetError', { target: getClosestAttributeValue(elt, 'hx-target') })
41724206
maybeCall(reject)
41734207
return promise
41744208
}
@@ -4790,8 +4824,6 @@ var htmx = (function() {
47904824
swapSpec.ignoreTitle = ignoreTitle
47914825
}
47924826

4793-
target.classList.add(htmx.config.swappingClass)
4794-
47954827
// optional transition API promise callbacks
47964828
let settleResolve = null
47974829
let settleReject = null
@@ -4822,7 +4854,7 @@ var htmx = (function() {
48224854
}
48234855

48244856
swap(target, serverResponse, swapSpec, {
4825-
select: selectOverride || select,
4857+
select: selectOverride === 'unset' ? null : selectOverride || select,
48264858
selectOOB,
48274859
eventInfo: responseInfo,
48284860
anchor: responseInfo.pathInfo.anchor,
@@ -4878,12 +4910,7 @@ var htmx = (function() {
48784910
})
48794911
}
48804912
}
4881-
4882-
if (swapSpec.swapDelay > 0) {
4883-
getWindow().setTimeout(doSwap, swapSpec.swapDelay)
4884-
} else {
4885-
doSwap()
4886-
}
4913+
doSwap()
48874914
}
48884915
if (isError) {
48894916
triggerErrorEvent(elt, 'htmx:responseError', mergeObjects({ error: 'Response Status Error Code ' + xhr.status + ' from ' + responseInfo.pathInfo.requestPath }, responseInfo))

test/attributes/hx-include.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,4 +434,88 @@ describe('hx-include attribute', function() {
434434
this.server.respond()
435435
btn.innerHTML.should.equal('Clicked!')
436436
})
437+
438+
it('`inherit` can be used to expand parent hx-include', function() {
439+
this.server.respondWith('POST', '/include', function(xhr) {
440+
var params = getParameters(xhr)
441+
params.i1.should.equal('test1')
442+
params.i2.should.equal('test2')
443+
xhr.respond(200, {}, 'Clicked!')
444+
})
445+
make('<div hx-include="#i1">' +
446+
' <button id="btn" hx-include="inherit, #i2" hx-post="/include"></button>' +
447+
'</div>' +
448+
'<input id="i1" name="i1" value="test1"/>' +
449+
'<input id="i2" name="i2" value="test2"/>')
450+
var btn = byId('btn')
451+
btn.click()
452+
this.server.respond()
453+
btn.innerHTML.should.equal('Clicked!')
454+
})
455+
456+
it('`inherit` can be used to expand multiple parents hx-include', function() {
457+
this.server.respondWith('POST', '/include', function(xhr) {
458+
var params = getParameters(xhr)
459+
params.i1.should.equal('test1')
460+
params.i2.should.equal('test2')
461+
params.i3.should.equal('test3')
462+
xhr.respond(200, {}, 'Clicked!')
463+
})
464+
make('<div hx-include="#i1">' +
465+
' <div hx-include="inherit, #i2">' +
466+
' <button id="btn" hx-include="inherit, #i3" hx-post="/include"></button>' +
467+
' </div>' +
468+
'</div>' +
469+
'<input id="i1" name="i1" value="test1"/>' +
470+
'<input id="i2" name="i2" value="test2"/>' +
471+
'<input id="i3" name="i3" value="test3"/>')
472+
var btn = byId('btn')
473+
btn.click()
474+
this.server.respond()
475+
btn.innerHTML.should.equal('Clicked!')
476+
})
477+
478+
it('`inherit` chain breaks properly', function() {
479+
this.server.respondWith('POST', '/include', function(xhr) {
480+
var params = getParameters(xhr)
481+
should.not.exist(params.i1)
482+
params.i2.should.equal('test2')
483+
params.i3.should.equal('test3')
484+
xhr.respond(200, {}, 'Clicked!')
485+
})
486+
make('<div hx-include="#i1">' +
487+
' <div hx-include="#i2">' +
488+
' <button id="btn" hx-include="inherit, #i3" hx-post="/include"></button>' +
489+
' </div>' +
490+
'</div>' +
491+
'<input id="i1" name="i1" value="test1"/>' +
492+
'<input id="i2" name="i2" value="test2"/>' +
493+
'<input id="i3" name="i3" value="test3"/>')
494+
var btn = byId('btn')
495+
btn.click()
496+
this.server.respond()
497+
btn.innerHTML.should.equal('Clicked!')
498+
})
499+
500+
it('`inherit` syntax regex properly catches keyword', function() {
501+
this.server.respondWith('POST', '/include', function(xhr) {
502+
var params = getParameters(xhr)
503+
params.i1.should.equal('test1')
504+
params.i2.should.equal('test2')
505+
params.i3.should.equal('test3')
506+
xhr.respond(200, {}, 'Clicked!')
507+
})
508+
make('<div hx-include="#i1">' +
509+
' <div hx-include="#i2, inherit,.nonexistent-class">' +
510+
' <button id="btn" hx-include="customtag,inherit , #i3" hx-post="/include"></button>' +
511+
' </div>' +
512+
'</div>' +
513+
'<input id="i1" name="i1" value="test1"/>' +
514+
'<input id="i2" name="i2" value="test2"/>' +
515+
'<input id="i3" name="i3" value="test3"/>')
516+
var btn = byId('btn')
517+
btn.click()
518+
this.server.respond()
519+
btn.innerHTML.should.equal('Clicked!')
520+
})
437521
})

test/attributes/hx-indicator.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,68 @@ describe('hx-indicator attribute', function() {
123123
b2.classList.contains('htmx-request').should.equal(false)
124124
a1.classList.contains('htmx-request').should.equal(false)
125125
})
126+
127+
it('`inherit` can be used to expand parent hx-indicator', function() {
128+
this.server.respondWith('GET', '/test', 'Clicked!')
129+
make('<div hx-indicator="#a1">' +
130+
' <button id="btn" hx-get="/test" hx-indicator="inherit, #a2">Click Me!</button>' +
131+
'</div>')
132+
var btn = byId('btn')
133+
var a1 = make('<a id="a1"></a>')
134+
var a2 = make('<a id="a2"></a>')
135+
btn.click()
136+
btn.classList.contains('htmx-request').should.equal(false)
137+
a1.classList.contains('htmx-request').should.equal(true)
138+
a2.classList.contains('htmx-request').should.equal(true)
139+
this.server.respond()
140+
btn.classList.contains('htmx-request').should.equal(false)
141+
a1.classList.contains('htmx-request').should.equal(false)
142+
a2.classList.contains('htmx-request').should.equal(false)
143+
})
144+
145+
it('`inherit` can be used to expand multiple parents hx-indicator', function() {
146+
this.server.respondWith('GET', '/test', 'Clicked!')
147+
make('<div hx-indicator="#a1">' +
148+
' <div hx-indicator="inherit, #a2">' +
149+
' <button id="btn" hx-get="/test" hx-indicator="inherit, #a3">Click Me!</button>' +
150+
' </div>' +
151+
'</div>')
152+
var btn = byId('btn')
153+
var a1 = make('<a id="a1"></a>')
154+
var a2 = make('<a id="a2"></a>')
155+
var a3 = make('<a id="a3"></a>')
156+
btn.click()
157+
btn.classList.contains('htmx-request').should.equal(false)
158+
a1.classList.contains('htmx-request').should.equal(true)
159+
a2.classList.contains('htmx-request').should.equal(true)
160+
a3.classList.contains('htmx-request').should.equal(true)
161+
this.server.respond()
162+
btn.classList.contains('htmx-request').should.equal(false)
163+
a1.classList.contains('htmx-request').should.equal(false)
164+
a2.classList.contains('htmx-request').should.equal(false)
165+
a3.classList.contains('htmx-request').should.equal(false)
166+
})
167+
168+
it('`inherit` chain breaks properly', function() {
169+
this.server.respondWith('GET', '/test', 'Clicked!')
170+
make('<div hx-indicator="#a1">' +
171+
' <div hx-indicator="#a2">' +
172+
' <button id="btn" hx-get="/test" hx-indicator="inherit, #a3">Click Me!</button>' +
173+
' </div>' +
174+
'</div>')
175+
var btn = byId('btn')
176+
var a1 = make('<a id="a1"></a>')
177+
var a2 = make('<a id="a2"></a>')
178+
var a3 = make('<a id="a3"></a>')
179+
btn.click()
180+
btn.classList.contains('htmx-request').should.equal(false)
181+
a1.classList.contains('htmx-request').should.equal(false)
182+
a2.classList.contains('htmx-request').should.equal(true)
183+
a3.classList.contains('htmx-request').should.equal(true)
184+
this.server.respond()
185+
btn.classList.contains('htmx-request').should.equal(false)
186+
a1.classList.contains('htmx-request').should.equal(false)
187+
a2.classList.contains('htmx-request').should.equal(false)
188+
a3.classList.contains('htmx-request').should.equal(false)
189+
})
126190
})

test/core/api.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,17 @@ describe('Core htmx API test', function() {
483483
output.innerHTML.should.be.equal('<div>Swapped!</div>')
484484
})
485485

486+
it('swap works with a swap delay', function(done) {
487+
var div = make("<div hx-get='/test'></div>")
488+
div.innerText.should.equal('')
489+
htmx.swap(div, 'jsswapped', { swapDelay: 10 })
490+
div.innerText.should.equal('')
491+
setTimeout(function() {
492+
div.innerText.should.equal('jsswapped')
493+
done()
494+
}, 30)
495+
})
496+
486497
it('swaps content properly (with select)', function() {
487498
var output = make('<output id="output"/>')
488499
htmx.swap('#output', '<div><p id="select-me">Swapped!</p></div>', { swapStyle: 'innerHTML' }, { select: '#select-me' })

test/core/events.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,4 +738,36 @@ describe('Core htmx Events', function() {
738738
htmx.off('htmx:afterSwap', afterSwapHandler)
739739
}
740740
})
741+
742+
it('htmx:targetError should include the hx-target value', function() {
743+
var target = null
744+
var handler = htmx.on('htmx:targetError', function(evt) {
745+
target = evt.detail.target
746+
})
747+
try {
748+
this.server.respondWith('GET', '/test', '')
749+
var div = make('<div hx-post="/test" hx-target="#non-existent"></div>')
750+
div.click()
751+
this.server.respond()
752+
target.should.equal('#non-existent')
753+
} finally {
754+
htmx.off('htmx:targetError', handler)
755+
}
756+
})
757+
758+
it('htmx:targetError can include an inherited hx-target value', function() {
759+
var target = null
760+
var handler = htmx.on('htmx:targetError', function(evt) {
761+
target = evt.detail.target
762+
})
763+
try {
764+
this.server.respondWith('GET', '/test', '')
765+
make('<div hx-target="#parent-target"><div id="child" hx-post="/test"></div></div>')
766+
byId('child').click()
767+
this.server.respond()
768+
target.should.equal('#parent-target')
769+
} finally {
770+
htmx.off('htmx:targetError', handler)
771+
}
772+
})
741773
})

test/core/headers.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,16 @@ describe('Core htmx AJAX headers', function() {
299299
div.innerHTML.should.equal('<div id="d2">bar</div>')
300300
})
301301

302+
it('should handle HX-Reselect unset', function() {
303+
this.server.respondWith('GET', '/test', [200, { 'HX-Reselect': 'unset' }, 'bar'])
304+
305+
var div = make('<div hx-get="/test" hx-select="#d2"></div>')
306+
div.click()
307+
this.server.respond()
308+
309+
div.innerHTML.should.equal('bar')
310+
})
311+
302312
it('should handle simple string HX-Trigger-After-Swap response header properly w/ outerHTML swap', function() {
303313
this.server.respondWith('GET', '/test', [200, { 'HX-Trigger-After-Swap': 'foo' }, ''])
304314

0 commit comments

Comments
 (0)