From 07db00f595d6d6f9933da8093a1ba18b89890313 Mon Sep 17 00:00:00 2001 From: Matteo Smaila Date: Wed, 18 Jun 2025 15:04:22 +0200 Subject: [PATCH 1/9] Added regression test for swap=outerHTML unexpected behavior, checked it failes, implemented initial fix in htmx.js that makes (all) test(s) run and pass. --- src/htmx.js | 33 ++++++++++++++++++++++++++++++++- test/core/regressions.js | 15 +++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/htmx.js b/src/htmx.js index 370cc0ff6..3c7e14310 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -1412,9 +1412,39 @@ var htmx = (function() { * @param {Element} mergeFrom */ function cloneAttributes(mergeTo, mergeFrom) { + const mergeToAttributesCopy = [] + for (const attribute of mergeTo.attributes) { + mergeToAttributesCopy.push(attribute) + } + + const mergeFromAttributesCopy = [] + for (const attribute of mergeFrom.attributes) { + mergeFromAttributesCopy.push(attribute) + } + forEach(mergeTo.attributes, function(attr) { if (!mergeFrom.hasAttribute(attr.name) && shouldSettleAttribute(attr.name)) { - mergeTo.removeAttribute(attr.name) + if (attr.name == 'class') { + const newClasses = [] + if (mergeTo.classList.contains(htmx.config.swappingClass)) { + newClasses.push(htmx.config.swappingClass) + } + if (mergeTo.classList.contains(htmx.config.addedClass)) { + newClasses.push(htmx.config.addedClass) + } + if (mergeTo.classList.contains(htmx.config.settlingClass)) { + newClasses.push(htmx.config.settlingClass) + } + + mergeTo.removeAttribute('class') + if (newClasses.length > 0) { + for (const newClass of newClasses) { + mergeTo.classList.add(newClass) + } + } + } else { + mergeTo.removeAttribute(attr.name) + } } }) forEach(mergeFrom.attributes, function(attr) { @@ -1972,6 +2002,7 @@ var htmx = (function() { target.classList.remove(htmx.config.swappingClass) forEach(settleInfo.elts, function(elt) { if (elt.classList) { + elt.classList.remove(htmx.config.swappingClass) elt.classList.add(htmx.config.settlingClass) } triggerEvent(elt, 'htmx:afterSwap', swapOptions.eventInfo) diff --git a/test/core/regressions.js b/test/core/regressions.js index c530011d6..c484ec0b4 100644 --- a/test/core/regressions.js +++ b/test/core/regressions.js @@ -270,4 +270,19 @@ describe('Core htmx Regression Tests', function() { done() }, 50) }) + + it('swap=outerHTML clears htmx-swapping class', function(done) { + this.server.respondWith('GET', '/test', '
Test
') + + var btn = make('') + var div = make('
') + btn.click() + + this.server.respond() + + var div = byId('test-div') + const isSwappingThere = div.classList.contains('htmx-swapping') + isSwappingThere.should.equal(false) + done() + }) }) From b96aa89e227b9b50ce5ef1a15ed0c7ee9be46532 Mon Sep 17 00:00:00 2001 From: Matteo Smaila Date: Wed, 18 Jun 2025 15:06:19 +0200 Subject: [PATCH 2/9] Renamed variable in my regression test to be more clear. --- test/core/regressions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/core/regressions.js b/test/core/regressions.js index c484ec0b4..9e27adac7 100644 --- a/test/core/regressions.js +++ b/test/core/regressions.js @@ -281,8 +281,8 @@ describe('Core htmx Regression Tests', function() { this.server.respond() var div = byId('test-div') - const isSwappingThere = div.classList.contains('htmx-swapping') - isSwappingThere.should.equal(false) + const isSwappingClassStillThere = div.classList.contains('htmx-swapping') + isSwappingClassStillThere.should.equal(false) done() }) }) From be5eeab9cb9cad55d624476d05f59af3bd2d7227 Mon Sep 17 00:00:00 2001 From: Matteo Smaila Date: Wed, 18 Jun 2025 16:53:24 +0200 Subject: [PATCH 3/9] I noticed I wasn't using the copies of the attributes I introduced.Tests were passing and I know why, though. This means I miss one more regression test for the bug in cloneAttributes. --- src/htmx.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/htmx.js b/src/htmx.js index 3c7e14310..a1751df5c 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -1422,7 +1422,7 @@ var htmx = (function() { mergeFromAttributesCopy.push(attribute) } - forEach(mergeTo.attributes, function(attr) { + forEach(mergeToAttributesCopy, function(attr) { if (!mergeFrom.hasAttribute(attr.name) && shouldSettleAttribute(attr.name)) { if (attr.name == 'class') { const newClasses = [] @@ -1447,7 +1447,7 @@ var htmx = (function() { } } }) - forEach(mergeFrom.attributes, function(attr) { + forEach(mergeFromAttributesCopy, function(attr) { if (shouldSettleAttribute(attr.name)) { mergeTo.setAttribute(attr.name, attr.value) } From 0234bb55020012ca40320ad95379e552c29255d0 Mon Sep 17 00:00:00 2001 From: Matteo Smaila Date: Wed, 18 Jun 2025 17:59:29 +0200 Subject: [PATCH 4/9] Added one more regression test for the fix in cloneAttributes. --- test/core/regressions.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/core/regressions.js b/test/core/regressions.js index 9e27adac7..8f563369c 100644 --- a/test/core/regressions.js +++ b/test/core/regressions.js @@ -285,4 +285,18 @@ describe('Core htmx Regression Tests', function() { isSwappingClassStillThere.should.equal(false) done() }) + + it('swap=outerHTML clear original user-defined classes', function(done) { + this.server.respondWith('GET', '/test', '
Test
') + + var btn = make('') + var div = make('
') + btn.click() + + this.server.respond() + + var div = byId('test-div') + div.classList.length.should.equal(0) + done() + }) }) From 9aee1b3493a79aba3d6d9e88dc12c6ce81ef8fdd Mon Sep 17 00:00:00 2001 From: Matteo Smaila Date: Wed, 18 Jun 2025 18:02:00 +0200 Subject: [PATCH 5/9] Made preservation of htmx- prefixed classes more robust in cloneAttributes after I noted they could as well be removed by mergeTo.setAttribute in the second forEach loop. --- src/htmx.js | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/src/htmx.js b/src/htmx.js index a1751df5c..1712e50f3 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -1407,7 +1407,7 @@ var htmx = (function() { return false } - /** + /** * @param {Element} mergeTo * @param {Element} mergeFrom */ @@ -1422,29 +1422,16 @@ var htmx = (function() { mergeFromAttributesCopy.push(attribute) } + const htmxClasses = [] + for (const klass of mergeTo.classList) { + if (klass.startsWith('htmx-')) { + htmxClasses.push(klass) + } + } + forEach(mergeToAttributesCopy, function(attr) { if (!mergeFrom.hasAttribute(attr.name) && shouldSettleAttribute(attr.name)) { - if (attr.name == 'class') { - const newClasses = [] - if (mergeTo.classList.contains(htmx.config.swappingClass)) { - newClasses.push(htmx.config.swappingClass) - } - if (mergeTo.classList.contains(htmx.config.addedClass)) { - newClasses.push(htmx.config.addedClass) - } - if (mergeTo.classList.contains(htmx.config.settlingClass)) { - newClasses.push(htmx.config.settlingClass) - } - - mergeTo.removeAttribute('class') - if (newClasses.length > 0) { - for (const newClass of newClasses) { - mergeTo.classList.add(newClass) - } - } - } else { - mergeTo.removeAttribute(attr.name) - } + mergeTo.removeAttribute(attr.name) } }) forEach(mergeFromAttributesCopy, function(attr) { @@ -1452,6 +1439,10 @@ var htmx = (function() { mergeTo.setAttribute(attr.name, attr.value) } }) + + for (const htmxClass of htmxClasses) { + mergeTo.classList.add(htmxClass) + } } /** From cf4b5a42c4c9c0468887ad6cbb89e07029adfcd7 Mon Sep 17 00:00:00 2001 From: Matteo Smaila Date: Wed, 18 Jun 2025 18:07:32 +0200 Subject: [PATCH 6/9] Started as a typo-fix, ended up renaming regression tests to be more explicit. --- test/core/regressions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/core/regressions.js b/test/core/regressions.js index 8f563369c..d3f1e7586 100644 --- a/test/core/regressions.js +++ b/test/core/regressions.js @@ -286,7 +286,7 @@ describe('Core htmx Regression Tests', function() { done() }) - it('swap=outerHTML clear original user-defined classes', function(done) { + it('swap=outerHTML clears original user-defined classes', function(done) { this.server.respondWith('GET', '/test', '
Test
') var btn = make('') From 893e151bc39334622e3362c1846dc8ec621edf87 Mon Sep 17 00:00:00 2001 From: Matteo Smaila Date: Wed, 18 Jun 2025 18:08:32 +0200 Subject: [PATCH 7/9] Started as a typo-fix, ended up renaming regression tests to be more explicit. --- test/core/regressions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/core/regressions.js b/test/core/regressions.js index d3f1e7586..e96c2d80b 100644 --- a/test/core/regressions.js +++ b/test/core/regressions.js @@ -271,7 +271,7 @@ describe('Core htmx Regression Tests', function() { }, 50) }) - it('swap=outerHTML clears htmx-swapping class', function(done) { + it('swap=outerHTML clears htmx-swapping class when old node has a style attribute and no class', function(done) { this.server.respondWith('GET', '/test', '
Test
') var btn = make('') @@ -286,7 +286,7 @@ describe('Core htmx Regression Tests', function() { done() }) - it('swap=outerHTML clears original user-defined classes', function(done) { + it('swap=outerHTML won\'t carry over user-defined classes when old node has a style attribute before the class attribute', function(done) { this.server.respondWith('GET', '/test', '
Test
') var btn = make('') From c51ddf5731e3faaabb0eb39ad75c4d1adfd8d88c Mon Sep 17 00:00:00 2001 From: Matteo Smaila Date: Wed, 18 Jun 2025 18:46:52 +0200 Subject: [PATCH 8/9] Removed space that I accidentally added before. --- src/htmx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/htmx.js b/src/htmx.js index 1712e50f3..ff79babb8 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -1407,7 +1407,7 @@ var htmx = (function() { return false } - /** + /** * @param {Element} mergeTo * @param {Element} mergeFrom */ From 35035ca2de8e454e71d7553e0cf0e3c989b508b9 Mon Sep 17 00:00:00 2001 From: Matteo Smaila Date: Wed, 25 Jun 2025 10:47:18 +0200 Subject: [PATCH 9/9] Applied changes as requested by MichaelWest22. --- src/htmx.js | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/src/htmx.js b/src/htmx.js index 45be81592..f1bd03f20 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -1413,37 +1413,16 @@ var htmx = (function() { * @param {Element} mergeFrom */ function cloneAttributes(mergeTo, mergeFrom) { - const mergeToAttributesCopy = [] - for (const attribute of mergeTo.attributes) { - mergeToAttributesCopy.push(attribute) - } - - const mergeFromAttributesCopy = [] - for (const attribute of mergeFrom.attributes) { - mergeFromAttributesCopy.push(attribute) - } - - const htmxClasses = [] - for (const klass of mergeTo.classList) { - if (klass.startsWith('htmx-')) { - htmxClasses.push(klass) - } - } - - forEach(mergeToAttributesCopy, function(attr) { + forEach(Array.from(mergeTo.attributes), function(attr) { if (!mergeFrom.hasAttribute(attr.name) && shouldSettleAttribute(attr.name)) { mergeTo.removeAttribute(attr.name) } }) - forEach(mergeFromAttributesCopy, function(attr) { + forEach(mergeFrom.attributes, function(attr) { if (shouldSettleAttribute(attr.name)) { mergeTo.setAttribute(attr.name, attr.value) } }) - - for (const htmxClass of htmxClasses) { - mergeTo.classList.add(htmxClass) - } } /** @@ -1989,7 +1968,6 @@ var htmx = (function() { target.classList.remove(htmx.config.swappingClass) forEach(settleInfo.elts, function(elt) { if (elt.classList) { - elt.classList.remove(htmx.config.swappingClass) elt.classList.add(htmx.config.settlingClass) } triggerEvent(elt, 'htmx:afterSwap', swapOptions.eventInfo)